[
  {
    "path": ".github/workflows/ci.yml",
    "content": "# If you change the name, change the link on  the README.md for the badge too\nname: Tests\n\non:\n  push:\n    paths:\n      - sc2/**\n      - examples/**\n      - test/**\n      - docs_generate/**\n      - .pre-commit-config.yaml\n      - generate_dicts_from_data_json.py\n      - generate_id_constants_from_stableid.py\n      - uv.lock\n      - pyproject.toml\n      - README.md\n      - .github/workflows/ci.yml\n  pull_request:\n    branches:\n      - master\n      - develop\n\nenv:\n  # Docker image version, see https://hub.docker.com/r/burnysc2/python-sc2-docker/tags\n  # This version should always lack behind one version behind the docker-ci.yml because it is possible that it doesn't exist\n  VERSION_NUMBER: \"1.0.4\"\n  # TODO Change to '3.14' when a new image has been pushed\n  LATEST_PYTHON_VERSION: \"3.13\"\n  LATEST_SC2_VERSION: \"4.10\"\n\njobs:\n  run_pre_commit_hook:\n    name: Run pre-commit hook\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ env.LATEST_PYTHON_VERSION }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Install dependencies\n        run: |\n          uv sync --frozen --no-cache --no-install-project\n          uv run pre-commit install\n\n      - name: Run pre-commit hooks\n        run: uv run pre-commit run --all-files --hook-stage pre-push\n\n  generate_dicts_from_data_json:\n    name: Generate dicts from data.json\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ env.LATEST_PYTHON_VERSION }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Install dependencies\n        run: |\n          uv sync --frozen --no-cache --no-install-project\n          uv run pre-commit install\n\n      - name: Run generate dicts\n        # Check if newly generated file is the same as existing file\n        # Run pre-commit hook to format files, always return exit code 0 to not end CI run\n        run: |\n          mv sc2/dicts sc2/dicts_old\n          uv run python generate_dicts_from_data_json.py\n          uv run pre-commit run --all-files --hook-stage pre-push || true\n          rm -rf sc2/dicts/__pycache__ sc2/dicts_old/__pycache__\n\n      - name: Upload generated dicts folder as artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: Generated_dicts\n          path: sc2/dicts\n\n      - name: Compare generated dict files\n        # Exit code will be 0 if the results of both commands are equal\n        run: |\n          [[ `ls sc2/dicts | md5sum` == `ls sc2/dicts_old | md5sum` ]]\n\n  run_pytest_tests:\n    # Run pytest tests on pickle files (pre-generated SC2 API observations)\n    name: Run pytest\n    needs: [run_pre_commit_hook, generate_dicts_from_data_json]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 15\n    strategy:\n      fail-fast: false\n      matrix:\n        # Python 3.6 fails due to: https://www.python.org/dev/peps/pep-0563/\n        # If all type annotations were removed, this library should run in py3.6 and perhaps even 3.5\n        # 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\n        # 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)\n        # Python 3.9 support has been dropped since numpy >=2.1.0 (this numpy version is required to run python 3.13)\n        os: [macos-latest, windows-latest, ubuntu-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ matrix.python-version }}\n        id: setup-python\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Install dependencies\n        run: uv sync --frozen --no-cache --no-install-project\n\n      - name: Run pytest\n        run: uv run python -m pytest test\n\n        # Run benchmarks\n      - name: Run benchmark benchmark_array_creation\n        run: uv run python -m pytest test/benchmark_array_creation.py\n\n      - name: Run benchmark benchmark_distance_two_points\n        run: uv run python -m pytest test/benchmark_distance_two_points.py\n\n      - name: Run benchmark benchmark_distances_cdist\n        run: uv run python -m pytest test/benchmark_distances_cdist.py\n\n      - name: Run benchmark benchmark_distances_points_to_point\n        run: uv run python -m pytest test/benchmark_distances_points_to_point.py\n\n      - name: Run benchmark benchmark_distances_units\n        run: uv run python -m pytest test/benchmark_distances_units.py\n\n      - name: Run benchmark benchmark_bot_ai_prepare_units\n        run: uv run python -m pytest test/benchmark_prepare_units.py\n\n      - name: Run benchmark benchmark_bot_ai_init\n        run: uv run python -m pytest test/benchmark_bot_ai_init.py\n\n  run_test_bots:\n    # Run test bots that download the SC2 linux client and run it\n    name: Run testbots linux\n    needs: [run_pytest_tests]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 20\n    strategy:\n      # Do not allow this test to cancel. Finish all jobs regardless of error\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n        sc2-version: [\"4.10\"]\n    env:\n      IMAGE_NAME: burnysc2/python-sc2:local\n\n    steps:\n      # Copy data from repository\n      - uses: actions/checkout@v3\n\n      - name: Print directories and files\n        run: sudo apt-get install tree && tree\n\n      - name: Load and build docker image\n        # Build docker image from Dockerfile using specific python and sc2 version\n        env:\n          BUILD_ARGS: --build-arg PYTHON_VERSION=${{ matrix.python-version }} --build-arg SC2_VERSION=${{ matrix.sc2-version }}\n        run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} .\n\n      - name: Run autotest_bot.py\n        # Run bot and list resulting files (replay file, stable_id.json)\n        run: |\n          docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n          docker exec -i my_container bash -c \"python test/travis_test_script.py test/autotest_bot.py\"\n          docker exec -i my_container bash -c \"tree\"\n          docker rm -f my_container\n\n      - name: Run upgradestest_bot.py\n        run: |\n          docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n          docker exec -i my_container bash -c \"python test/travis_test_script.py test/upgradestest_bot.py\"\n          docker exec -i my_container bash -c \"tree\"\n          docker rm -f my_container\n\n      - name: Run damagetest_bot.py\n        run: |\n          docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n          docker exec -i my_container bash -c \"python test/travis_test_script.py test/damagetest_bot.py\"\n          docker exec -i my_container bash -c \"tree\"\n          docker rm -f my_container\n\n      - name: Run queries_test_bot.py\n        run: |\n          docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n          docker exec -i my_container bash -c \"python test/travis_test_script.py test/queries_test_bot.py\"\n          docker exec -i my_container bash -c \"tree\"\n          docker rm -f my_container\n\n  run_example_bots:\n    # Run example bots against computer\n    name: Run example bots against computer\n    needs: [run_pytest_tests]\n    runs-on: ubuntu-latest\n    timeout-minutes: 60\n    env:\n      IMAGE_NAME: burnysc2/python-sc2-docker:local\n\n    steps:\n      # Copy data from repository\n      - uses: actions/checkout@v3\n\n      - name: Print directories and files\n        run: sudo apt-get install tree && tree\n\n      - name: Load and build docker image\n        # Build docker image from Dockerfile using specific python and sc2 version\n        env:\n          BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }}\n        run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} .\n\n      - name: Run example bots vs computer\n        run: |\n          docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n          docker exec -i my_container bash -c \"python test/run_example_bots_vs_computer.py\"\n          docker exec -i my_container bash -c \"tree\"\n          docker rm -f my_container\n\n  # TODO Fix in main.py \"run_multiple_games\" or \"a_run_multiple_games\" or \"a_run_multiple_games_nokill\"\n  #  run_bot_vs_bot:\n  #    # Run bot vs bot\n  #    name: Run example bots against each other\n  #    needs: [run_pytest_tests]\n  #    timeout-minutes: 60\n  #    env:\n  #      IMAGE_NAME: burnysc2/python-sc2-docker:local\n  #\n  #    steps:\n  #    # Copy data from repository\n  #    - uses: actions/checkout@v3\n  #\n  #    - name: Print directories and files\n  #      run: |\n  #        sudo apt-get install tree\n  #        tree\n  #\n  #    - name: Load and build docker image\n  #      # Build docker image from Dockerfile using specific python and sc2 version\n  #      env:\n  #        BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }}\n  #      run: |\n  #        docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} .\n  #\n  #    - name: Run example bots vs each other\n  #      run: |\n  #        docker run -i -d --name my_container --env 'PYTHONPATH=/root/python-sc2' $IMAGE_NAME\n  #        docker exec -i my_container bash -c \"python test/run_example_bots_vs_each_other.py\"\n  #        docker exec -i my_container bash -c \"tree\"\n  #        docker rm -f my_container\n\n  run_coverage:\n    # Run and upload coverage report\n    # This coverage test does not cover the whole testing range, check /bat_files/rune_code_coverage.bat\n    name: Run coverage\n    needs: [run_test_bots, run_example_bots]\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    env:\n      IMAGE_NAME: burnysc2/python-sc2-docker:local\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Load and build docker image\n        # Build docker image from Dockerfile using specific python and sc2 version\n        env:\n          BUILD_ARGS: --build-arg PYTHON_VERSION=${{ env.LATEST_PYTHON_VERSION }} --build-arg SC2_VERSION=${{ env.LATEST_SC2_VERSION }}\n        run: docker build -f test/Dockerfile -t $IMAGE_NAME $BUILD_ARGS --build-arg VERSION_NUMBER=${{ env.VERSION_NUMBER }} .\n\n      - name: Set up container\n        run: |\n          mkdir htmlcov\n          docker run -i -d \\\n            -v $(pwd)/htmlcov:/root/python-sc2/htmlcov \\\n            --name my_container \\\n            --env 'PYTHONPATH=/root/python-sc2/' \\\n            --entrypoint /bin/bash \\\n            $IMAGE_NAME\n          echo \"Install dev requirements because only non dev requirements exist in the docker image at the moment\"\n          docker exec -i my_container bash -c \"pip install uv \\\n              && uv sync --frozen --no-cache --no-install-project\"\n\n      - name: Run coverage on tests\n        run: docker exec -i my_container bash -c \"uv run pytest --cov=./\"\n\n      - name: Run coverage on autotest_bot.py\n        run: docker exec -i my_container bash -c \"uv run coverage run -a test/travis_test_script.py test/autotest_bot.py\"\n\n      - name: Run coverage on upgradestest_bot.py\n        run: docker exec -i my_container bash -c \"uv run coverage run -a test/travis_test_script.py test/upgradestest_bot.py\"\n\n      - name: Run coverage on damagetest_bot.py\n        run: docker exec -i my_container bash -c \"uv run coverage run -a test/travis_test_script.py test/damagetest_bot.py\"\n\n      - name: Run coverage on queries_test_bot.py\n        run: docker exec -i my_container bash -c \"uv run coverage run -a test/travis_test_script.py test/queries_test_bot.py\"\n\n      # Bots might run differently long each time and create flucuations in code coverage - better to mock behavior instead\n      #    - name: Run coverage on example bots\n      #      run: |\n      #        docker exec -i my_container bash -c \"uv run coverage run -a test/run_example_bots_vs_computer.py\"\n\n      - name: Generate xml coverage file\n        run: |\n          docker exec -i my_container bash -c \"uv run coverage xml\"\n          docker cp my_container:/root/python-sc2/coverage.xml $(pwd)/coverage.xml\n\n      - name: Generate html coverage files in htmlcov/ folder\n        run: |\n          docker exec -i my_container bash -c \"uv run coverage html\"\n          echo \"Upload htmlcov folder because it was mounted in container, so it will be available in host machine\"\n\n      - name: Upload htmlcov/ folder as artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: Coverage_report\n          path: htmlcov\n\n  run_radon:\n    name: Run radon complexity analysis\n    needs: [run_test_bots, run_example_bots]\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ env.LATEST_PYTHON_VERSION }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Install dependencies\n        run: uv sync --frozen --no-cache --no-install-project\n\n      - name: Run uv radon\n        run: uv run radon cc sc2/ -a -nb\n\n  release_to_github_pages:\n    name: GitHub Pages\n    needs: [run_test_bots, run_example_bots]\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ env.LATEST_PYTHON_VERSION }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Install dependencies\n        run: uv sync --frozen --no-cache --no-install-project\n\n      - name: Build docs from scratch\n        run: |\n          echo \"<meta http-equiv=\\\"refresh\\\" content=\\\"0; url=./docs/index.html\\\" />\" > index.html\n          mkdir -p docs\n          cd docs_generate\n          uv run sphinx-build -a -E -b html . ../docs\n\n      - name: Debug-list all generated files\n        run: sudo apt-get install tree && tree\n\n      - name: Publish to Github Pages\n        if: github.ref == 'refs/heads/develop' && github.event_name == 'push'\n        uses: JamesIves/github-pages-deploy-action@releases/v4\n        with:\n          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}\n          BASE_BRANCH: develop # The branch the action should deploy from.\n          BRANCH: gh-pages # The branch the action should deploy to.\n          FOLDER: docs # The folder the action should deploy.\n\n  release_to_pypi:\n    name: Pypi package release\n    needs: [run_test_bots, run_example_bots]\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}\n        uses: actions/setup-python@v4\n        with:\n          python-version: ${{ env.LATEST_PYTHON_VERSION }}\n\n      - name: Install uv\n        run: pip install uv\n\n      - name: Remove test folder to not include it in package\n        run: rm -rf test\n\n      - name: Build package\n        run: uv build\n\n      - name: Publish to pypi\n        if: github.ref == 'refs/heads/develop' && github.event_name == 'push'\n        continue-on-error: true\n        run: uv publish --token ${{ secrets.PYPI_PYTHON_SC2_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/docker-ci.yml",
    "content": "name: Build and push Docker image\n\n# Only run if Dockerfile or docker-ci.yml changed\non:\n  push:\n    paths:\n      - dockerfiles/**\n      - uv.lock\n      - pyproject.toml\n      - .github/workflows/docker-ci.yml\n  pull_request:\n    branches:\n      - master\n      - develop\n\nenv:\n  VERSION_NUMBER: \"1.0.7\"\n  LATEST_PYTHON_VERSION: \"3.14\"\n  LATEST_SC2_VERSION: \"4.10\"\n  EXPERIMENTAL_PYTHON_VERSION: \"3.15\"\n\njobs:\n  download_sc2_maps:\n    name: Download and cache sc2 maps\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 15\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Cache sc2 maps\n        uses: actions/cache@v4\n        id: cache-sc2-maps\n        with:\n          path: |\n            dockerfiles/maps\n          key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }}\n          restore-keys: |\n            ${{ runner.os }}-maps-\n\n      - name: Download sc2 maps\n        run: sh dockerfiles/download_maps.sh\n        if: steps.cache-sc2-maps.outputs.cache-hit != 'true'\n\n  run_test_docker_image:\n    name: Run test_docker_image.sh\n    needs: [download_sc2_maps]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Cache sc2 maps\n        uses: actions/cache@v4\n        id: cache-sc2-maps\n        with:\n          path: |\n            dockerfiles/maps\n          key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }}\n          restore-keys: |\n            ${{ runner.os }}-maps-\n\n      - name: Download sc2 maps\n        run: sh dockerfiles/download_maps.sh\n        if: steps.cache-sc2-maps.outputs.cache-hit != 'true'\n\n      - name: Run shell script\n        env:\n          VERSION_NUMBER: ${{ env.VERSION_NUMBER }}\n          PYTHON_VERSION: ${{ env.LATEST_PYTHON_VERSION }}\n          SC2_VERSION: ${{ env.LATEST_SC2_VERSION }}\n        run: sh dockerfiles/test_docker_image.sh\n\n  run_test_new_python_version:\n    name: Run test_new_python_candidate.sh\n    needs: [download_sc2_maps]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Cache sc2 maps\n        uses: actions/cache@v4\n        id: cache-sc2-maps\n        with:\n          path: |\n            dockerfiles/maps\n          key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }}\n          restore-keys: |\n            ${{ runner.os }}-maps-\n\n      - name: Download sc2 maps\n        run: sh dockerfiles/download_maps.sh\n        if: steps.cache-sc2-maps.outputs.cache-hit != 'true'\n\n      - name: Run shell script\n        continue-on-error: true\n        env:\n          VERSION_NUMBER: ${{ env.VERSION_NUMBER }}\n          PYTHON_VERSION: ${{ env.EXPERIMENTAL_PYTHON_VERSION }}\n          SC2_VERSION: ${{ env.LATEST_SC2_VERSION }}\n        run: sh dockerfiles/test_new_python_candidate.sh\n\n  docker_build:\n    name: Build docker image\n    needs: [download_sc2_maps]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n        python-version: [\"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        sc2-version: [\"4.10\"]\n    env:\n      IMAGE_NAME: burnysc2/python-sc2-docker:py_${{ matrix.python-version }}-sc2_${{ matrix.sc2-version }}\n      BUILD_ARGS: --build-arg PYTHON_VERSION=${{ matrix.python-version }} --build-arg SC2_VERSION=${{ matrix.sc2-version }}\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Cache sc2 maps\n        uses: actions/cache@v4\n        id: cache-sc2-maps\n        with:\n          path: |\n            dockerfiles/maps\n          key: ${{ runner.os }}-maps-${{ hashFiles('dockerfiles/maps/**') }}\n          restore-keys: |\n            ${{ runner.os }}-maps-\n\n      - name: Download sc2 maps\n        run: sh dockerfiles/download_maps.sh\n        if: steps.cache-sc2-maps.outputs.cache-hit != 'true'\n\n      - name: Build docker image\n        run: docker build -f dockerfiles/Dockerfile -t $IMAGE_NAME-v$VERSION_NUMBER $BUILD_ARGS .\n\n      - name: Run test bots on image\n        run: |\n          echo \"Start container, override the default entrypoint\"\n          docker run -i -d \\\n            --name test_container \\\n            --env 'PYTHONPATH=/root/python-sc2/' \\\n            --entrypoint /bin/bash \\\n            $IMAGE_NAME-v$VERSION_NUMBER\n          echo \"Install python-sc2\"\n          docker exec -i test_container mkdir -p /root/python-sc2\n          docker cp pyproject.toml test_container:/root/python-sc2/\n          docker cp uv.lock test_container:/root/python-sc2/\n          docker cp sc2 test_container:/root/python-sc2/sc2\n          docker cp test test_container:/root/python-sc2/test\n          docker cp examples test_container:/root/python-sc2/examples\n          docker exec -i test_container bash -c \"pip install uv \\\n              && cd python-sc2 && uv sync --frozen --no-cache --no-install-project\"\n          echo \"Run various test bots\"\n          docker exec -i test_container bash -c \"cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py\"\n          docker exec -i test_container bash -c \"cd python-sc2 && uv run python test/run_example_bots_vs_computer.py\"\n\n      - name: Login to DockerHub\n        if: github.ref == 'refs/heads/develop' && github.event_name == 'push'\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Upload docker image\n        if: github.ref == 'refs/heads/develop' && github.event_name == 'push'\n        run: docker push $IMAGE_NAME-v$VERSION_NUMBER\n\n      - name: Upload docker image as latest tag\n        if: github.ref == 'refs/heads/develop' && github.event_name == 'push' && matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.sc2-version == env.LATEST_SC2_VERSION\n        run: |\n          docker tag $IMAGE_NAME-v$VERSION_NUMBER burnysc2/python-sc2-docker:latest\n          docker push burnysc2/python-sc2-docker:latest\n"
  },
  {
    "path": ".gitignore",
    "content": "# Misc\n.DS_Store\n\n# Python + mypy + pytest\n__pycache__/\n*.pyc\n\n.mypy_cache/\n.pytest_cache/\n\nbuild/\ndist/\n*.egg-info/\n\n.cache/\n\n# SC2 things\nmaps/\nmini_games/\n\n.hypothesis/\n\n# Editors\n.idea/\n.vscode/\n\n# Code coverage\n.coverage\n/htmlcov\n\ndocs/\n\n.pyre\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v4.4.0\n  hooks:\n    # Check yaml files like this one and github actions if they are valid\n  - id: check-yaml\n    # Check toml files like pyproject.toml if it is valid\n  - id: check-toml\n    # Check if python files are valid\n  - id: check-ast\n  - id: check-builtin-literals\n  - id: check-docstring-first\n  - id: debug-statements\n\n# Check github action workflow files\n- repo: https://github.com/sirosen/check-jsonschema\n  rev: 0.22.0\n  hooks:\n  - id: check-github-workflows\n\n# Convert relative to absolute imports\n- repo: https://github.com/MarcoGorelli/absolufy-imports\n  rev: v0.3.1\n  hooks:\n  - id: absolufy-imports\n\n- repo: https://github.com/pre-commit/pygrep-hooks\n  rev: v1.10.0\n  hooks:\n  # Check for bad code\n  - id: python-no-eval\n  - id: python-no-log-warn\n  # Enforce type annotation instead of comment annotation\n  - id: python-use-type-annotations\n\n- repo: local\n  hooks:\n  # Autoformat code\n  - id: ruff-format-check\n    name: Check if files are formatted\n    stages: [pre-push]\n    language: system\n    # Run the following command to fix:\n    # uv run ruff format . \n    entry: uv run ruff format . --check --diff\n    pass_filenames: false\n\n  - id: ruff-lint\n    name: Lint files\n    stages: [pre-push]\n    language: system\n    # Run the following command to fix:\n    # uv run ruff check . --fix\n    entry: uv run ruff check .\n    pass_filenames: false\n\n  - id: pyrefly\n    name: Static types checking with pyrefly\n    stages: [pre-push]\n    language: system\n    entry: uv run pyrefly check\n    pass_filenames: false\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Hannes Karppila\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Actions Status](https://github.com/BurnySc2/python-sc2/workflows/Tests/badge.svg)](https://github.com/BurnySc2/python-sc2/actions)\n[![codecov](https://codecov.io/gh/BurnySc2/python-sc2/branch/develop/graph/badge.svg?token=Pq5XkKw5VC)](https://codecov.io/gh/BurnySc2/python-sc2)\n\n# A StarCraft II API Client for Python 3\n\nAn 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.\n\n**This library (currently) covers only the raw scripted interface.** At this time I don't intend to add support for graphics-based interfaces.\n\nThe [documentation can be found here](https://burnysc2.github.io/python-sc2/index.html).\nFor 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.\n\nI 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.\n\nFor a list of ongoing changes and differences to the main repository of Dentosal, [check here](https://github.com/BurnySc2/python-sc2/issues/4).\n\n## Installation\n\nBy 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).\n\nFor this fork, you'll need Python 3.9 or newer.\n\nInstall the pypi package:\n```\npip install --upgrade burnysc2\n```\nor directly from develop branch:\n```\npip install --upgrade --force-reinstall https://github.com/BurnySc2/python-sc2/archive/develop.zip\n```\nBoth 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.\n\n## StarCraft II\nYou'll need a StarCraft II executable. If you are running Windows or macOS, just install SC2 from [blizzard app](https://starcraft2.com/).\n\n### Linux installation\n\nYou 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.\nStarcraft II can be directly installed from Battlenet once it is downloaded with Lutris.\nBy default, it will be installed here:\n```\n/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/\n```\nNext, set the following environment variables (either globally or within your development environment, e.g. Pycharm: `Run -> Edit Configurations -> Environment Variables`):\n\n```\nSC2PF=WineLinux\nWINE=/usr/bin/wine\n# Or a wine binary from lutris:\n# WINE=/home/burny/.local/share/lutris/runners/wine/lutris-4.20-x86_64/bin/wine64\n# Default Lutris StarCraftII Installation path:\nSC2PATH='/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/'\n```\n\n### WSL\n\nWhen running WSL in Windows, python-sc2 detects WSL by default and starts Windows Starcraft 2 instead of Linux Starcraft 2.\nIf you wish to instead have the game played in Linux, you can disable this behavior by setting `SC2_WSL_DETECT`\nenvironment variable to \"0\". You can do this inside python with the following code:\n```py\nimport os\nos.environ[\"SC2_WSL_DETECT\"] = \"0\"\n```  \n\nWSL version 1 should not require any configuration. You may be asked to allow Python through your firewall.\n\nWhen running WSL version 2 you need to supply the following environment variables so that your bot can connect:\n\n```\nSC2CLIENTHOST=<your windows IP>\nSC2SERVERHOST=0.0.0.0\n```\n\nIf you are adding these to your .bashrc, you may need to export your environment variables by adding:\n```sh\nexport SC2CLIENTHOST\nexport SC2SERVERHOST\n```\n\nYou can find your Windows IP using `ipconfig /all` from `PowerShell.exe` or `CMD.exe`.\n\n## Maps\nYou will need maps to run the library.\n\n#### Official maps\nOfficial Blizzard map downloads are available from [Blizzard/s2client-proto](https://github.com/Blizzard/s2client-proto#downloads).  \nExtract these maps into their respective *subdirectories* in the SC2 maps directory.  \ne.g. `install-dir/Maps/Ladder2017Season1/`\n\n#### Bot ladder maps\nMaps 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).   \n**Extract these maps into the *root* of the SC2 maps directory** (otherwise ladder replays won't work).  \ne.g. `install-dir/Maps/AcropolisLE.SC2Map`\n\n### Running\n\nAfter 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:\n\n```sh\npython examples/protoss/cannon_rush.py\n```\n\n## Example\n\nAs promised, worker rush in less than twenty lines:\n\n```python\nfrom sc2 import maps\nfrom sc2.player import Bot, Computer\nfrom sc2.main import run_game\nfrom sc2.data import Race, Difficulty\nfrom sc2.bot_ai import BotAI\n\nclass WorkerRushBot(BotAI):\n    async def on_step(self, iteration: int):\n        if iteration == 0:\n            for worker in self.workers:\n                worker.attack(self.enemy_start_locations[0])\n\nrun_game(maps.get(\"Abyssal Reef LE\"), [\n    Bot(Race.Zerg, WorkerRushBot()),\n    Computer(Race.Protoss, Difficulty.Medium)\n], realtime=True)\n```\n\nThis 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.\n\nYou can find more examples in the [`examples/`](/examples) folder.\n\n## API Configuration Options\n\nThe API supports a number of options for configuring how it operates.\n\n### `unit_command_uses_self_do`\nSet this to 'True' if your bot is issueing commands using `self.do(Unit(Ability, Target))` instead of `Unit(Ability, Target)`.\n```python\nclass MyBot(BotAI):\n    def __init__(self):\n        self.unit_command_uses_self_do = True\n```\n\n### `raw_affects_selection`\nSetting this to true improves bot performance by a little bit.\n```python\nclass MyBot(BotAI):\n    def __init__(self):\n        self.raw_affects_selection = True\n```\n\n### `distance_calculation_method`\nThe distance calculation method:\n- 0 for raw python\n- 1 for scipy pdist\n- 2 for scipy cdist\n```python\nclass MyBot(BotAI):\n    def __init__(self):\n        self.distance_calculation_method: int = 2\n```\n\n### `game_step`\nOn game start or in any frame actually, you can set the game step. This controls how often your bot's `step` method is called.  \n__Do not set this in the \\_\\_init\\_\\_ function as the client will not have been initialized yet!__\n```python\nclass MyBot(BotAI):\n    def __init__(self):\n        pass  # don't set it here!\n\n    async def on_start(self):\n        self.client.game_step: int = 2\n```\n\n## Community - Help and support\n\nYou 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.\n\n## Bug reports, feature requests and ideas\n\nIf 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!\n\n\n## Contributing & style guidelines\n\nGit commit messages use [imperative-style messages](https://stackoverflow.com/a/3580764/2867076), start with capital letter and do not have trailing commas.\n\nTo run pre-commit hooks (which run autoformatting and autosort imports) you can run\n```sh\nuv run pre-commit install\nuv run pre-commit run --all-files --hook-stage pre-push\n```\n"
  },
  {
    "path": "data/README.md",
    "content": "This data comes from dentosals tech tree and is only used to generate the dictionaries in /sc2/dicts/:\n\nhttps://github.com/BurnySc2/sc2-techtree\n\nIf you see abilities missing, requirements wrong or anything else related to the /sc2/dicts/, please open and write an issue here:\nhttps://github.com/BurnySc2/sc2-techtree/issues/new"
  },
  {
    "path": "data/data.json",
    "content": "{\"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}}]}"
  },
  {
    "path": "dockerfiles/Dockerfile",
    "content": "# Set up StarCraft II Test Environment for python-sc2 bots (not pysc2 bots!)\nARG PYTHON_VERSION=3.10\n\n# https://docs.astral.sh/uv/guides/integration/docker/#available-images\nFROM ghcr.io/astral-sh/uv:python$PYTHON_VERSION-bookworm-slim AS base\n\nARG SC2_VERSION=4.10\n\n# Debugging purposes\nRUN echo $PYTHON_VERSION\nRUN echo $SC2_VERSION\n\nUSER root\n\n# Update system\nRUN apt-get update \\\n    && apt-get upgrade --assume-yes --quiet=2\n\n# Update and install packages for SC2 development environment\n# gcc to compile packages\n# libc6-dev required by gcc: /usr/local/include/python3.12/Python.h:23:12: fatal error: stdlib.h: No such file or directory\n# git, unzip and wget for download and extraction\n# rename to rename maps\n# tree for debugging\nRUN apt-get install --assume-yes --no-install-recommends --no-show-upgraded \\\n    gcc \\\n    libc6-dev \\\n    git  \\\n    unzip \\\n    curl \\\n    rename \\\n    tree\n\n# Set working directory to root, this uncompresses StarCraftII below to folder /root/StarCraftII\nWORKDIR /root/\n\n# Download and uncompress StarCraftII from https://github.com/Blizzard/s2client-proto#linux-packages and remove zip file\n# If file is locally available, use this instead:\n#COPY SC2.4.10.zip /root/\nRUN curl --retry 5 --retry-delay 5 -C - http://blzdistsc2-a.akamaihd.net/Linux/SC2.$SC2_VERSION.zip -o \"SC2.$SC2_VERSION.zip\" \\\n    && unzip -q -P iagreetotheeula \"SC2.$SC2_VERSION.zip\" \\\n    && rm SC2.$SC2_VERSION.zip\n\n# Remove Battle.net folder\nRUN rm -rf /root/StarCraftII/Battle.net/* \\\n    # Remove Shaders folder\n    && rm -rf /root/StarCraftII/Versions/Shaders*\n\n# Create a symlink for the maps directory\nRUN ln -s /root/StarCraftII/Maps /root/StarCraftII/maps \\\n    # Remove the Maps that come with the SC2 client\n    && rm -rf /root/StarCraftII/maps/*\n\n# See download_maps.sh\nCOPY dockerfiles/maps/* /root/StarCraftII/maps/\n\n# Squash image with trick https://stackoverflow.com/a/56118557/10882657\nFROM scratch\nCOPY --from=base / /\nWORKDIR /root/\nENTRYPOINT [ \"/bin/bash\" ]\n\n# To run a python-sc2 bot:\n# Install python-sc2 and requirements via pip:\n# pip install --upgrade https://github.com/BurnySc2/python-sc2/archive/develop.zip\n\n# To run an example bot, copy one to your container and then run it with python:\n# python /your-bot.py"
  },
  {
    "path": "dockerfiles/README.md",
    "content": "# Dockerfile\nThis dockerfile is meant to be as small as possible, contain only the sc2 binary and maps.\n\nIt is used to run python-sc2 tests.\n\n# Source repo\nhttps://github.com/BurnySc2/python-sc2/tree/develop/dockerfiles\nOriginally but deprecated:\nhttps://github.com/BurnySc2/python-sc2-docker\n\nThis repository is related to the docker image on https://hub.docker.com/r/burnysc2/python-sc2-docker/\n\n# Build locally\nSee build shell script `test_docker_image.sh`\n"
  },
  {
    "path": "dockerfiles/download_maps.sh",
    "content": "#!/bin/bash\n# Run via\n# sh dockerfiles/download_maps.sh\nset -e\n\n# Create maps directory if it doesn't exist\nmkdir -p dockerfiles/maps\ncd dockerfiles/maps\n\n# Download function with retries\ndownload_with_retry() {\n    local url=$1\n    local output=$2\n    curl --retry 5 --retry-delay 5 -C - -L \"$url\" -o \"$output\"\n}\n\n# Download and process sc2ai.net ladder maps\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/9/\" \"1.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/14/\" \"2.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/21/\" \"3.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/35/\" \"4.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/36/\" \"5.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/38/\" \"6.zip\"\ndownload_with_retry \"https://sc2ai.net/wiki/184/plugin/attachments/download/39/\" \"7.zip\"\nunzip -q -o '*.zip'\nrm *.zip\n\n# Download and process official blizzard maps\ndownload_with_retry \"http://blzdistsc2-a.akamaihd.net/MapPacks/Ladder2019Season3.zip\" \"Ladder2019Season3.zip\"\nunzip -q -P iagreetotheeula -o \"Ladder2019Season3.zip\"\nmv Ladder2019Season3/* .\nrm \"Ladder2019Season3.zip\"\nrmdir Ladder2019Season3\n\n# Download and process v5.0.6 maps\ndownload_with_retry \"https://github.com/shostyn/sc2patch/raw/4987d4915b47c801adbc05e297abaa9ca2988838/Maps/506.zip\" \"506.zip\"\nunzip -q -o \"506.zip\"\nrm \"506.zip\"\n\n# Download and process flat/empty maps\ndownload_with_retry \"http://blzdistsc2-a.akamaihd.net/MapPacks/Melee.zip\" \"Melee.zip\"\nunzip -q -P iagreetotheeula -o \"Melee.zip\"\nmv Melee/* .\nrm \"Melee.zip\"\nrmdir Melee\n\n# Remove LE suffix from file names\nfor f in *LE.SC2Map; do \n    mv -- \"$f\" \"${f%LE.SC2Map}.SC2Map\"; \ndone\n"
  },
  {
    "path": "dockerfiles/test_docker_image.sh",
    "content": "# This script is meant for development, which produces fresh images and then runs tests\n# Run via\n# sh dockerfiles/test_docker_image.sh\n\n# Stop on error https://stackoverflow.com/a/2871034/10882657\nset -e\n\n# Set which versions to use\nexport VERSION_NUMBER=${VERSION_NUMBER:-0.9.9}\nexport PYTHON_VERSION=${PYTHON_VERSION:-3.13}\nexport SC2_VERSION=${SC2_VERSION:-4.10}\n\n# For better readability, set local variables\nIMAGE_NAME=burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER\nBUILD_ARGS=\"--build-arg PYTHON_VERSION=$PYTHON_VERSION --build-arg SC2_VERSION=$SC2_VERSION\"\n\n# Build image without context\n# https://stackoverflow.com/a/54666214/10882657\ndocker build -f dockerfiles/Dockerfile -t $IMAGE_NAME $BUILD_ARGS .\n\n# Delete previous container if it exists\ndocker rm -f test_container\n\n# Start container, override the default entrypoint\ndocker run -i -d \\\n  --name test_container \\\n  --env 'PYTHONPATH=/root/python-sc2' \\\n  --entrypoint /bin/bash \\\n  $IMAGE_NAME\n\n# Install requirements\ndocker exec -i test_container mkdir -p /root/python-sc2\ndocker cp pyproject.toml test_container:/root/python-sc2/\ndocker cp uv.lock test_container:/root/python-sc2/\ndocker exec -i test_container bash -c \"pip install uv && cd python-sc2 && uv sync --no-cache --no-install-project\"\n\ndocker cp sc2 test_container:/root/python-sc2/sc2\ndocker cp test test_container:/root/python-sc2/test\n\n# Run various test bots\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py\"\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/travis_test_script.py test/queries_test_bot.py\"\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/travis_test_script.py test/damagetest_bot.py\"\n\ndocker cp examples test_container:/root/python-sc2/examples\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/run_example_bots_vs_computer.py\"\n\n# Command for entering the container to debug if something went wrong:\n# docker exec -it test_container bash\n"
  },
  {
    "path": "dockerfiles/test_new_python_candidate.sh",
    "content": "# This script is meant for development, which produces fresh images and then runs tests\n# If it succeeds, a new python version can be added to CI\n# Run via\n# sh dockerfiles/test_new_python_candidate.sh\n\n# Stop on error https://stackoverflow.com/a/2871034/10882657\nset -e\n\n# Set which versions to use\nexport VERSION_NUMBER=${VERSION_NUMBER:-0.9.9}\nexport PYTHON_VERSION=${PYTHON_VERSION:-3.14}\nexport SC2_VERSION=${SC2_VERSION:-4.10}\n\n# For better readability, set local variables\nIMAGE_NAME=burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER\nBUILD_ARGS=\"--build-arg PYTHON_VERSION=$PYTHON_VERSION --build-arg SC2_VERSION=$SC2_VERSION\"\n\n# Build image\ndocker build -f dockerfiles/Dockerfile -t $IMAGE_NAME $BUILD_ARGS .\n\n# Delete previous container if it exists\ndocker rm -f test_container\n\n# Start container\n# https://docs.docker.com/storage/bind-mounts/#use-a-read-only-bind-mount\ndocker run -i -d \\\n  --name test_container \\\n  --env 'PYTHONPATH=/root/python-sc2' \\\n  --entrypoint /bin/bash \\\n  $IMAGE_NAME\n\n# Install requirements\ndocker exec -i test_container mkdir -p /root/python-sc2\ndocker cp pyproject.toml test_container:/root/python-sc2/\ndocker cp uv.lock test_container:/root/python-sc2/\ndocker exec -i test_container bash -c \"pip install uv && cd python-sc2 && uv sync --no-cache --no-install-project\"\n\ndocker cp sc2 test_container:/root/python-sc2/sc2\ndocker cp test test_container:/root/python-sc2/test\n\n# Run various test bots\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py\"\n\ndocker cp examples test_container:/root/python-sc2/examples\ndocker exec -i test_container bash -c \"cd python-sc2 && uv run python test/run_example_bots_vs_computer.py\"\n"
  },
  {
    "path": "docs_generate/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs_generate/bot_ai/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nbot_ai.py\n****************************\n\nThis basic bot class contains a few helper functions, basic properties and variables to get a simple bot started.\n\nHow to use this information is shown in the bot examples (with comments).\n\n\n.. autoclass:: sc2.bot_ai.BotAI\n   :members:\n.. autoclass:: sc2.bot_ai_internal.BotAIInternal\n   :members:"
  },
  {
    "path": "docs_generate/client/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nclient.py\n****************************\n\n.. autoclass:: sc2.client.Client\n   :members:"
  },
  {
    "path": "docs_generate/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# http://www.sphinx-doc.org/en/master/config\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(\"..\"))  # noqa: PTH100\n\n# -- Project information -----------------------------------------------------\n\nproject = \"python-sc2\"\ncopyright = \"2019, tweakimp BurnySc2\"\nauthor = \"tweakimp, BurnySc2\"\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx_autodoc_typehints\",\n    # https://www.sphinx-doc.org/en/master/usage/extensions/viewcode.html\n    \"sphinx.ext.viewcode\",\n]\n\n# autodoc_typehints options https://github.com/agronholm/sphinx-autodoc-typehints#options\nalways_document_param_types = True\ntypehints_use_signature = True\ntypehints_use_signature_return = True\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\n# Themes to choose from:\n# http://www.sphinx-doc.org/en/stable/theming.html\n# https://www.writethedocs.org/guide/tools/sphinx-themes/\n# https://sphinx-themes.org/\nhtml_theme = \"sphinx_book_theme\"\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n"
  },
  {
    "path": "docs_generate/game_data/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n********************************************************\ngame_data.py\n********************************************************\n\n.. autoclass:: sc2.game_data.GameData\n   :members:\n.. autoclass:: sc2.game_data.AbilityData\n   :members:\n.. autoclass:: sc2.game_data.UnitTypeData\n   :members:\n.. autoclass:: sc2.game_data.UpgradeData\n   :members:\n.. autoclass:: sc2.game_data.Cost\n   :members:\n"
  },
  {
    "path": "docs_generate/game_info/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\ngame_info.py\n****************************\n\n.. autoclass:: sc2.game_info.Ramp\n   :members:\n.. autoclass:: sc2.game_info.GameInfo\n   :members:"
  },
  {
    "path": "docs_generate/game_state/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n********************************************************\ngame_state.py\n********************************************************\n\n.. autoclass:: sc2.game_state.GameState\n   :members:\n.. autoclass:: sc2.game_state.Blip\n   :members:\n.. autoclass:: sc2.game_state.Common\n   :members:\n.. autoclass:: sc2.game_state.EffectData\n   :members:"
  },
  {
    "path": "docs_generate/ids/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nids\n****************************\n\n.. autoclass:: sc2.ids.ability_id.AbilityId\n   :members:\n.. autoclass:: sc2.ids.buff_id.BuffId\n   :members:\n.. autoclass:: sc2.ids.effect_id.EffectId\n   :members:\n.. autoclass:: sc2.ids.unit_typeid.UnitTypeId\n   :members:\n.. autoclass:: sc2.ids.upgrade_id.UpgradeId\n   :members:"
  },
  {
    "path": "docs_generate/index.rst",
    "content": ".. python-sc2 documentation master file, created by\n   sphinx-quickstart on Sat Jun 15 05:52:03 2019.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to python-sc2's documentation!\n======================================\n\n.. toctree::\n   :maxdepth: 2\n\n   text_files/introduction.rst\n   text_files/docker.rst\n   bot_ai/index.rst\n   unit/index.rst\n   units/index.rst\n   game_data/index.rst\n   game_info/index.rst\n   game_state/index.rst\n   client/index.rst\n   position/index.rst\n   pixel_map/index.rst\n   distances/index.rst\n   protocol/index.rst\n   score/index.rst\n   unit_command/index.rst\n   ids/index.rst\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs_generate/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=.\nset BUILDDIR=_build\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\n\n:end\npopd\n"
  },
  {
    "path": "docs_generate/pixel_map/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\npixel_map.py\n****************************\n\n.. autoclass:: sc2.pixel_map.PixelMap\n   :members:"
  },
  {
    "path": "docs_generate/position/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nposition.py\n****************************\n\n.. autoclass:: sc2.position.Pointlike\n   :members:\n.. autoclass:: sc2.position.Point2\n   :members:\n.. autoclass:: sc2.position.Point3\n   :members:\n.. autoclass:: sc2.position.Size\n   :members:\n.. autoclass:: sc2.position.Rect\n   :members:"
  },
  {
    "path": "docs_generate/protocol/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nprotocol.py\n****************************\n\n.. autoclass:: sc2.protocol.Protocol\n   :members:"
  },
  {
    "path": "docs_generate/score/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nscore.py\n****************************\n\n.. autoclass:: sc2.score.ScoreDetails\n   :members:"
  },
  {
    "path": "docs_generate/text_files/docker.rst",
    "content": "*****************************\nUsing docker to run SC2 bots\n*****************************\nThis 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.\nFor a quick summary of commands, scroll to the bottom.\n\nRequirements\n------------\n- Docker installed and running\n- Internet\n- Doesn't require a GPU\n\nPulling the Docker image\n------------------------\nThe 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 ::\n\n    docker pull burnysc2/python-sc2-docker:release-python_3.10-sc2_4.10_arenaclient_burny\n\nDeleting previous containers\n-----------------------------\nTo remove the previously used ``app`` container::\n\n    docker run -it -d --name app test_image\n\nLaunching a new container\n--------------------------\nThe following command launches a new container in interactive mode, which means it will not shut down once it is done running::\n\n    docker run -it -d --name app burnysc2/python-sc2-docker:release-python_3.10-sc2_4.10_arenaclient_burny\n\nInstall bot requirements\n-------------------------\nThe 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.\n\nSince 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\n\nIf 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.\n\nCopying Bots to the container\n------------------------------\nThe bots in the container need to be located under ``app:/root/StarCraftII/Bots/<bot_name>``\nA 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.\nDon'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.\n\nFor more info about competitive bots setup, see:\n\nhttps://github.com/BurnySc2/python-sc2/tree/develop/examples/competitive or https://eschamp.discourse.group/t/simple-starcraft-2-bot-template-to-get-started/155\n\nCopying the runner to the container\n------------------------------------\nYou will have to configure the ``custom_run_local.py`` file (ctrl+f for ``def main()``).\nIt can be found here: https://github.com/BurnySc2/python-sc2/blob/fa4933a1bf89540a052482b1a394c8d6206d7491/bat_files/docker/custom_run_local.py\n\nYou may also customize the arenaclient ``settings.json`` (e.g. max game time) which is located under ``/root/aiarena-client/arenaclient/proxy/settings.json``\nClick here to check which settings are available: https://github.com/BurnySc2/aiarena-client/blob/a1cd2e9314e7fd2accd0e69aa77d89a9978e619c/arenaclient/proxy/server.py#L164-L170\n\nAfter you are done customizing which matches should be played, run the following to finalize your setup::\n\n    docker cp bat_files/docker/custom_run_local.py app:/root/aiarena-client/arenaclient/run_local.py\n\nRunning the match(es)\n---------------------\nNow you are ready to let docker run your matches (headless)::\n\n    docker exec -i app uv run python /root/aiarena-client/arenaclient/run_local.py\n\nCopying the replay from container to host machine\n--------------------------------------------------------------\nTo copy the ``results.json`` to the host machine to analyse the results, use::\n\n    mkdir -p temp\n    docker cp app:/root/aiarena-client/arenaclient/proxy/results.json temp/results.json\n\nTo copy all newly generated replays from the container, use::\n\n    mkdir -p temp/replays\n    docker cp app:/root/StarCraftII/Replays/. temp/replays\n\nSummary using a shell script\n-----------------------------\nFor a full runner script, see:\n\nhttps://github.com/BurnySc2/python-sc2/blob/fa4933a1bf89540a052482b1a394c8d6206d7491/bat_files/docker/docker_run_bots.sh\n\nDocker cleanup\n---------------\nSee also: https://docs.docker.com/config/pruning/\nForce removing all containers (including running)::\n\n    docker rm -f $(docker ps -aq)\n\nRemoving all images::\n\n    docker rmi $(docker images -q)\n\nPrune everything docker related::\n\n    docker system prune --volumes"
  },
  {
    "path": "docs_generate/text_files/introduction.rst",
    "content": "*************\nIntroduction\n*************\n\nThis is an overview to the BurnySc2/python-sc2 library which can be found here: https://github.com/BurnySc2/python-sc2\n\nRequirements\n-------------\n- Python 3.9 or newer\n- StarCraft 2 Client installation in the **default installation path** which should be ``C:\\Program Files (x86)\\StarCraft II``\n\nInstallation\n-------------\nInstall 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``.\n\nAlternatively (of if the command above doesn't work) you can install a specific branch directly from github, here the develop branch::\n\n    pip install --upgrade --force-reinstall https://github.com/BurnySc2/python-sc2/archive/develop.zip\n\nCreating a bot\n---------------\nA basic bot can be made by creating a new file `my_bot.py` and filling it with the following contents::\n\n    import sc2\n    from sc2.bot_ai import BotAI\n    from sc2.player import Bot, Computer\n\n    class MyBot(BotAI):\n        async def on_step(self, iteration: int):\n            print(f\"This is my bot in iteration {iteration}!\")\n\n    sc2.run_game(\n        sc2.maps.get(\"AcropolisLE\"),\n        [Bot(sc2.Race.Zerg, MyBot()), Computer(sc2.Race.Zerg, sc2.Difficulty.Hard)],\n        realtime=False,\n    )\n\nYou can now run the file using command ``python my_bot.py`` or double clicking the file.\n\nA 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.\n\nAvailable information in the game\n------------------------------------\n\nInformation about your bot::\n\n    # Resources and supply\n    self.minerals: int\n    self.vespene: int\n    self.supply_army: int # 0 at game start\n    self.supply_workers: int # 12 at game start\n    self.supply_cap: int # 14 for zerg, 15 for T and P at game start\n    self.supply_used: int # 12 at game start\n    self.supply_left: int # 2 for zerg, 3 for T and P at game start\n\n    # Units\n    self.warp_gate_count: Units # Your warp gate count (only protoss)\n    self.idle_worker_count: int # Workers that are doing nothing\n    self.army_count: int # Amount of army units\n    self.workers: Units # Your workers\n    self.larva: Units # Your larva (only zerg)\n    self.townhalls: Units # Your townhalls (nexus, hatchery, lair, hive, command center, orbital command, planetary fortress\n    self.gas_buildings: Units # Your gas structures (refinery, extractor, assimilator\n    self.units: Units # Your units (includes larva and workers)\n    self.structures: Units # Your structures (includes townhalls and gas buildings)\n\n    # Other information about your bot\n    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)\n    self.player_id: int # Your bot id (can be 1 or 2 in a 2 player game)\n    # Your spawn location (your first townhall location)\n    self.start_location: Point2\n    # Location of your main base ramp, and has some information on how to wall the main base as terran bot (see GameInfo)\n    self.main_base_ramp: Ramp\n\nInformation about the enemy player::\n\n    # The following contains enemy units and structures inside your units' vision range (including invisible units, but not burrowed units)\n    self.enemy_units: Units\n    self.enemy_structures: Units\n\n    # Enemy spawn locations as a list of Point2 points\n    self.enemy_start_locations: list[Point2]\n\n    # Enemy units that are inside your sensor tower range\n    self.blips: set[Blip]\n\n    # The enemy race. If the enemy chose random, this will stay at random forever\n    self.enemy_race: Race\n\nOther information::\n\n    # Neutral units and structures\n    self.mineral_field: Units # All mineral fields on the map\n    self.vespene_geyser: Units # All vespene fields, even those that have a gas building on them\n    self.resources: Units # Both of the above combined\n    self.destructables: Units # All destructable rocks (except the platforms below the main base ramp)\n    self.watchtowers: Units # All watch towers on the map (some maps don't have watch towers)\n    self.all_units: Units # All units combined: yours, enemy's and neutral\n\n    # Locations of possible expansions\n    self.expansion_locations: dict[Point2, Units]\n\n    # Game data about units, abilities and upgrades (see game_data.py)\n    self.game_data: GameData\n\n    # Information about the map: pathing grid, building placement, terrain height, vision and creep are found here (see game_info.py)\n    self.game_info: GameInfo\n\n    # Other information that gets updated every step (see game_state.py)\n    self.state: GameState\n\n    # Extra information\n    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()\n    self.time: float # The current game time in seconds\n    self.time_formatted: str # The current game time properly formatted in 'min:sec'\n\nPossible bot actions\n---------------------\n\nThe 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).\n\nTraining a unit\n^^^^^^^^^^^^^^^^\n\nAssuming 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.\n\nYou 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::\n\n    from sc2.ids.ability_id import AbilityId\n\n    my_larva = self.larva.random\n    my_larva(AbilityId.LARVATRAIN_DRONE)\n\nImportant: 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.\n\nThe last two issues can be fixed by calling it differently, specifically::\n\n    self.larva.random(AbilityId.LARVATRAIN_DRONE, subtract_cost=True, subtract_supply=True)\n\nThe 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.\n\nThere are two more ways to do the same::\n\n    from sc2.ids.unit_typeid import UnitTypeId\n\n    self.larva.random.train(UnitTypeId.DRONE)\n\nThis converts the UnitTypeId to the AbilityId that is required to train the unit.\n\nAnother way is to use the train function from the api::\n\n    self.train(UnitTypeId.DRONE, amount=1)\n\nThis 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.\n\nSo a more performant way to train as many drones as possible is::\n\n    for loop_larva in self.larva:\n        if self.can_afford(UnitTypeId.DRONE):\n            loop_larva.train(UnitTypeId.DRONE)\n            # Add break statement here if you only want to train one\n        else:\n            # Can't afford drones anymore\n            break\n\n``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.\n\nWarning: 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::\n\n    for loop_larva in self.larva:\n        if loop_larva.tag in self.unit_tags_received_action:\n            continue\n        if self.can_afford(UnitTypeId.DRONE):\n            loop_larva.train(UnitTypeId.DRONE)\n            # Add break statement here if you only want to train one\n        else:\n            # Can't afford drones anymore\n            break\n\nBuilding a structure\n^^^^^^^^^^^^^^^^^^^^^\n\nNearly the same procedure is when you want to build a structure. All that is needed is\n\n- Which building type should be built\n- Can you afford building it\n- Which worker should be used\n- Where should the building be placed\n\nThe building type could be ``UnitTypeId.SPAWNINGPOOL``. To check if you can afford it you do ``if self.can_afford(UnitTypeId.SPAWNINGPOOL):``.\n\nFiguring 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.\n\nLastly, figuring out where to place the spawning pool. This can be as easy as::\n\n    map_center = self.game_info.map_center\n    placement_position = self.start_location.towards(map_center, distance=5)\n\nBut 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).\n\nA 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::\n\n    map_center = self.game_info.map_center\n    position_towards_map_center = self.start_location.towards(map_center, distance=5)\n    placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)\n    # Can return None if no position was found\n    if placement_position:\n\nOne 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::\n\n    if self.already_pending(UnitTypeId.SPAWNINGPOOL) + self.structures.filter(lambda structure: structure.type_id == UnitTypeId.SPAWNINGPOOL and structure.is_ready).amount == 0:\n        # Build spawning pool\n\nSo in total: To build a spawning pool in direction of the map center, it is recommended to use::\n\n    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:\n        worker_candidates = self.workers.filter(lambda worker: (worker.is_collecting or worker.is_idle) and worker.tag not in self.unit_tags_received_action)\n        # Worker_candidates can be empty\n        if worker_candidates:\n            map_center = self.game_info.map_center\n            position_towards_map_center = self.start_location.towards(map_center, distance=5)\n            placement_position = await self.find_placement(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)\n            # Placement_position can be None\n            if placement_position:\n                build_worker = worker_candidates.closest_to(placement_position)\n                build_worker.build(UnitTypeId.SPAWNINGPOOL, placement_position)\n\nThe same can be achieved with the convenience function ``self.build`` which automatically picks a worker and internally uses ``self.find_placement``::\n\n    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:\n        map_center = self.game_info.map_center\n        position_towards_map_center = self.start_location.towards(map_center, distance=5)\n        await self.build(UnitTypeId.SPAWNINGPOOL, near=position_towards_map_center, placement_step=1)\n\n"
  },
  {
    "path": "docs_generate/unit/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nunit.py\n****************************\n\n.. autoclass:: sc2.unit.Unit\n   :members:"
  },
  {
    "path": "docs_generate/unit_command/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nunit_command.py\n****************************\n\n.. autoclass:: sc2.unit_command.UnitCommand\n   :members:"
  },
  {
    "path": "docs_generate/units/index.rst",
    "content": ".. toctree::\n   :maxdepth: 2\n\n****************************\nunits.py\n****************************\n\n.. autoclass:: sc2.units.Units\n   :members:"
  },
  {
    "path": "examples/__init__.py",
    "content": "from pathlib import Path\n\n__all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == \".py\" and p.stem != \"__init__\"]\n"
  },
  {
    "path": "examples/arcade_bot.py",
    "content": "\"\"\"\nTo play an arcade map, you need to download the map first.\n\nOpen the StarCraft2 Map Editor through the Battle.net launcher, in the top left go to\nFile -> 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\nHit \"Ok\" and confirm the download. Now that the map is opened, go to \"File -> Save as\" to store it on your hard drive.\nNow load the arcade map by entering your map name below in\nsc2.maps.get(\"YOURMAPNAME\") without the .SC2Map extension\n\n\nMap info:\nYou start with 30 marines, level N has 15+N speed banelings on creep\n\nType in game \"sling\" to activate zergling+baneling combo\nType in game \"stim\" to activate stimpack\n\n\nImprovements that could be made:\n- Make marines constantly run if they have a ling/bane very close to them\n- Split marines before engaging\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot\nfrom sc2.position import Point2, Point3\nfrom sc2.unit import Unit\n\n\nclass MarineSplitChallenge(BotAI):\n    async def on_start(self):\n        await self.chat_send(\"Edit this message for automatic chat commands.\")\n        self.client.game_step = 2\n\n    async def on_step(self, iteration: int):\n        # do marine micro vs zerglings\n        for unit in self.units(UnitTypeId.MARINE):\n            if self.enemy_units:\n                # attack (or move towards) zerglings / banelings\n                if unit.weapon_cooldown <= self.client.game_step / 2:\n                    enemies_in_range = self.enemy_units.filter(unit.target_in_range)\n\n                    # attack lowest hp enemy if any enemy is in range\n                    if enemies_in_range:\n                        # Use stimpack\n                        if (\n                            self.already_pending_upgrade(UpgradeId.STIMPACK) == 1\n                            and not unit.has_buff(BuffId.STIMPACK)\n                            and unit.health > 10\n                        ):\n                            unit(AbilityId.EFFECT_STIM)\n\n                        # attack baneling first\n                        filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.BANELING)\n\n                        if not filtered_enemies_in_range:\n                            filtered_enemies_in_range = enemies_in_range.of_type(UnitTypeId.ZERGLING)\n                        # attack lowest hp unit\n                        lowest_hp_enemy_in_range = min(filtered_enemies_in_range, key=lambda u: u.health)\n                        unit.attack(lowest_hp_enemy_in_range)\n\n                    # no enemy is in attack-range, so give attack command to closest instead\n                    else:\n                        closest_enemy = self.enemy_units.closest_to(unit)\n                        unit.attack(closest_enemy)\n\n                # move away from zergling / banelings\n                else:\n                    stutter_step_positions = self.position_around_unit(unit, distance=4)\n\n                    # filter in pathing grid\n                    stutter_step_positions = {p for p in stutter_step_positions if self.in_pathing_grid(p)}\n\n                    # find position furthest away from enemies and closest to unit\n                    enemies_in_range = self.enemy_units.filter(lambda u: unit.target_in_range(u, -0.5))\n\n                    if stutter_step_positions and enemies_in_range:\n                        retreat_position = max(\n                            stutter_step_positions,\n                            key=lambda x: x.distance_to(enemies_in_range.center) - x.distance_to(unit),\n                        )\n                        unit.move(retreat_position)\n\n                    else:\n                        logger.info(f\"No retreat positions detected for unit {unit} at {unit.position.rounded}.\")\n\n    def position_around_unit(\n        self,\n        pos: Unit | Point2 | Point3,\n        distance: int = 1,\n        step_size: int = 1,\n        exclude_out_of_bounds: bool = True,\n    ):\n        pos = pos.position.rounded\n        positions = {\n            pos.offset(Point2((x, y)))\n            for x in range(-distance, distance + 1, step_size)\n            for y in range(-distance, distance + 1, step_size)\n            if (x, y) != (0, 0)\n        }\n        # filter positions outside map size\n        if exclude_out_of_bounds:\n            positions = {\n                p\n                for p in positions\n                if 0 <= p[0] < self.game_info.pathing_grid.width and 0 <= p[1] < self.game_info.pathing_grid.height\n            }\n        return positions\n\n\ndef main():\n    run_game(\n        maps.get(\"Marine Split Challenge\"),\n        [Bot(Race.Terran, MarineSplitChallenge())],\n        realtime=False,\n        save_replay_as=\"Example.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/bot_vs_bot.py",
    "content": "\"\"\"\nThis script shows how to let two custom bots play against each other.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom loguru import logger\n\nfrom examples.protoss.warpgate_push import WarpGateBot\nfrom examples.zerg.zerg_rush import ZergRushBot\nfrom sc2 import maps\nfrom sc2.data import Race, Result\nfrom sc2.main import GameMatch, run_game, run_multiple_games\nfrom sc2.player import Bot\n\n\ndef main_old():\n    result: Result | list[Result | None] = run_game(\n        maps.get(\"AcropolisLE\"),\n        [\n            Bot(Race.Protoss, WarpGateBot()),\n            Bot(Race.Zerg, ZergRushBot()),\n        ],\n        realtime=False,\n        game_time_limit=2,\n        save_replay_as=\"Example.SC2Replay\",\n    )\n    logger.info(f\"Result: {result}\")\n\n\ndef main():\n    result = run_multiple_games(\n        [\n            GameMatch(\n                map_sc2=maps.get(\"AcropolisLE\"),\n                players=[\n                    Bot(Race.Protoss, WarpGateBot()),\n                    Bot(Race.Zerg, ZergRushBot()),\n                ],\n                realtime=False,\n                game_time_limit=2,\n            )\n        ]\n    )\n    logger.info(f\"Result: {result}\")\n\n\nif __name__ == \"__main__\":\n    main_old()\n    # TODO Why does \"run_multiple_games\" get stuck?\n    # main()\n"
  },
  {
    "path": "examples/competitive/README.md",
    "content": "# Example competitive bot\n\nThis 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/).\n\nCopy 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.\n\nChange the bot race in the [run.py](run.py) (line 8) and in the [ladderbots.json](ladderbots.json) file (line 4).\n\nZip the entire folder to a <YOUR_BOTS_NAME_HERE>.zip file. Make sure that the files are in the root folder of the zip.\nhttps://aiarena.net/wiki/getting-started/#wiki-toc-bot-zip\n\n## AI Arena\n\nTo compete on AI Arena...\n\nMake 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.\n\nMake 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).\n\n## Sc2AI & Probots\n\nThe [ladderbots.json](ladderbots.json) file contains parameters to support play for Sc2AI and Probots. Don't forget to update them!\n\nBoth Sc2AI and Probots will pip install your \"requirements.txt\" file for you.\n"
  },
  {
    "path": "examples/competitive/__init__.py",
    "content": "import argparse\nimport asyncio\n\nimport aiohttp\nfrom loguru import logger\n\nfrom sc2.client import Client\nfrom sc2.main import _play_game\nfrom sc2.portconfig import Portconfig\nfrom sc2.protocol import ConnectionAlreadyClosedError\n\n\n# Run ladder game\n# This lets python-sc2 connect to a LadderManager game: https://github.com/Cryptyc/Sc2LadderServer\n# Based on: https://github.com/Dentosal/python-sc2/blob/master/examples/run_external.py\ndef run_ladder_game(bot):\n    # Load command line arguments\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--GamePort\", type=int, nargs=\"?\", help=\"Game port\")\n    parser.add_argument(\"--StartPort\", type=int, nargs=\"?\", help=\"Start port\")\n    parser.add_argument(\"--LadderServer\", type=str, nargs=\"?\", help=\"Ladder server\")\n    parser.add_argument(\"--ComputerOpponent\", type=str, nargs=\"?\", help=\"Computer opponent\")\n    parser.add_argument(\"--ComputerRace\", type=str, nargs=\"?\", help=\"Computer race\")\n    parser.add_argument(\"--ComputerDifficulty\", type=str, nargs=\"?\", help=\"Computer difficulty\")\n    parser.add_argument(\"--OpponentId\", type=str, nargs=\"?\", help=\"Opponent ID\")\n    parser.add_argument(\"--RealTime\", action=\"store_true\", help=\"Real time flag\")\n    args, _unknown = parser.parse_known_args()\n\n    host = \"127.0.0.1\" if args.LadderServer is None else args.LadderServer\n\n    host_port = args.GamePort\n    lan_port = args.StartPort\n\n    # Add opponent_id to the bot class (accessed through self.opponent_id)\n    bot.ai.opponent_id = args.OpponentId\n\n    realtime = args.RealTime\n\n    # Port config\n    if lan_port is None:\n        portconfig = None\n    else:\n        ports = [lan_port + p for p in range(1, 6)]\n\n        portconfig = Portconfig()\n        portconfig.server = [ports[1], ports[2]]\n        portconfig.players = [[ports[3], ports[4]]]\n\n    # Join ladder game\n    g = join_ladder_game(host=host, port=host_port, players=[bot], realtime=realtime, portconfig=portconfig)\n\n    # Run it\n    result = asyncio.get_event_loop().run_until_complete(g)\n    return result, args.OpponentId\n\n\n# 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)\nasync def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, game_time_limit=None):\n    ws_url = f\"ws://{host}:{port}/sc2api\"\n    # pyrefly: ignore\n    ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120)\n    client = Client(ws_connection)\n    try:\n        result = await _play_game(players[0], client, realtime, portconfig, game_time_limit)\n        if save_replay_as is not None:\n            await client.save_replay(save_replay_as)\n        # await client.leave()\n        # await client.quit()\n    except ConnectionAlreadyClosedError:\n        logger.error(\"Connection was closed before the game ended\")\n        return None\n    finally:\n        await ws_connection.close()\n\n    return result\n"
  },
  {
    "path": "examples/competitive/bot.py",
    "content": "from sc2.bot_ai import BotAI\nfrom sc2.data import Result\n\n\nclass CompetitiveBot(BotAI):\n    async def on_start(self):\n        print(\"Game started\")\n        # Do things here before the game starts\n\n    async def on_step(self, iteration: int):\n        # Populate this function with whatever your bot should do!\n        pass\n\n    async def on_end(self, game_result: Result):\n        print(\"Game ended.\")\n        # Do things here after the game ends\n"
  },
  {
    "path": "examples/competitive/ladderbots.json",
    "content": "{\n    \"Bots\": {\n        \"YOUR_BOTS_NAME_HERE\": {\n            \"Race\": \"Put Terran Zerg or Protoss here\",\n            \"Type\": \"Python\",\n            \"RootPath\": \"./\",\n            \"FileName\": \"run.py\",\n            \"Args\": \"-O\",\n            \"Debug\": true,\n            \"SurrenderPhrase\": \"(pineapple)\"\n        }\n    }\n}"
  },
  {
    "path": "examples/competitive/run.py",
    "content": "import sys\n\nfrom examples.competitive.__init__ import run_ladder_game\n\n# Load bot\nfrom examples.competitive.bot import CompetitiveBot\n\nfrom sc2 import maps\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\nbot = Bot(Race.Terran, CompetitiveBot())\n\n# Start game\nif __name__ == \"__main__\":\n    if \"--LadderServer\" in sys.argv:\n        # Ladder game started by LadderManager\n        print(\"Starting ladder game...\")\n        result, opponentid = run_ladder_game(bot)\n        print(result, \" against opponent \", opponentid)\n    else:\n        # Local game\n        print(\"Starting local game...\")\n        run_game(maps.get(\"Abyssal Reef LE\"), [bot, Computer(Race.Protoss, Difficulty.VeryHard)], realtime=True)\n"
  },
  {
    "path": "examples/distributed_workers.py",
    "content": "from sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass TerranBot(BotAI):\n    async def on_step(self, iteration: int):\n        await self.distribute_workers()\n        await self.build_supply()\n        await self.build_workers()\n        await self.expand()\n\n    async def build_workers(self):\n        for cc in self.townhalls(UnitTypeId.COMMANDCENTER).ready.idle:\n            if self.can_afford(UnitTypeId.SCV):\n                cc.train(UnitTypeId.SCV)\n\n    async def expand(self):\n        if self.townhalls(UnitTypeId.COMMANDCENTER).amount < 3 and self.can_afford(UnitTypeId.COMMANDCENTER):\n            await self.expand_now()\n\n    async def build_supply(self):\n        ccs = self.townhalls(UnitTypeId.COMMANDCENTER).ready\n        if ccs.exists:\n            cc = ccs.first\n            if self.supply_left < 4 and not self.already_pending(UnitTypeId.SUPPLYDEPOT):\n                if self.can_afford(UnitTypeId.SUPPLYDEPOT):\n                    await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 5))\n\n\nif __name__ == \"__main__\":\n    run_game(\n        maps.get(\"Abyssal Reef LE\"),\n        [Bot(Race.Terran, TerranBot()), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=False,\n    )\n"
  },
  {
    "path": "examples/external_bot.py",
    "content": "from pathlib import Path\n\nfrom sc2 import maps\nfrom sc2.data import Race\nfrom sc2.main import GameMatch, run_multiple_games\nfrom sc2.player import BotProcess, Computer\n\n\ndef main():\n    run_multiple_games(\n        [\n            GameMatch(\n                maps.get(\"AcropolisLE\"),\n                [\n                    # Enable up to 2 of the 4 following bots to test this file\n                    # Assuming you launch external_bot.py from the root directory of 'python-sc2'\n                    BotProcess(\n                        Path.cwd(),\n                        [\"python\", \"examples/competitive/run.py\"],\n                        Race.Terran,\n                        \"CompetiveBot\",\n                        stdout=\"temp.txt\",\n                    ),\n                    # Bot(Race.Zerg, ZergRushBot()),\n                    # Bot(Race.Zerg, ZergRushBot()),\n                    Computer(Race.Zerg),\n                ],\n                realtime=True,\n            ),\n        ]\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/fastreload.py",
    "content": "from importlib import reload\n\nfrom examples.zerg import zerg_rush\nfrom sc2 import maps\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import _host_game_iter\nfrom sc2.player import AbstractPlayer, Bot, Computer\n\n\ndef main():\n    player_config: list[AbstractPlayer] = [\n        Bot(Race.Zerg, zerg_rush.ZergRushBot()),\n        Computer(Race.Terran, Difficulty.Medium),\n    ]\n\n    gen = _host_game_iter(maps.get(\"Abyssal Reef LE\"), player_config, realtime=False)\n\n    _r = next(gen)\n    while True:\n        input(\"Press enter to reload \")\n\n        reload(zerg_rush)\n        if isinstance(player_config[0], Bot):\n            player_config[0].ai = zerg_rush.ZergRushBot()\n        gen.send(player_config)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/host_external_norestart.py",
    "content": "from examples.zerg.zerg_rush import ZergRushBot\nfrom sc2 import maps\nfrom sc2.data import Race\nfrom sc2.main import _host_game_iter\nfrom sc2.player import Bot\nfrom sc2.portconfig import Portconfig\n\n\ndef main():\n    portconfig: Portconfig = Portconfig()\n    print(portconfig.as_json)\n\n    # pyrefly: ignore\n    player_config = [Bot(Race.Zerg, ZergRushBot()), Bot(Race.Zerg, None)]\n\n    for g in _host_game_iter(maps.get(\"Abyssal Reef LE\"), player_config, realtime=False, portconfig=portconfig):\n        print(g)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/observer_easy_vs_easy.py",
    "content": "from examples.protoss.cannon_rush import CannonRushBot\nfrom sc2 import maps\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\ndef main():\n    run_game(\n        maps.get(\"Abyssal Reef LE\"),\n        [Bot(Race.Protoss, CannonRushBot()), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=True,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/play_tvz.py",
    "content": "\"\"\"\nThis script let's you play as human against a simple zerg rush bot.\n\"\"\"\n\nfrom examples.zerg.zerg_rush import ZergRushBot\nfrom sc2 import maps\nfrom sc2.data import Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Human\n\n\ndef main():\n    run_game(maps.get(\"Abyssal Reef LE\"), [Human(Race.Terran), Bot(Race.Zerg, ZergRushBot())], realtime=True)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/protoss/__init__.py",
    "content": "from pathlib import Path\n\n__all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == \".py\" and p.stem != \"__init__\"]\n"
  },
  {
    "path": "examples/protoss/cannon_rush.py",
    "content": "import random\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass CannonRushBot(BotAI):\n    async def on_step(self, iteration: int):\n        if iteration == 0:\n            await self.chat_send(\"(probe)(pylon)(cannon)(cannon)(gg)\")\n\n        if not self.townhalls:\n            # 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\n            for worker in self.workers:\n                worker.attack(self.enemy_start_locations[0])\n            return\n\n        nexus = self.townhalls.random\n\n        # Make probes until we have 16 total\n        if self.supply_workers < 16 and nexus.is_idle:\n            if self.can_afford(UnitTypeId.PROBE):\n                nexus.train(UnitTypeId.PROBE)\n\n        # If we have no pylon, build one near starting nexus\n        elif not self.structures(UnitTypeId.PYLON) and self.already_pending(UnitTypeId.PYLON) == 0:\n            if self.can_afford(UnitTypeId.PYLON):\n                await self.build(UnitTypeId.PYLON, near=nexus)\n\n        # If we have no forge, build one near the pylon that is closest to our starting nexus\n        elif not self.structures(UnitTypeId.FORGE):\n            pylon_ready = self.structures(UnitTypeId.PYLON).ready\n            if pylon_ready:\n                if self.can_afford(UnitTypeId.FORGE):\n                    await self.build(UnitTypeId.FORGE, near=pylon_ready.closest_to(nexus))\n\n        # If we have less than 2 pylons, build one at the enemy base\n        elif self.structures(UnitTypeId.PYLON).amount < 2:\n            if self.can_afford(UnitTypeId.PYLON):\n                pos = self.enemy_start_locations[0].towards(self.game_info.map_center, random.randrange(8, 15))\n                await self.build(UnitTypeId.PYLON, near=pos)\n\n        # If we have no cannons but at least 2 completed pylons, automatically find a placement location and build them near enemy start location\n        elif not self.structures(UnitTypeId.PHOTONCANNON):\n            if self.structures(UnitTypeId.PYLON).ready.amount >= 2 and self.can_afford(UnitTypeId.PHOTONCANNON):\n                pylon = self.structures(UnitTypeId.PYLON).closer_than(20, self.enemy_start_locations[0]).random\n                await self.build(UnitTypeId.PHOTONCANNON, near=pylon)\n\n        # Decide if we should make pylon or cannons, then build them at random location near enemy spawn\n        elif self.can_afford(UnitTypeId.PYLON) and self.can_afford(UnitTypeId.PHOTONCANNON):\n            # Ensure \"fair\" decision\n            for _ in range(20):\n                pos = self.enemy_start_locations[0].random_on_distance(random.randrange(5, 12))\n                building = UnitTypeId.PHOTONCANNON if self.state.psionic_matrix.covers(pos) else UnitTypeId.PYLON\n                await self.build(building, near=pos)\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Protoss, CannonRushBot(), name=\"CheeseCannon\"), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/protoss/find_adept_shades.py",
    "content": "import math\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\n\n\nclass FindAdeptShadesBot(BotAI):\n    def __init__(self):\n        self.shaded = False\n        self.shades_mapping: dict[int, int] = {}\n\n    async def on_start(self):\n        self.client.game_step = 2\n        await self.client.debug_create_unit(\n            [(UnitTypeId.ADEPT, 10, self.townhalls[0].position.towards(self.game_info.map_center, 5), 1)]\n        )\n\n    async def on_step(self, iteration: int):\n        adepts = self.units(UnitTypeId.ADEPT)\n        if adepts and not self.shaded:\n            # Wait for adepts to spawn and then cast ability\n            for adept in adepts:\n                adept(AbilityId.ADEPTPHASESHIFT_ADEPTPHASESHIFT, self.game_info.map_center)\n            self.shaded = True\n        elif self.shades_mapping:\n            # Debug log and draw a line between the two units\n            for adept_tag, shade_tag in self.shades_mapping.items():\n                adept = self.units.find_by_tag(adept_tag)\n                shade = self.units.find_by_tag(shade_tag)\n                if shade:\n                    # logger.info(f\"Remaining shade time: {shade.buff_duration_remain} / {shade.buff_duration_max}\")\n                    pass\n                if adept and shade:\n                    self.client.debug_line_out(adept, shade, (0, 255, 0))\n            # logger.info(self.shades_mapping)\n        elif self.shaded:\n            # Find shades\n            shades = self.units(UnitTypeId.ADEPTPHASESHIFT)\n            for shade in shades:\n                remaining_adepts = adepts.tags_not_in(self.shades_mapping)\n                # Figure out where the shade should have been \"self.client.game_step\"-frames ago\n                forward_position = Point2(\n                    (shade.position.x + math.cos(shade.facing), shade.position.y + math.sin(shade.facing))\n                )\n                previous_shade_location = shade.position.towards(\n                    forward_position, -(self.client.game_step / 16) * shade.movement_speed\n                )  # See docstring of movement_speed attribute\n\n                closest_adept = remaining_adepts.closest_to(previous_shade_location)\n                self.shades_mapping[closest_adept.tag] = shade.tag\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Protoss, FindAdeptShadesBot()), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/protoss/threebase_voidray.py",
    "content": "from sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass ThreebaseVoidrayBot(BotAI):\n    async def on_step(self, iteration: int):\n        target_base_count = 3\n        target_stargate_count = 3\n\n        if iteration == 0:\n            await self.chat_send(\"(glhf)\")\n\n        if not self.townhalls.ready:\n            # 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\n            for worker in self.workers:\n                worker.attack(self.enemy_start_locations[0])\n            return\n\n        nexus = self.townhalls.ready.random\n\n        # If this random nexus is not idle and has not chrono buff, chrono it with one of the nexuses we have\n        if not nexus.is_idle and not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST):\n            nexuses = self.structures(UnitTypeId.NEXUS)\n            abilities = await self.get_available_abilities(nexuses)\n            for loop_nexus, abilities_nexus in zip(nexuses, abilities):\n                if AbilityId.EFFECT_CHRONOBOOSTENERGYCOST in abilities_nexus:\n                    loop_nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus)\n                    break\n\n        # If we have at least 5 void rays, attack closes enemy unit/building, or if none is visible: attack move towards enemy spawn\n        if self.units(UnitTypeId.VOIDRAY).amount > 5:\n            for vr in self.units(UnitTypeId.VOIDRAY):\n                # Activate charge ability if the void ray just attacked\n                if vr.weapon_cooldown > 0:\n                    vr(AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT)\n                # Choose target and attack, filter out invisible targets\n                targets = (self.enemy_units | self.enemy_structures).filter(lambda unit: unit.can_be_attacked)\n                if targets:\n                    target = targets.closest_to(vr)\n                    vr.attack(target)\n                else:\n                    vr.attack(self.enemy_start_locations[0])\n\n        # Distribute workers in gas and across bases\n        await self.distribute_workers()\n\n        # If we are low on supply, build pylon\n        if (\n            self.supply_left < 2\n            and self.already_pending(UnitTypeId.PYLON) == 0\n            or self.supply_used > 15\n            and self.supply_left < 4\n            and self.already_pending(UnitTypeId.PYLON) < 2\n        ):\n            # Always check if you can afford something before you build it\n            if self.can_afford(UnitTypeId.PYLON):\n                await self.build(UnitTypeId.PYLON, near=nexus)\n\n        # Train probe on nexuses that are undersaturated (avoiding distribute workers functions)\n        # if nexus.assigned_harvesters < nexus.ideal_harvesters and nexus.is_idle:\n        if self.supply_workers + self.already_pending(UnitTypeId.PROBE) < self.townhalls.amount * 22 and nexus.is_idle:\n            if self.can_afford(UnitTypeId.PROBE):\n                nexus.train(UnitTypeId.PROBE)\n\n        # If we have less than 3 nexuses and none pending yet, expand\n        if self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) < 3:\n            if self.can_afford(UnitTypeId.NEXUS):\n                await self.expand_now()\n\n        # Once we have a pylon completed\n        if self.structures(UnitTypeId.PYLON).ready:\n            pylon = self.structures(UnitTypeId.PYLON).ready.random\n            if self.structures(UnitTypeId.GATEWAY).ready:\n                # If we have gateway completed, build cyber core\n                if not self.structures(UnitTypeId.CYBERNETICSCORE):\n                    if (\n                        self.can_afford(UnitTypeId.CYBERNETICSCORE)\n                        and self.already_pending(UnitTypeId.CYBERNETICSCORE) == 0\n                    ):\n                        await self.build(UnitTypeId.CYBERNETICSCORE, near=pylon)\n            else:\n                # If we have no gateway, build gateway\n                if self.can_afford(UnitTypeId.GATEWAY) and self.already_pending(UnitTypeId.GATEWAY) == 0:\n                    await self.build(UnitTypeId.GATEWAY, near=pylon)\n\n        # Build gas near completed nexuses once we have a cybercore (does not need to be completed\n        if self.structures(UnitTypeId.CYBERNETICSCORE):\n            for nexus in self.townhalls.ready:\n                vgs = self.vespene_geyser.closer_than(15, nexus)\n                for vg in vgs:\n                    if self.can_afford(UnitTypeId.ASSIMILATOR):\n                        worker = self.select_build_worker(vg.position)\n                        if worker is not None:\n                            if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg):\n                                worker.build_gas(vg)\n                                worker.stop(queue=True)\n\n        # If we have less than 3  but at least 3 nexuses, build stargate\n        if self.structures(UnitTypeId.PYLON).ready and self.structures(UnitTypeId.CYBERNETICSCORE).ready:\n            pylon = self.structures(UnitTypeId.PYLON).ready.random\n            if (\n                self.townhalls.ready.amount + self.already_pending(UnitTypeId.NEXUS) >= target_base_count\n                and self.structures(UnitTypeId.STARGATE).ready.amount + self.already_pending(UnitTypeId.STARGATE)\n                < target_stargate_count\n            ):\n                if self.can_afford(UnitTypeId.STARGATE):\n                    await self.build(UnitTypeId.STARGATE, near=pylon)\n\n        # Save up for expansions, loop over idle completed stargates and queue void ray if we can afford\n        if self.townhalls.amount >= 3:\n            for sg in self.structures(UnitTypeId.STARGATE).ready.idle:\n                if self.can_afford(UnitTypeId.VOIDRAY):\n                    sg.train(UnitTypeId.VOIDRAY)\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Protoss, ThreebaseVoidrayBot()), Computer(Race.Protoss, Difficulty.Easy)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/protoss/warpgate_push.py",
    "content": "from loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.unit import Unit\n\n\nclass WarpGateBot(BotAI):\n    def __init__(self):\n        # Initialize inherited class\n        self.proxy_built = False\n\n    async def warp_new_units(self, proxy: Unit):\n        for warpgate in self.structures(UnitTypeId.WARPGATE).ready:\n            abilities = await self.get_available_abilities([warpgate])\n            # all the units have the same cooldown anyway so let's just look at ZEALOT\n            if AbilityId.WARPGATETRAIN_STALKER in abilities[0]:\n                pos = proxy.position.to2.random_on_distance(4)\n                placement = await self.find_placement(AbilityId.WARPGATETRAIN_STALKER, pos, placement_step=1)\n                if placement is None:\n                    # return ActionResult.CantFindPlacementLocation\n                    logger.info(\"can't place\")\n                    return\n                warpgate.warp_in(UnitTypeId.STALKER, placement)\n\n    async def on_step(self, iteration: int):\n        await self.distribute_workers()\n\n        if not self.townhalls.ready:\n            # 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\n            for worker in self.workers:\n                worker.attack(self.enemy_start_locations[0])\n            return\n\n        nexus = self.townhalls.ready.random\n\n        # Build pylon when on low supply\n        if self.supply_left < 2 and self.already_pending(UnitTypeId.PYLON) == 0:\n            # Always check if you can afford something before you build it\n            if self.can_afford(UnitTypeId.PYLON):\n                await self.build(UnitTypeId.PYLON, near=nexus)\n            return\n\n        if self.workers.amount < self.townhalls.amount * 22 and nexus.is_idle:\n            if self.can_afford(UnitTypeId.PROBE):\n                nexus.train(UnitTypeId.PROBE)\n\n        elif self.structures(UnitTypeId.PYLON).amount < 5 and self.already_pending(UnitTypeId.PYLON) == 0:\n            if self.can_afford(UnitTypeId.PYLON):\n                await self.build(UnitTypeId.PYLON, near=nexus.position.towards(self.game_info.map_center, 5))\n\n        proxy = None\n        if self.structures(UnitTypeId.PYLON).ready:\n            proxy = self.structures(UnitTypeId.PYLON).closest_to(self.enemy_start_locations[0])\n            pylon = self.structures(UnitTypeId.PYLON).ready.random\n            if self.structures(UnitTypeId.GATEWAY).ready:\n                # If we have no cyber core, build one\n                if not self.structures(UnitTypeId.CYBERNETICSCORE):\n                    if (\n                        self.can_afford(UnitTypeId.CYBERNETICSCORE)\n                        and self.already_pending(UnitTypeId.CYBERNETICSCORE) == 0\n                    ):\n                        await self.build(UnitTypeId.CYBERNETICSCORE, near=pylon)\n            # Build up to 4 gates\n            if (\n                self.can_afford(UnitTypeId.GATEWAY)\n                and self.structures(UnitTypeId.WARPGATE).amount + self.structures(UnitTypeId.GATEWAY).amount < 4\n            ):\n                await self.build(UnitTypeId.GATEWAY, near=pylon)\n\n        # Build gas\n        for nexus in self.townhalls.ready:\n            vgs = self.vespene_geyser.closer_than(15, nexus)\n            for vg in vgs:\n                if self.can_afford(UnitTypeId.ASSIMILATOR):\n                    worker = self.select_build_worker(vg.position)\n                    if worker is not None:\n                        if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg):\n                            worker.build_gas(vg)\n                            worker.stop(queue=True)\n\n        # Research warp gate if cybercore is completed\n        if (\n            self.structures(UnitTypeId.CYBERNETICSCORE).ready\n            and self.can_afford(AbilityId.RESEARCH_WARPGATE)\n            and self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 0\n        ):\n            ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.first\n            ccore.research(UpgradeId.WARPGATERESEARCH)\n\n        # Morph to warp gate when research is complete\n        for gateway in self.structures(UnitTypeId.GATEWAY).ready.idle:\n            if self.already_pending_upgrade(UpgradeId.WARPGATERESEARCH) == 1:\n                gateway(AbilityId.MORPH_WARPGATE)\n\n        if self.proxy_built and proxy:\n            await self.warp_new_units(proxy)\n\n        # Make stalkers attack either closest enemy unit or enemy spawn location\n        if self.units(UnitTypeId.STALKER).amount > 3:\n            for stalker in self.units(UnitTypeId.STALKER).ready.idle:\n                targets = (self.enemy_units | self.enemy_structures).filter(lambda unit: unit.can_be_attacked)\n                if targets:\n                    target = targets.closest_to(stalker)\n                    stalker.attack(target)\n                else:\n                    stalker.attack(self.enemy_start_locations[0])\n\n        # Build proxy pylon\n        if (\n            self.structures(UnitTypeId.CYBERNETICSCORE).amount >= 1\n            and not self.proxy_built\n            and self.can_afford(UnitTypeId.PYLON)\n        ):\n            p = self.game_info.map_center.towards(self.enemy_start_locations[0], 20)\n            await self.build(UnitTypeId.PYLON, near=p)\n            self.proxy_built = True\n\n        # Chrono nexus if cybercore is not ready, else chrono cybercore\n        if not self.structures(UnitTypeId.CYBERNETICSCORE).ready:\n            if not nexus.has_buff(BuffId.CHRONOBOOSTENERGYCOST) and not nexus.is_idle:\n                if nexus.energy >= 50:\n                    nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus)\n        else:\n            ccore = self.structures(UnitTypeId.CYBERNETICSCORE).ready.first\n            if not ccore.has_buff(BuffId.CHRONOBOOSTENERGYCOST) and not ccore.is_idle:\n                if nexus.energy >= 50:\n                    nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, ccore)\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Protoss, WarpGateBot()), Computer(Race.Protoss, Difficulty.Easy)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/simulate_fight_scenario.py",
    "content": "from loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\n\nMY_PLAYER_ID = 1\nOPPONENT_PLAYER_ID = 2\n\n\nclass FightBot(BotAI):\n    def __init__(self):\n        super().__init__()\n        self.enemy_location: Point2 | None = None\n        self.fight_started = False\n\n    async def on_start(self):\n        # Retrieve control by enabling enemy control and showing whole map\n        await self.client.debug_show_map()\n        await self.client.debug_control_enemy()\n\n    async def on_step(self, iteration: int):\n        # Wait till control retrieved, destroy all starting units, recreate the world\n        if iteration > 0 and self.enemy_units and not self.enemy_location:\n            await self.reset_arena()\n\n        if (self.units or self.structures) and (self.enemy_units or self.enemy_structures):\n            self.enemy_location = (self.enemy_units + self.enemy_structures).center\n            self.fight_started = True\n\n        await self.manage_enemy_units()\n        await self.manage_own_units()\n\n        # In case of no units left - do not wait for game to finish\n        if self.fight_started and (not self.units or not self.enemy_units):\n            logger.info(\"LOSE\" if not self.units else \"WIN\")\n            await self.client.quit()  # or reset level\n            return\n\n    async def reset_arena(self):\n        if self.enemy_location is None:\n            return\n        await self.client.debug_kill_unit(self.all_units)\n\n        await self.client.debug_create_unit(\n            [\n                (UnitTypeId.SUPPLYDEPOT, 1, self.enemy_location, OPPONENT_PLAYER_ID),\n                (UnitTypeId.MARINE, 4, self.enemy_location.towards(self.start_location, 8), OPPONENT_PLAYER_ID),\n            ]\n        )\n\n        await self.client.debug_create_unit(\n            [\n                (UnitTypeId.SUPPLYDEPOT, 1, self.start_location, MY_PLAYER_ID),\n                (UnitTypeId.MARINE, 4, self.start_location.towards(self.enemy_location, 8), MY_PLAYER_ID),\n            ]\n        )\n\n    async def manage_enemy_units(self):\n        for unit in self.enemy_units:\n            unit.attack(self.start_location)\n\n    async def manage_own_units(self):\n        if self.enemy_location is None:\n            return\n        for unit in self.units(UnitTypeId.MARINE):\n            unit.attack(self.enemy_location)\n            # TODO: implement your fight logic here\n            # if unit.weapon_cooldown != 0:\n            #     unit.move(u.position.towards(self.start_location))\n            # else:\n            #     unit.attack(self.enemy_location)\n            # pass\n\n\ndef main():\n    run_game(\n        maps.get(\"Flat64\"),\n        # NOTE: you can have two bots fighting with each other here\n        [Bot(Race.Terran, FightBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=True,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/terran/__init__.py",
    "content": "from pathlib import Path\n\n__all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == \".py\" and p.stem != \"__init__\"]\n"
  },
  {
    "path": "examples/terran/cyclone_push.py",
    "content": "from sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass CyclonePush(BotAI):\n    def select_target(self) -> Point2:\n        # Pick a random enemy structure's position\n        targets = self.enemy_structures\n        if targets:\n            return targets.random.position\n\n        # Pick a random enemy unit's position\n        targets = self.enemy_units\n        if targets:\n            return targets.random.position\n\n        # Pick enemy start location if it has no friendly units nearby\n        if min(unit.distance_to(self.enemy_start_locations[0]) for unit in self.units) > 5:\n            return self.enemy_start_locations[0]\n\n        # Pick a random mineral field on the map\n        return self.mineral_field.random.position\n\n    async def on_step(self, iteration: int):\n        CCs: Units = self.townhalls(UnitTypeId.COMMANDCENTER)\n        # If no command center exists, attack-move with all workers and cyclones\n        if not CCs:\n            target = self.structures.random_or(self.enemy_start_locations[0]).position\n            for unit in self.workers | self.units(UnitTypeId.CYCLONE):\n                unit.attack(target)\n            return\n\n        # Otherwise, grab the first command center from the list of command centers\n        cc: Unit = CCs.first\n\n        # Every 50 iterations (here: every 50*8 = 400 frames)\n        if iteration % 50 == 0 and self.units(UnitTypeId.CYCLONE).amount > 2:\n            target: Point2 = self.select_target()\n            forces: Units = self.units(UnitTypeId.CYCLONE)\n            # Every 4000 frames: send all forces to attack-move the target position\n            if iteration % 500 == 0:\n                for unit in forces:\n                    unit.attack(target)\n            # Every 400 frames: only send idle forces to attack the target position\n            else:\n                for unit in forces.idle:\n                    unit.attack(target)\n\n        # While we have less than 22 workers: build more\n        # Check if we can afford them (by minerals and by supply)\n        if (\n            self.can_afford(UnitTypeId.SCV)\n            and self.supply_workers + self.already_pending(UnitTypeId.SCV) < 22\n            and cc.is_idle\n        ):\n            cc.train(UnitTypeId.SCV)\n\n        # Build supply depots if we are low on supply, do not construct more than 2 at a time\n        elif self.supply_left < 3:\n            if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 2:\n                # This picks a near-random worker to build a depot at location\n                # 'from command center towards game center, distance 8'\n                await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 8))\n\n        # If we have supply depots (careful, lowered supply depots have a different UnitTypeId: UnitTypeId.SUPPLYDEPOTLOWERED)\n        if self.structures(UnitTypeId.SUPPLYDEPOT):\n            # If we have no barracks\n            if not self.structures(UnitTypeId.BARRACKS):\n                # If we can afford barracks\n                if self.can_afford(UnitTypeId.BARRACKS):\n                    # Near same command as above with the depot\n                    await self.build(UnitTypeId.BARRACKS, near=cc.position.towards(self.game_info.map_center, 8))\n\n        # If we have a barracks (complete or under construction) and less than 2 gas structures (here: refineries)\n        if self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2:\n            if self.can_afford(UnitTypeId.REFINERY):\n                # All the vespene geysirs nearby, including ones with a refinery on top of it\n                vgs = self.vespene_geyser.closer_than(10, cc)\n                for vg in vgs:\n                    has_refinery = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1)\n                    if has_refinery:\n                        continue\n                    # Select a worker closest to the vespene geysir\n                    worker: Unit | None = self.select_build_worker(vg)\n                    # Worker can be none in cases where all workers are dead\n                    # or 'select_build_worker' function only selects from workers which carry no minerals\n                    if worker is not None:\n                        # Issue the build command to the worker, important: vg has to be a Unit, not a position\n                        worker.build_gas(vg)\n\n        # If we have at least one barracks that is completed, build factory\n        if self.structures(UnitTypeId.BARRACKS).ready:\n            if self.structures(UnitTypeId.FACTORY).amount < 3 and not self.already_pending(UnitTypeId.FACTORY):\n                if self.can_afford(UnitTypeId.FACTORY):\n                    position: Point2 = cc.position.towards_with_random_angle(self.game_info.map_center, 16)\n                    await self.build(UnitTypeId.FACTORY, near=position)\n\n        for factory in self.structures(UnitTypeId.FACTORY).ready.idle:\n            # Reactor allows us to build two at a time\n            if self.can_afford(UnitTypeId.CYCLONE):\n                factory.train(UnitTypeId.CYCLONE)\n\n        # Saturate gas\n        for refinery in self.gas_buildings:\n            if refinery.assigned_harvesters < refinery.ideal_harvesters:\n                workers: Units = self.workers.closer_than(10, refinery)\n                if workers:\n                    workers.random.gather(refinery)\n\n        for scv in self.workers.idle:\n            scv.gather(self.mineral_field.closest_to(cc))\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [\n            # Human(Race.Terran),\n            Bot(Race.Terran, CyclonePush()),\n            Computer(Race.Zerg, Difficulty.Easy),\n        ],\n        realtime=False,\n        sc2_version=\"4.7\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/terran/mass_reaper.py",
    "content": "\"\"\"\nBot that stays on 1base, goes 4 rax mass reaper\nThis bot is one of the first examples that are micro intensive\nBot has a chance to win against elite (=Difficulty.VeryHard) zerg AI\n\nBot made by Burny\n\"\"\"\n\nfrom __future__ import annotations\n\nimport random\nfrom typing import Literal\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass MassReaperBot(BotAI):\n    def __init__(self):\n        # 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\n        self.distance_calculation_method = 3\n\n    async def on_step(self, iteration: int):\n        # Benchmark and print duration time of the on_step method based on \"self.distance_calculation_method\" value\n        # logger.info(self.time_formatted, self.supply_used, self.step_time[1])\n        \"\"\"\n        - build depots when low on remaining supply\n        - townhalls contains commandcenter and orbitalcommand\n        - self.units(TYPE).not_ready.amount selects all units of that type, filters incomplete units, and then counts the amount\n        - self.already_pending(TYPE) counts how many units are queued\n        \"\"\"\n        if (\n            self.supply_left < 5\n            and self.townhalls\n            and self.supply_used >= 14\n            and self.can_afford(UnitTypeId.SUPPLYDEPOT)\n            and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 1\n        ):\n            workers: Units = self.workers.gathering\n            # If workers were found\n            if workers:\n                worker: Unit = workers.furthest_to(workers.center)\n                location: Point2 | None = await self.find_placement(\n                    UnitTypeId.SUPPLYDEPOT, worker.position, placement_step=3\n                )\n                # If a placement location was found\n                if location:\n                    # Order worker to build exactly on that location\n                    worker.build(UnitTypeId.SUPPLYDEPOT, location)\n\n        # Lower all depots when finished\n        for depot in self.structures(UnitTypeId.SUPPLYDEPOT).ready:\n            depot(AbilityId.MORPH_SUPPLYDEPOT_LOWER)\n\n        # Morph commandcenter to orbitalcommand\n        # Check if tech requirement for orbital is complete (e.g. you need a barracks to be able to morph an orbital)\n        orbital_tech_requirement: float = self.tech_requirement_progress(UnitTypeId.ORBITALCOMMAND)\n        if orbital_tech_requirement == 1:\n            # Loop over all idle command centers (CCs that are not building SCVs or morphing to orbital)\n            for cc in self.townhalls(UnitTypeId.COMMANDCENTER).idle:\n                # 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\n                if self.can_afford(UnitTypeId.ORBITALCOMMAND):\n                    cc(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND)\n\n        # Expand if we can afford (400 minerals) and have less than 2 bases\n        if (\n            1 <= self.townhalls.amount < 2\n            and self.already_pending(UnitTypeId.COMMANDCENTER) == 0\n            and self.can_afford(UnitTypeId.COMMANDCENTER)\n        ):\n            # get_next_expansion returns the position of the next possible expansion location where you can place a command center\n            location: Point2 | None = await self.get_next_expansion()\n            if location:\n                # Now we \"select\" (or choose) the nearest worker to that found location\n                worker2: Unit | None = self.select_build_worker(location)\n                if worker2 and self.can_afford(UnitTypeId.COMMANDCENTER):\n                    # The worker will be commanded to build the command center\n                    worker2.build(UnitTypeId.COMMANDCENTER, location)\n\n        # Build up to 4 barracks if we can afford them\n        # Check if we have a supply depot (tech requirement) before trying to make barracks\n        barracks_tech_requirement: float = self.tech_requirement_progress(UnitTypeId.BARRACKS)\n        if (\n            barracks_tech_requirement == 1\n            # self.structures.of_type(\n            #     [UnitTypeId.SUPPLYDEPOT, UnitTypeId.SUPPLYDEPOTLOWERED, UnitTypeId.SUPPLYDEPOTDROP]\n            # ).ready\n            and self.structures(UnitTypeId.BARRACKS).ready.amount + self.already_pending(UnitTypeId.BARRACKS) < 4\n            and self.can_afford(UnitTypeId.BARRACKS)\n        ):\n            workers: Units = self.workers.gathering\n            if (\n                workers and self.townhalls\n            ):  # need to check if townhalls.amount > 0 because placement is based on townhall location\n                worker: Unit = workers.furthest_to(workers.center)\n                # I chose placement_step 4 here so there will be gaps between barracks hopefully\n                location: Point2 | None = await self.find_placement(\n                    UnitTypeId.BARRACKS, self.townhalls.random.position, placement_step=4\n                )\n                if location:\n                    worker.build(UnitTypeId.BARRACKS, location)\n\n        # Build refineries (on nearby vespene) when at least one barracks is in construction\n        if (\n            self.structures(UnitTypeId.BARRACKS).ready.amount + self.already_pending(UnitTypeId.BARRACKS) > 0\n            and self.already_pending(UnitTypeId.REFINERY) < 1\n        ):\n            # Loop over all townhalls that are 100% complete\n            for th in self.townhalls.ready:\n                # Find all vespene geysers that are closer than range 10 to this townhall\n                vgs: Units = self.vespene_geyser.closer_than(10, th)\n                for vg in vgs:\n                    if await self.can_place_single(UnitTypeId.REFINERY, vg.position) and self.can_afford(\n                        UnitTypeId.REFINERY\n                    ):\n                        workers: Units = self.workers.gathering\n                        if workers:  # same condition as above\n                            worker: Unit = workers.closest_to(vg)\n                            # Caution: the target for the refinery has to be the vespene geyser, not its position!\n                            worker.build_gas(vg)\n\n        # 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)\n        # Stop scv production when barracks is complete but we still have a command center (priotize morphing to orbital command)\n        if (\n            self.can_afford(UnitTypeId.SCV)\n            and self.supply_left > 0\n            and self.supply_workers < 22\n            and (\n                self.structures(UnitTypeId.BARRACKS).ready.amount < 1\n                and self.townhalls(UnitTypeId.COMMANDCENTER).idle\n                or self.townhalls(UnitTypeId.ORBITALCOMMAND).idle\n            )\n        ):\n            for th in self.townhalls.idle:\n                th.train(UnitTypeId.SCV)\n\n        # Make reapers if we can afford them and we have supply remaining\n        if self.supply_left > 0:\n            # Loop through all idle barracks\n            for rax in self.structures(UnitTypeId.BARRACKS).idle:\n                if self.can_afford(UnitTypeId.REAPER):\n                    rax.train(UnitTypeId.REAPER)\n\n        # Send workers to mine from gas\n        if iteration % 25 == 0:\n            await self.my_distribute_workers()\n\n        # Reaper micro\n        enemies: Units = self.enemy_units | self.enemy_structures\n        enemies_can_attack: Units = enemies.filter(lambda unit: unit.can_attack_ground)\n        for reaper_unit in self.units(UnitTypeId.REAPER):\n            # Move to range 15 of closest unit if reaper is below 20 hp and not regenerating\n            enemy_threats_close: Units = enemies_can_attack.filter(\n                lambda unit: unit.distance_to(reaper_unit) < 15\n            )  # Threats that can attack the reaper\n\n            if reaper_unit.health_percentage < 2 / 5 and enemy_threats_close:\n                retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8(\n                    reaper_unit.position, distance=4\n                )\n                # Filter points that are pathable\n                retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)}\n                if retreat_points:\n                    closest_enemy: Unit = enemy_threats_close.closest_to(reaper_unit)\n                    retreat_point: Point2 = closest_enemy.position.furthest(retreat_points)\n                    reaper_unit.move(retreat_point)\n                    continue  # Continue for loop, dont execute any of the following\n\n            # Reaper is ready to attack, shoot nearest ground unit\n            enemy_ground_units: Units = enemies.filter(\n                lambda unit: unit.distance_to(reaper_unit) < 5 and not unit.is_flying\n            )  # Hardcoded attackrange of 5\n            if reaper_unit.weapon_cooldown == 0 and enemy_ground_units:\n                enemy_ground_units: Units = enemy_ground_units.sorted(lambda x: x.distance_to(reaper_unit))\n                closest_enemy: Unit = enemy_ground_units[0]\n                reaper_unit.attack(closest_enemy)\n                continue  # Continue for loop, dont execute any of the following\n\n            # Attack is on cooldown, check if grenade is on cooldown, if not then throw it to furthest enemy in range 5\n            reaper_grenade_range: float = self.game_data.abilities[\n                AbilityId.KD8CHARGE_KD8CHARGE.value\n            ]._proto.cast_range\n            enemy_ground_units_in_grenade_range: Units = enemies_can_attack.filter(\n                lambda unit: not unit.is_structure\n                and not unit.is_flying\n                and unit.type_id not in {UnitTypeId.LARVA, UnitTypeId.EGG}\n                and unit.distance_to(reaper_unit) < reaper_grenade_range\n            )\n            if enemy_ground_units_in_grenade_range and (reaper_unit.is_attacking or reaper_unit.is_moving):\n                # If AbilityId.KD8CHARGE_KD8CHARGE in abilities, we check that to see if the reaper grenade is off cooldown\n                abilities: list[AbilityId] = await self.get_available_abilities(reaper_unit)  # pyrefly: ignore\n                enemy_ground_units_in_grenade_range = enemy_ground_units_in_grenade_range.sorted(\n                    lambda x: x.distance_to(reaper_unit), reverse=True\n                )\n                furthest_enemy: Unit | None = None\n                for enemy in enemy_ground_units_in_grenade_range:\n                    if await self.can_cast(\n                        reaper_unit, AbilityId.KD8CHARGE_KD8CHARGE, enemy, cached_abilities_of_unit=abilities\n                    ):\n                        furthest_enemy = enemy\n                        break\n                if furthest_enemy is not None:\n                    reaper_unit(AbilityId.KD8CHARGE_KD8CHARGE, furthest_enemy)\n                    continue  # Continue for loop, don't execute any of the following\n\n            # Move to max unit range if enemy is closer than 4\n            enemy_threats_very_close: Units = enemies.filter(\n                lambda unit: unit.can_attack_ground and unit.distance_to(reaper_unit) < 4.5\n            )  # Hardcoded attackrange minus 0.5\n            # Threats that can attack the reaper\n            if reaper_unit.weapon_cooldown != 0 and enemy_threats_very_close:\n                retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8(\n                    reaper_unit.position, distance=4\n                )\n                # Filter points that are pathable by a reaper\n                retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)}\n                if retreat_points:\n                    closest_enemy: Unit = enemy_threats_very_close.closest_to(reaper_unit)\n                    retreat_point: Point2 = max(\n                        retreat_points, key=lambda x: x.distance_to(closest_enemy) - x.distance_to(reaper_unit)\n                    )\n                    reaper_unit.move(retreat_point)\n                    continue  # Continue for loop, don't execute any of the following\n\n            # Move to nearest enemy ground unit/building because no enemy unit is closer than 5\n            all_enemy_ground_units: Units = self.enemy_units.not_flying\n            if all_enemy_ground_units:\n                closest_enemy: Unit = all_enemy_ground_units.closest_to(reaper_unit)\n                reaper_unit.move(closest_enemy)\n                continue  # Continue for loop, don't execute any of the following\n\n            # Move to random enemy start location if no enemy buildings have been seen\n            reaper_unit.move(random.choice(self.enemy_start_locations))\n\n        # Manage idle scvs, would be taken care by distribute workers aswell\n        if self.townhalls:\n            for w in self.workers.idle:\n                th: Unit = self.townhalls.closest_to(w)\n                mfs: Units = self.mineral_field.closer_than(10, th)\n                if mfs:\n                    mf: Unit = mfs.closest_to(w)\n                    w.gather(mf)\n\n        # Manage orbital energy and drop mules\n        for oc in self.townhalls(UnitTypeId.ORBITALCOMMAND).filter(lambda x: x.energy >= 50):\n            mfs: Units = self.mineral_field.closer_than(10, oc)\n            if mfs:\n                mf: Unit = max(mfs, key=lambda x: x.mineral_contents)\n                oc(AbilityId.CALLDOWNMULE_CALLDOWNMULE, mf)\n\n        # When running out of mineral fields near command center, fly to next base with minerals\n\n    # Helper functions\n\n    # Stolen and modified from position.py\n\n    @staticmethod\n    def neighbors4(position: Point2, distance: float = 1) -> set[Point2]:\n        p = position\n        d = distance\n        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))}\n\n    # Stolen and modified from position.py\n    def neighbors8(self, position: Point2, distance: float = 1) -> set[Point2]:\n        p = position\n        d = distance\n        return self.neighbors4(position, distance) | {\n            Point2((p.x - d, p.y - d)),\n            Point2((p.x - d, p.y + d)),\n            Point2((p.x + d, p.y - d)),\n            Point2((p.x + d, p.y + d)),\n        }\n\n    # Distribute workers function rewritten, the default distribute_workers() function did not saturate gas quickly enough\n    async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas=False):\n        mineral_tags = [x.tag for x in self.mineral_field]\n        gas_building_tags = [x.tag for x in self.gas_buildings]\n\n        worker_pool = Units([], self)\n        worker_pool_tags = set()\n\n        # Find all gas_buildings that have surplus or deficit\n        deficit_gas_buildings = {}\n        surplus_gas_buildings = {}\n        for g in self.gas_buildings.filter(lambda x: x.vespene_contents > 0):\n            # Only loop over gas_buildings that have still gas in them\n            deficit = g.ideal_harvesters - g.assigned_harvesters\n            if deficit > 0:\n                deficit_gas_buildings[g.tag] = {\"unit\": g, \"deficit\": deficit}\n            elif deficit < 0:\n                surplus_workers = self.workers.closer_than(10, g).filter(\n                    lambda w: w not in worker_pool_tags\n                    and len(w.orders) == 1\n                    and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER]\n                    and w.orders[0].target in gas_building_tags\n                )\n                for _ in range(-deficit):\n                    if surplus_workers.amount > 0:\n                        w = surplus_workers.pop()\n                        worker_pool.append(w)\n                        worker_pool_tags.add(w.tag)\n                surplus_gas_buildings[g.tag] = {\"unit\": g, \"deficit\": deficit}\n\n        # Find all townhalls that have surplus or deficit\n        deficit_townhalls: dict[int, dict[Literal[\"unit\", \"deficit\"], Unit | int]] = {}\n        surplus_townhalls: dict[int, dict[Literal[\"unit\", \"deficit\"], Unit | int]] = {}\n        if not only_saturate_gas:\n            for th in self.townhalls:\n                deficit = th.ideal_harvesters - th.assigned_harvesters\n                if deficit > 0:\n                    deficit_townhalls[th.tag] = {\"unit\": th, \"deficit\": deficit}\n                elif deficit < 0:\n                    surplus_workers = self.workers.closer_than(10, th).filter(\n                        lambda w: w.tag not in worker_pool_tags\n                        and len(w.orders) == 1\n                        and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER]\n                        and w.orders[0].target in mineral_tags\n                    )\n                    # worker_pool.extend(surplus_workers)\n                    for _ in range(-deficit):\n                        if surplus_workers.amount > 0:\n                            w = surplus_workers.pop()\n                            worker_pool.append(w)\n                            worker_pool_tags.add(w.tag)\n                    surplus_townhalls[th.tag] = {\"unit\": th, \"deficit\": deficit}\n\n            if all(\n                [\n                    len(deficit_gas_buildings) == 0,\n                    len(surplus_gas_buildings) == 0,\n                    len(surplus_townhalls) == 0 or deficit_townhalls == 0,\n                ]\n            ):\n                # Cancel early if there is nothing to balance\n                return\n\n        # Check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases\n        # pyrefly: ignore\n        deficit_gas_count = sum(\n            # pyrefly: ignore\n            gas_info[\"deficit\"]\n            for _gas_tag, gas_info in deficit_gas_buildings.items()\n            # pyrefly: ignore\n            if gas_info[\"deficit\"] > 0\n        )\n        surplus_count = sum(\n            # pyrefly: ignore\n            -gas_info[\"deficit\"]\n            for _gas_tag, gas_info in surplus_gas_buildings.items()\n            # pyrefly: ignore\n            if gas_info[\"deficit\"] < 0\n        )\n        surplus_count += sum(\n            # pyrefly: ignore\n            -townhall_info[\"deficit\"]\n            for _townhall_tag, townhall_info in surplus_townhalls.items()\n            # pyrefly: ignore\n            if townhall_info[\"deficit\"] < 0\n        )\n\n        if deficit_gas_count - surplus_count > 0:\n            # Grab workers near the gas who are mining minerals\n            for _gas_tag, gas_info in deficit_gas_buildings.items():\n                if worker_pool.amount >= deficit_gas_count:\n                    break\n                # pyrefly: ignore\n                workers_near_gas = self.workers.closer_than(10, gas_info[\"unit\"]).filter(\n                    lambda w: w.tag not in worker_pool_tags\n                    and len(w.orders) == 1\n                    and w.orders[0].ability.id in [AbilityId.HARVEST_GATHER]\n                    and w.orders[0].target in mineral_tags\n                )\n                while workers_near_gas.amount > 0 and worker_pool.amount < deficit_gas_count:\n                    w = workers_near_gas.pop()\n                    worker_pool.append(w)\n                    worker_pool_tags.add(w.tag)\n\n        # 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\n        for _gas_tag, gas_info in deficit_gas_buildings.items():\n            if performance_heavy:\n                # Sort furthest away to closest (as the pop() function will take the last element)\n                worker_pool.sort(key=lambda x: x.distance_to(gas_info[\"unit\"]), reverse=True)\n            # pyrefly: ignore\n            for _ in range(gas_info[\"deficit\"]):\n                if worker_pool.amount > 0:\n                    w = worker_pool.pop()\n                    if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]:\n                        # pyrefly: ignore\n                        w.gather(gas_info[\"unit\"], queue=True)\n                    else:\n                        # pyrefly: ignore\n                        w.gather(gas_info[\"unit\"])\n\n        if not only_saturate_gas:\n            # If we now have left over workers, make them mine at bases with deficit in mineral workers\n            for townhall_tag, townhall_info in deficit_townhalls.items():\n                if performance_heavy:\n                    # Sort furthest away to closest (as the pop() function will take the last element)\n                    worker_pool.sort(key=lambda x: x.distance_to(townhall_info[\"unit\"]), reverse=True)\n                # pyrefly: ignore\n                for _ in range(townhall_info[\"deficit\"]):\n                    if worker_pool.amount > 0:\n                        w = worker_pool.pop()\n                        mf = self.mineral_field.closer_than(\n                            10,\n                            # pyrefly: ignore\n                            townhall_info[\"unit\"],\n                        ).closest_to(w)\n                        if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]:\n                            w.gather(mf, queue=True)\n                        else:\n                            w.gather(mf)\n\n\ndef main():\n    # Multiple difficulties for enemy bots available https://github.com/Blizzard/s2client-api/blob/ce2b3c5ac5d0c85ede96cef38ee7ee55714eeb2f/include/sc2api/sc2_gametypes.h#L30\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Terran, MassReaperBot()), Computer(Race.Zerg, Difficulty.VeryHard)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/terran/onebase_battlecruiser.py",
    "content": "from __future__ import annotations\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2, Point3\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass BCRushBot(BotAI):\n    def select_target(self) -> tuple[Point2, bool]:\n        \"\"\"Select an enemy target the units should attack.\"\"\"\n        targets: Units = self.enemy_structures\n        if targets:\n            return targets.random.position, True\n\n        targets: Units = self.enemy_units\n        if targets:\n            return targets.random.position, True\n\n        if self.units and min(u.position.distance_to(self.enemy_start_locations[0]) for u in self.units) < 5:\n            return self.enemy_start_locations[0].position, False\n\n        return self.mineral_field.random.position, False\n\n    async def on_step(self, iteration: int):\n        ccs: Units = self.townhalls\n        # If we no longer have townhalls, attack with all workers\n        if not ccs:\n            target, target_is_enemy_unit = self.select_target()\n            for unit in self.workers | self.units(UnitTypeId.BATTLECRUISER):\n                if not unit.is_attacking:\n                    unit.attack(target)\n            return\n\n        cc: Unit = ccs.random\n\n        # Send all BCs to attack a target.\n        bcs: Units = self.units(UnitTypeId.BATTLECRUISER)\n        if bcs:\n            target, target_is_enemy_unit = self.select_target()\n            bc: Unit\n            for bc in bcs:\n                # Order the BC to attack-move the target\n                if target_is_enemy_unit and (bc.is_idle or bc.is_moving):\n                    bc.attack(target)\n                # Order the BC to move to the target, and once the select_target returns an attack-target, change it to attack-move\n                elif bc.is_idle:\n                    bc.move(target)\n\n        # Build more SCVs until 22\n        if self.can_afford(UnitTypeId.SCV) and self.supply_workers < 22 and cc.is_idle:\n            cc.train(UnitTypeId.SCV)\n\n        # Build more BCs\n        if self.structures(UnitTypeId.FUSIONCORE) and self.can_afford(UnitTypeId.BATTLECRUISER):\n            for starport in self.structures(UnitTypeId.STARPORT).idle:\n                if starport.has_add_on:\n                    if self.can_afford(UnitTypeId.BATTLECRUISER):\n                        starport.train(UnitTypeId.BATTLECRUISER)\n\n        # Build more supply depots\n        if self.supply_left < 6 and self.supply_used >= 14 and not self.already_pending(UnitTypeId.SUPPLYDEPOT):\n            if self.can_afford(UnitTypeId.SUPPLYDEPOT):\n                await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 8))\n\n        # Build barracks if we have none\n        if self.tech_requirement_progress(UnitTypeId.BARRACKS) == 1:\n            if not self.structures(UnitTypeId.BARRACKS):\n                if self.can_afford(UnitTypeId.BARRACKS):\n                    await self.build(UnitTypeId.BARRACKS, near=cc.position.towards(self.game_info.map_center, 8))\n\n            # Build refineries\n            elif self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2:\n                if self.can_afford(UnitTypeId.REFINERY):\n                    vgs: Units = self.vespene_geyser.closer_than(20, cc)\n                    for vg in vgs:\n                        has_gas_building = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1)\n                        if not has_gas_building:\n                            worker: Unit | None = self.select_build_worker(vg.position)\n                            if worker is not None:\n                                worker.build_gas(vg)\n\n            # Build factory if we dont have one\n            if self.tech_requirement_progress(UnitTypeId.FACTORY) == 1:\n                factories: Units = self.structures(UnitTypeId.FACTORY)\n                if not factories:\n                    if self.can_afford(UnitTypeId.FACTORY):\n                        await self.build(UnitTypeId.FACTORY, near=cc.position.towards(self.game_info.map_center, 8))\n                # Build starport once we can build starports, up to 2\n                elif (\n                    factories.ready\n                    and self.structures.of_type({UnitTypeId.STARPORT, UnitTypeId.STARPORTFLYING}).ready.amount\n                    + self.already_pending(UnitTypeId.STARPORT)\n                    < 2\n                ):\n                    if self.can_afford(UnitTypeId.STARPORT):\n                        await self.build(\n                            UnitTypeId.STARPORT,\n                            near=cc.position.towards(self.game_info.map_center, 15).random_on_distance(8),\n                        )\n\n        def starport_points_to_build_addon(sp_position: Point2) -> list[Point2]:\n            \"\"\"Return all points that need to be checked when trying to build an addon. Returns 4 points.\"\"\"\n            addon_offset: Point2 = Point2((2.5, -0.5))\n            addon_position: Point2 = sp_position + addon_offset\n            addon_points = [\n                (addon_position + Point2((x - 0.5, y - 0.5))).rounded for x in range(0, 2) for y in range(0, 2)\n            ]\n            return addon_points\n\n        # Build starport techlab or lift if no room to build techlab\n        starport: Unit\n        for starport in self.structures(UnitTypeId.STARPORT).ready.idle:\n            if not starport.has_add_on and self.can_afford(UnitTypeId.STARPORTTECHLAB):\n                addon_points = starport_points_to_build_addon(starport.position)\n                if all(\n                    self.in_map_bounds(addon_point)\n                    and self.in_placement_grid(addon_point)\n                    and self.in_pathing_grid(addon_point)\n                    for addon_point in addon_points\n                ):\n                    starport.build(UnitTypeId.STARPORTTECHLAB)\n                else:\n                    starport(AbilityId.LIFT)\n\n        def starport_land_positions(sp_position: Point2) -> list[Point2]:\n            \"\"\"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.\"\"\"\n            land_positions = [(sp_position + Point2((x, y))).rounded for x in range(-1, 2) for y in range(-1, 2)]\n            return land_positions + starport_points_to_build_addon(sp_position)\n\n        def try_land_starports():\n            # Find a position to land for a flying starport so that it can build an addon\n            for starport in self.structures(UnitTypeId.STARPORTFLYING).idle:\n                possible_land_positions_offset = sorted(\n                    (Point2((x, y)) for x in range(-10, 10) for y in range(-10, 10)),\n                    key=lambda point: point.x**2 + point.y**2,\n                )\n                offset_point: Point2 = Point2((-0.5, -0.5))\n                possible_land_positions = (\n                    starport.position.rounded + offset_point + p for p in possible_land_positions_offset\n                )\n                for target_land_position in possible_land_positions:\n                    land_and_addon_points: list[Point2] = starport_land_positions(target_land_position)\n                    if all(\n                        self.in_map_bounds(land_pos)\n                        and self.in_placement_grid(land_pos)\n                        and self.in_pathing_grid(land_pos)\n                        for land_pos in land_and_addon_points\n                    ):\n                        starport(AbilityId.LAND, target_land_position)\n                        return\n\n        try_land_starports()\n\n        # Show where it is flying to and show grid\n        for starport in self.structures(UnitTypeId.STARPORTFLYING).filter(lambda unit: not unit.is_idle):\n            if isinstance(starport.order_target, Point2):\n                p: Point3 = Point3((*starport.order_target, self.get_terrain_z_height(starport.order_target)))\n                self.client.debug_box2_out(p, color=Point3((255, 0, 0)))\n\n        # Build fusion core\n        if self.structures(UnitTypeId.STARPORT).ready:\n            if self.can_afford(UnitTypeId.FUSIONCORE) and not self.structures(UnitTypeId.FUSIONCORE):\n                await self.build(UnitTypeId.FUSIONCORE, near=cc.position.towards(self.game_info.map_center, 8))\n\n        # Saturate refineries\n        for refinery in self.gas_buildings:\n            if refinery.assigned_harvesters < refinery.ideal_harvesters:\n                workers: Units = self.workers.closer_than(10, refinery)\n                if workers:\n                    workers.random.gather(refinery)\n\n        # Send workers back to mine if they are idle\n        for scv in self.workers.idle:\n            scv.gather(self.mineral_field.closest_to(cc))\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [\n            # Human(Race.Terran),\n            Bot(Race.Terran, BCRushBot()),\n            Computer(Race.Zerg, Difficulty.Hard),\n        ],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/terran/proxy_rax.py",
    "content": "from sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass ProxyRaxBot(BotAI):\n    async def on_start(self):\n        self.client.game_step = 2\n\n    async def on_step(self, iteration: int):\n        # If we don't have a townhall anymore, send all units to attack\n        ccs: Units = self.townhalls(UnitTypeId.COMMANDCENTER)\n        if not ccs:\n            target: Point2 = self.enemy_structures.random_or(self.enemy_start_locations[0]).position\n            for unit in self.workers | self.units(UnitTypeId.MARINE):\n                unit.attack(target)\n            return\n\n        cc: Unit = ccs.first\n\n        # Send marines in waves of 15, each time 15 are idle, send them to their death\n        marines: Units = self.units(UnitTypeId.MARINE).idle\n        if marines.amount > 15:\n            target: Point2 = self.enemy_structures.random_or(self.enemy_start_locations[0]).position\n            for marine in marines:\n                marine.attack(target)\n\n        # Train more SCVs\n        if self.can_afford(UnitTypeId.SCV) and self.supply_workers < 16 and cc.is_idle:\n            cc.train(UnitTypeId.SCV)\n\n        # Build more depots\n        elif (\n            self.supply_left < (2 if self.structures(UnitTypeId.BARRACKS).amount < 3 else 4) and self.supply_used >= 14\n        ):\n            if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) < 2:\n                await self.build(UnitTypeId.SUPPLYDEPOT, near=cc.position.towards(self.game_info.map_center, 5))\n\n        # Build proxy barracks\n        elif self.structures(UnitTypeId.BARRACKS).amount < 3 or (\n            self.minerals > 400 and self.structures(UnitTypeId.BARRACKS).amount < 5\n        ):\n            if self.can_afford(UnitTypeId.BARRACKS):\n                p: Point2 = self.game_info.map_center.towards(self.enemy_start_locations[0], 25)\n                await self.build(UnitTypeId.BARRACKS, near=p)\n\n        # Train marines\n        for rax in self.structures(UnitTypeId.BARRACKS).ready.idle:\n            if self.can_afford(UnitTypeId.MARINE):\n                rax.train(UnitTypeId.MARINE)\n\n        # Send idle workers to gather minerals near command center\n        for scv in self.workers.idle:\n            scv.gather(self.mineral_field.closest_to(cc))\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Terran, ProxyRaxBot()), Computer(Race.Zerg, Difficulty.Hard)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/terran/ramp_wall.py",
    "content": "import random\n\nimport numpy as np\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2, Point3\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass RampWallBot(BotAI):\n    def __init__(self):\n        self.unit_command_uses_self_do = False\n\n    async def on_step(self, iteration: int):\n        ccs: Units = self.townhalls(UnitTypeId.COMMANDCENTER)\n        if not ccs:\n            return\n\n        cc: Unit = ccs.first\n\n        await self.distribute_workers()\n\n        if self.can_afford(UnitTypeId.SCV) and self.workers.amount < 16 and cc.is_idle:\n            cc.train(UnitTypeId.SCV)\n\n        def raise_and_lower_depots():\n            # Raise depos when enemies are nearby\n            for depo in self.structures(UnitTypeId.SUPPLYDEPOT).ready:\n                for unit in self.enemy_units:\n                    if unit.distance_to(depo) < 15:\n                        return\n                else:\n                    depo(AbilityId.MORPH_SUPPLYDEPOT_LOWER)\n            # Lower depos when no enemies are nearby\n            for depo in self.structures(UnitTypeId.SUPPLYDEPOTLOWERED).ready:\n                for unit in self.enemy_units:\n                    if unit.distance_to(depo) < 10:\n                        depo(AbilityId.MORPH_SUPPLYDEPOT_RAISE)\n                        return\n\n        raise_and_lower_depots()\n\n        # Draw ramp points\n        self.draw_ramp_points()\n\n        # Draw all detected expansions on the map\n        self.draw_expansions()\n\n        # # Draw pathing grid\n        self.draw_pathing_grid()\n\n        # Draw placement  grid\n        self.draw_placement_grid()\n\n        # Draw vision blockers\n        self.draw_vision_blockers()\n\n        # Draw visibility pixelmap for debugging purposes\n        self.draw_visibility_pixelmap()\n\n        # Draw some example boxes around units, lines towards command center, text on the screen and barracks\n        self.draw_example()\n\n        # Draw if two selected units are facing each other - green if this guy is facing the other, red if he is not\n        self.draw_facing_units()\n\n        depot_placement_positions: set[Point2] = self.main_base_ramp.corner_depots\n        # 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\n        # depot_placement_positions = self.main_base_ramp.corner_depots | {self.main_base_ramp.depot_in_middle}\n\n        barracks_placement_position: Point2 | None = self.main_base_ramp.barracks_correct_placement\n        # If you prefer to have the barracks in the middle without room for addons, use the following instead\n        # barracks_placement_position = self.main_base_ramp.barracks_in_middle\n\n        depots: Units = self.structures.of_type({UnitTypeId.SUPPLYDEPOT, UnitTypeId.SUPPLYDEPOTLOWERED})\n\n        # Filter locations close to finished supply depots\n        if depots:\n            depot_placement_positions: set[Point2] = {\n                d for d in depot_placement_positions if depots.closest_distance_to(d) > 1\n            }\n\n        # Build depots\n        if self.can_afford(UnitTypeId.SUPPLYDEPOT) and self.already_pending(UnitTypeId.SUPPLYDEPOT) == 0:\n            if len(depot_placement_positions) == 0:\n                return\n            # Choose any depot location\n            target_depot_location: Point2 = depot_placement_positions.pop()\n            workers: Units = self.workers.gathering\n            if workers:  # if workers were found\n                worker: Unit = workers.random\n                worker.build(UnitTypeId.SUPPLYDEPOT, target_depot_location)\n\n        # Build barracks\n        if depots.ready and self.can_afford(UnitTypeId.BARRACKS) and self.already_pending(UnitTypeId.BARRACKS) == 0:\n            if self.structures(UnitTypeId.BARRACKS).amount + self.already_pending(UnitTypeId.BARRACKS) > 0:\n                return\n            workers = self.workers.gathering\n            if workers and barracks_placement_position:  # if workers were found\n                worker: Unit = workers.random\n                worker.build(UnitTypeId.BARRACKS, barracks_placement_position)\n\n    async def on_building_construction_started(self, unit: Unit):\n        logger.info(f\"Construction of building {unit} started at {unit.position}.\")\n\n    async def on_building_construction_complete(self, unit: Unit):\n        logger.info(f\"Construction of building {unit} completed at {unit.position}.\")\n\n    def draw_ramp_points(self):\n        for ramp in self.game_info.map_ramps:\n            for p in ramp.points:\n                h2 = self.get_terrain_z_height(p)\n                pos = Point3((p.x, p.y, h2))\n                color = Point3((255, 0, 0))\n                if p in ramp.upper:\n                    color = Point3((0, 255, 0))\n                if p in ramp.upper2_for_ramp_wall:\n                    color = Point3((0, 255, 255))\n                if p in ramp.lower:\n                    color = Point3((0, 0, 255))\n                self.client.debug_box2_out(pos + Point2((0.5, 0.5)), half_vertex_length=0.25, color=color)\n                # Identical to above:\n                # p0 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z + 0.25))\n                # p1 = Point3((pos.x + 0.75, pos.y + 0.75, pos.z - 0.25))\n                # logger.info(f\"Drawing {p0} to {p1}\")\n                # self.client.debug_box_out(p0, p1, color=color)\n\n    def draw_expansions(self):\n        green = Point3((0, 255, 0))\n        for expansion_pos in self.expansion_locations_list:\n            height = self.get_terrain_z_height(expansion_pos)\n            expansion_pos3 = Point3((*expansion_pos, height))\n            self.client.debug_box2_out(expansion_pos3, half_vertex_length=2.5, color=green)\n\n    def draw_pathing_grid(self):\n        map_area = self.game_info.playable_area\n        for (b, a), value in np.ndenumerate(self.game_info.pathing_grid.data_numpy):\n            if value == 0:\n                continue\n            # Skip values outside of playable map area\n            if not map_area.x <= a < map_area.x + map_area.width:\n                continue\n            if not map_area.y <= b < map_area.y + map_area.height:\n                continue\n            p = Point2((a, b))\n            h2 = self.get_terrain_z_height(p)\n            pos = Point3((p.x, p.y, h2))\n            p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5))\n            p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5))\n            # logger.info(f\"Drawing {p0} to {p1}\")\n            color = Point3((0, 255, 0))\n            self.client.debug_box_out(p0, p1, color=color)\n\n    def draw_placement_grid(self):\n        map_area = self.game_info.playable_area\n        for (b, a), value in np.ndenumerate(self.game_info.placement_grid.data_numpy):\n            if value == 0:\n                continue\n            # Skip values outside of playable map area\n            if not map_area.x <= a < map_area.x + map_area.width:\n                continue\n            if not map_area.y <= b < map_area.y + map_area.height:\n                continue\n            p = Point2((a, b))\n            h2 = self.get_terrain_z_height(p)\n            pos = Point3((p.x, p.y, h2))\n            p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5))\n            p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5))\n            # logger.info(f\"Drawing {p0} to {p1}\")\n            color = Point3((0, 255, 0))\n            self.client.debug_box_out(p0, p1, color=color)\n\n    def draw_vision_blockers(self):\n        for p in self.game_info.vision_blockers:\n            h2 = self.get_terrain_z_height(p)\n            pos = Point3((p.x, p.y, h2))\n            p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5))\n            p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5))\n            # logger.info(f\"Drawing {p0} to {p1}\")\n            color = Point3((255, 0, 0))\n            self.client.debug_box_out(p0, p1, color=color)\n\n    def draw_visibility_pixelmap(self):\n        for (y, x), value in np.ndenumerate(self.state.visibility.data_numpy):\n            p = Point2((x, y))\n            h2 = self.get_terrain_z_height(p)\n            pos = Point3((p.x, p.y, h2))\n            p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25)) + Point2((0.5, 0.5))\n            p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25)) + Point2((0.5, 0.5))\n            # Red\n            color = Point3((255, 0, 0))\n            # If value == 2: show green (= we have vision on that point)\n            if value == 2:\n                color = Point3((0, 255, 0))\n            self.client.debug_box_out(p0, p1, color=color)\n\n    def draw_example(self):\n        # Draw green boxes around SCVs if they are gathering, yellow if they are returning cargo, red the rest\n        scv: Unit\n        for scv in self.workers:\n            pos = scv.position3d\n            p0 = Point3((pos.x - 0.25, pos.y - 0.25, pos.z + 0.25))\n            p1 = Point3((pos.x + 0.25, pos.y + 0.25, pos.z - 0.25))\n            # Red\n            color = Point3((255, 0, 0))\n            if scv.is_gathering:\n                color = Point3((0, 255, 0))\n            elif scv.is_returning:\n                color = Point3((255, 255, 0))\n            self.client.debug_box_out(p0, p1, color=color)\n\n        # Draw lines from structures to command center\n        if self.townhalls:\n            cc = self.townhalls[0]\n            p0 = cc.position3d\n            if not self.structures:\n                return\n            structure: Unit\n            for structure in self.structures:\n                if structure == cc:\n                    continue\n                p1 = structure.position3d\n                # Red\n                color = Point3((255, 0, 0))\n                self.client.debug_line_out(p0, p1, color=color)\n\n            # Draw text on barracks\n            if structure.type_id == UnitTypeId.BARRACKS:\n                # Blue\n                color = Point3((0, 0, 255))\n                pos = structure.position3d + Point3((0, 0, 0.5))\n                # TODO: Why is this text flickering\n                self.client.debug_text_world(text=\"MY RAX\", pos=pos, color=color, size=16)\n\n        # Draw text in top left of screen\n        self.client.debug_text_screen(text=\"Hello world!\", pos=Point2((0, 0)), color=None, size=16)\n        self.client.debug_text_simple(text=\"Hello world2!\")\n\n    def draw_facing_units(self):\n        \"\"\"Draws green box on top of selected_unit2, if selected_unit2 is facing selected_unit1\"\"\"\n        selected_unit1: Unit\n        selected_unit2: Unit\n        red = Point3((255, 0, 0))\n        green = Point3((0, 255, 0))\n        for selected_unit1 in (self.units | self.structures).selected:\n            for selected_unit2 in self.units.selected:\n                if selected_unit1 == selected_unit2:\n                    continue\n                if selected_unit2.is_facing(selected_unit1):\n                    self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=green)\n                else:\n                    self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=red)\n\n\ndef main():\n    _map = random.choice(\n        [\n            # Most maps have 2 upper points at the ramp (len(self.main_base_ramp.upper) == 2)\n            \"AutomatonLE\",\n            \"BlueshiftLE\",\n            \"CeruleanFallLE\",\n            \"KairosJunctionLE\",\n            \"ParaSiteLE\",\n            \"PortAleksanderLE\",\n            \"StasisLE\",\n            \"DarknessSanctuaryLE\",\n            \"ParaSiteLE\",  # Has 5 upper points at the main ramp\n            \"AcolyteLE\",  # Has 4 upper points at the ramp to the in-base natural and 2 upper points at the small ramp\n            \"HonorgroundsLE\",  # Has 4 or 9 upper points at the large main base ramp\n        ]\n    )\n    _map = \"PillarsofGoldLE\"\n    run_game(\n        maps.get(_map),\n        [Bot(Race.Terran, RampWallBot()), Computer(Race.Zerg, Difficulty.Hard)],\n        realtime=True,\n        # sc2_version=\"4.10.1\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/too_slow_bot.py",
    "content": "import asyncio\nimport random\n\nfrom examples.terran.proxy_rax import ProxyRaxBot\nfrom sc2 import maps\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass SlowBot(ProxyRaxBot):\n    async def on_step(self, iteration: int):\n        await asyncio.sleep(random.random())\n        await super().on_step(iteration)\n\n\ndef main():\n    run_game(\n        maps.get(\"Abyssal Reef LE\"),\n        [Bot(Race.Terran, SlowBot()), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=False,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/watch_replay.py",
    "content": "import platform\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom sc2.main import run_replay\nfrom sc2.observer_ai import ObserverAI\n\n\nclass ObserverBot(ObserverAI):\n    \"\"\"\n    A replay bot that can run replays.\n    Check sc2/observer_ai.py for more available functions\n    \"\"\"\n\n    async def on_start(self):\n        print(\"Replay on_start() was called\")\n\n    async def on_step(self, iteration: int):\n        print(f\"Replay iteration: {iteration}\")\n\n\nif __name__ == \"__main__\":\n    my_observer_ai = ObserverBot()  # pyrefly: ignore\n    # Enter replay name here\n    # The replay should be either in this folder and you can give it a relative path, or change it to the absolute path\n    replay_name = \"WorkerRush.SC2Replay\"\n    if platform.system() == \"Linux\":\n        home_replay_folder = Path.home() / \"Documents\" / \"StarCraft II\" / \"Replays\"\n        replay_path = home_replay_folder / replay_name\n        if not replay_path.is_file():\n            logger.warning(f\"You are on linux, please put the replay in directory {home_replay_folder}\")\n            raise FileNotFoundError\n    elif Path(replay_name).is_absolute():\n        replay_path = Path(replay_name)\n    else:\n        # Convert relative path to absolute path, assuming this replay is in this folder\n        folder_path = Path(__file__).parent\n        replay_path = folder_path / replay_name\n    assert replay_path.is_file(), (\n        \"Run worker_rush.py in the same folder first to generate a replay. Then run watch_replay.py again.\"\n    )\n    run_replay(my_observer_ai, replay_path=str(replay_path))\n"
  },
  {
    "path": "examples/worker_rush.py",
    "content": "from sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass WorkerRushBot(BotAI):\n    async def on_step(self, iteration: int):\n        if iteration == 0:\n            for worker in self.workers:\n                worker.attack(self.enemy_start_locations[0])\n\n\ndef main():\n    run_game(\n        maps.get(\"Abyssal Reef LE\"),\n        [Bot(Race.Zerg, WorkerRushBot()), Computer(Race.Protoss, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"WorkerRush.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/worker_stack_bot.py",
    "content": "\"\"\"\nThis bot attempts to stack workers 'perfectly'.\nThis is only a demo that works on game start, but does not work when adding more workers or bases.\n\nThis bot exists only to showcase how to keep track of mineral tag over multiple steps / frames.\n\nTask for the user who wants to enhance this bot:\n- Allow mining from vespene geysirs\n- Remove dead workers and re-assign (new) workers to that mineral patch, or pick a worker from a long distance mineral patch\n- Re-assign workers when new base is completed (or near complete)\n- Re-assign workers when base died\n- Re-assign workers when mineral patch mines out\n- Re-assign workers when gas mines out\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass WorkerStackBot(BotAI):\n    def __init__(self):\n        self.worker_to_mineral_patch_dict: dict[int, int] = {}\n        self.mineral_patch_to_list_of_workers: dict[int, set[int]] = {}\n        self.minerals_sorted_by_distance: Units = Units([], self)\n        # Distance 0.01 to 0.1 seems fine\n        self.townhall_distance_threshold = 0.01\n        # Distance factor between 0.95 and 1.0 seems fine\n        self.townhall_distance_factor = 1\n\n    async def on_start(self):\n        self.client.game_step = 1\n        await self.assign_workers()\n\n    async def assign_workers(self):\n        self.minerals_sorted_by_distance = self.mineral_field.closer_than(\n            10, self.start_location\n        ).sorted_by_distance_to(self.start_location)\n\n        # Assign workers to mineral patch, start with the mineral patch closest to base\n        for mineral in self.minerals_sorted_by_distance:\n            # Assign workers closest to the mineral patch\n            workers = self.workers.tags_not_in(self.worker_to_mineral_patch_dict).sorted_by_distance_to(mineral)\n            for worker in workers:\n                # Assign at most 2 workers per patch\n                # 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\n                if len(self.mineral_patch_to_list_of_workers.get(mineral.tag, [])) < 2:\n                    if len(self.mineral_patch_to_list_of_workers.get(mineral.tag, [])) == 0:\n                        self.mineral_patch_to_list_of_workers[mineral.tag] = {worker.tag}\n                    else:\n                        self.mineral_patch_to_list_of_workers[mineral.tag].add(worker.tag)\n                    # Keep track of which mineral patch the worker is assigned to - if the mineral patch mines out, reassign the worker to another patch\n                    self.worker_to_mineral_patch_dict[worker.tag] = mineral.tag\n                else:\n                    break\n\n    async def on_step(self, iteration: int):\n        if self.worker_to_mineral_patch_dict:\n            # Quick-access cache mineral tag to mineral Unit\n            minerals: dict[int, Unit] = {mineral.tag: mineral for mineral in self.mineral_field}\n\n            worker: Unit\n            for worker in self.workers:\n                if not self.townhalls:\n                    logger.error(\"All townhalls died - can't return resources\")\n                    break\n                mineral_tag = self.worker_to_mineral_patch_dict[worker.tag]\n                mineral = minerals.get(mineral_tag)\n                if mineral is None:\n                    logger.error(f\"Mined out mineral with tag {mineral_tag} for worker {worker.tag}\")\n                    continue\n\n                # Order worker to mine at target mineral patch if isn't carrying minerals\n                if not worker.is_carrying_minerals:\n                    if not worker.is_gathering or worker.order_target != mineral.tag:\n                        worker.gather(mineral)\n                # Order worker to return minerals if carrying minerals\n                else:\n                    th = self.townhalls.closest_to(worker)\n                    # Move worker in front of the nexus to avoid deceleration until the last moment\n                    if worker.distance_to(th) > th.radius + worker.radius + self.townhall_distance_threshold:\n                        pos: Point2 = th.position\n\n                        worker.move(pos.towards(worker, th.radius * self.townhall_distance_factor))\n                        worker.return_resource(queue=True)\n                    else:\n                        worker.return_resource()\n                        worker.gather(mineral, queue=True)\n\n        # Print info every 30 game-seconds\n\n        if self.state.game_loop % (22.4 * 30) == 0:\n            logger.info(f\"{self.time_formatted} Mined a total of {int(self.state.score.collected_minerals)} minerals\")\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Protoss, WorkerStackBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        random_seed=0,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/__init__.py",
    "content": "from pathlib import Path\n\n__all__ = [p.stem for p in Path().iterdir() if p.is_file() and p.suffix == \".py\" and p.stem != \"__init__\"]\n"
  },
  {
    "path": "examples/zerg/banes_banes_banes.py",
    "content": "import random\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass BanesBanesBanes(BotAI):\n    \"\"\"\n    A dumb bot that a-moves banes.\n    Use to check if bane morphs are working correctly\n    \"\"\"\n\n    def select_target(self) -> Point2:\n        if self.enemy_structures:\n            return random.choice(self.enemy_structures).position\n        return self.enemy_start_locations[0]\n\n    async def on_step(self, iteration: int):\n        larvae: Units = self.larva\n        lings: Units = self.units(UnitTypeId.ZERGLING)\n        # Send all idle banes to enemy\n        if banes := [u for u in self.units if u.type_id == UnitTypeId.BANELING and u.is_idle]:\n            for unit in banes:\n                unit.attack(self.select_target())\n\n        # If supply is low, train overlords\n        if (\n            self.supply_left < 2\n            and larvae\n            and self.can_afford(UnitTypeId.OVERLORD)\n            and not self.already_pending(UnitTypeId.OVERLORD)\n        ):\n            larvae.random.train(UnitTypeId.OVERLORD)\n            return\n\n        # If bane nest is ready, train banes\n        if lings and self.can_afford(UnitTypeId.BANELING) and self.structures(UnitTypeId.BANELINGNEST).ready:\n            # TODO: Get lings.random.train(UnitTypeId.BANELING) to work\n            #   Broken on recent patches\n            # lings.random.train(UnitTypeId.BANELING)\n\n            # This way is working\n            lings.random(AbilityId.MORPHTOBANELING_BANELING)\n            return\n\n        # If all our townhalls are dead, send all our units to attack\n        if not self.townhalls:\n            for unit in self.units.of_type({UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING}):\n                unit.attack(self.enemy_start_locations[0])\n            return\n\n        hq: Unit = self.townhalls.first\n\n        # Send idle queens with >=25 energy to inject\n        for queen in self.units(UnitTypeId.QUEEN).idle:\n            # 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\n            # abilities = await self.get_available_abilities(queen)\n            # if AbilityId.EFFECT_INJECTLARVA in abilities:\n            if queen.energy >= 25:\n                queen(AbilityId.EFFECT_INJECTLARVA, hq)\n\n        # Build spawning pool\n        if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0:\n            if self.can_afford(UnitTypeId.SPAWNINGPOOL):\n                await self.build(\n                    UnitTypeId.SPAWNINGPOOL,\n                    near=hq.position.towards(self.game_info.map_center, 5),\n                )\n\n        # Upgrade to lair if spawning pool is complete\n        # if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n        #     if hq.is_idle and not self.townhalls(UnitTypeId.LAIR):\n        #         if self.can_afford(UnitTypeId.LAIR):\n        #             hq.build(UnitTypeId.LAIR)\n\n        # If lair is ready and we have no hydra den on the way: build hydra den\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready and self.can_afford(UnitTypeId.BANELINGNEST):\n            if self.structures(UnitTypeId.BANELINGNEST).amount + self.already_pending(UnitTypeId.BANELINGNEST) == 0:\n                await self.build(\n                    UnitTypeId.BANELINGNEST,\n                    near=hq.position.towards(self.game_info.map_center, 5),\n                )\n\n        # If we dont have both extractors: build them\n        if (\n            self.structures(UnitTypeId.SPAWNINGPOOL)\n            and self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2\n            and self.can_afford(UnitTypeId.EXTRACTOR)\n        ):\n            # May crash if we dont have any drones\n            for vg in self.vespene_geyser.closer_than(10, hq):\n                drone: Unit = self.workers.random\n                drone.build_gas(vg)\n                return\n\n        # If we have less than 22 drones, build drones\n        if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22:\n            if larvae and self.can_afford(UnitTypeId.DRONE):\n                larva: Unit = larvae.random\n                larva.train(UnitTypeId.DRONE)\n                return\n\n        # Saturate gas\n        for a in self.gas_buildings:\n            if a.assigned_harvesters < a.ideal_harvesters:\n                w: Units = self.workers.closer_than(10, a)\n                if w:\n                    w.random.gather(a)\n\n        # Build queen once the pool is done\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n            if not self.units(UnitTypeId.QUEEN) and hq.is_idle:\n                if self.can_afford(UnitTypeId.QUEEN):\n                    hq.train(UnitTypeId.QUEEN)\n\n        # Train zerglings\n        if larvae and self.can_afford(UnitTypeId.ZERGLING):\n            larvae.random.train(UnitTypeId.ZERGLING)\n\n\ndef main():\n    run_game(\n        maps.get(\"GoldenAura513AIE\"),\n        [Bot(Race.Zerg, BanesBanesBanes()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"ZvT.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/expand_everywhere.py",
    "content": "import random\nfrom contextlib import suppress\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\n\n\nclass ExpandEverywhere(BotAI):\n    async def on_start(self):\n        self.client.game_step = 50\n        await self.client.debug_show_map()\n\n    async def on_step(self, iteration: int):\n        # Build overlords if about to be supply blocked\n        if (\n            self.supply_left < 2\n            and self.supply_cap < 200\n            and self.already_pending(UnitTypeId.OVERLORD) < 2\n            and self.can_afford(UnitTypeId.OVERLORD)\n        ):\n            self.train(UnitTypeId.OVERLORD)\n\n        # While we have less than 16 drones, make more drones\n        if (\n            self.can_afford(UnitTypeId.DRONE)\n            and self.supply_workers - self.worker_en_route_to_build(UnitTypeId.HATCHERY)\n            < (self.townhalls.amount + self.placeholders(UnitTypeId.HATCHERY).amount) * 16\n        ):\n            self.train(UnitTypeId.DRONE)\n\n        # Send workers across bases\n        await self.distribute_workers()\n\n        # Expand if we have 300 minerals, try to expand if there is one more expansion location available\n        with suppress(AssertionError):\n            if self.can_afford(UnitTypeId.HATCHERY):\n                planned_hatch_locations: set[Point2] = {placeholder.position for placeholder in self.placeholders}\n                my_structure_locations: set[Point2] = {structure.position for structure in self.structures}\n                enemy_structure_locations: set[Point2] = {structure.position for structure in self.enemy_structures}\n                blocked_locations: set[Point2] = (\n                    my_structure_locations | planned_hatch_locations | enemy_structure_locations\n                )\n                shuffled_expansions = self.expansion_locations_list.copy()\n                random.shuffle(shuffled_expansions)\n                for exp_pos in shuffled_expansions:\n                    if exp_pos in blocked_locations:\n                        continue\n                    for drone in self.workers.collecting:\n                        drone: Unit\n                        drone.build(UnitTypeId.HATCHERY, exp_pos)\n                        assert False, \"Break out of 2 for loops\"\n\n        # Kill all enemy units in vision / sight\n        if self.enemy_units:\n            await self.client.debug_kill_unit(self.enemy_units)\n\n    async def on_building_construction_complete(self, unit: Unit):\n        \"\"\"Set rally point of new hatcheries.\"\"\"\n        if unit.type_id == UnitTypeId.HATCHERY and self.mineral_field:\n            mf = self.mineral_field.closest_to(unit)\n            unit.smart(mf)\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Zerg, ExpandEverywhere()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"ZvT.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/hydralisk_push.py",
    "content": "import random\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass Hydralisk(BotAI):\n    def select_target(self) -> Point2:\n        if self.enemy_structures:\n            return random.choice(self.enemy_structures).position\n        return self.enemy_start_locations[0]\n\n    async def on_step(self, iteration: int):\n        larvae: Units = self.larva\n        forces: Units = self.units.of_type({UnitTypeId.ZERGLING, UnitTypeId.HYDRALISK})\n\n        # Send all idle lings + hydras to attack-move if we have at least 10 hydras, every 400th frame\n        if self.units(UnitTypeId.HYDRALISK).amount >= 10 and iteration % 50 == 0:\n            for unit in forces.idle:\n                unit.attack(self.select_target())\n\n        # If supply is low, train overlords\n        if self.supply_left < 2 and larvae and self.can_afford(UnitTypeId.OVERLORD):\n            larvae.random.train(UnitTypeId.OVERLORD)\n            return\n\n        # If hydra den is ready and idle, research upgrades\n        hydra_dens = self.structures(UnitTypeId.HYDRALISKDEN)\n        if hydra_dens:\n            for hydra_den in hydra_dens.ready.idle:\n                if self.already_pending_upgrade(UpgradeId.EVOLVEGROOVEDSPINES) == 0 and self.can_afford(\n                    UpgradeId.EVOLVEGROOVEDSPINES\n                ):\n                    hydra_den.research(UpgradeId.EVOLVEGROOVEDSPINES)\n                elif self.already_pending_upgrade(UpgradeId.EVOLVEMUSCULARAUGMENTS) == 0 and self.can_afford(\n                    UpgradeId.EVOLVEMUSCULARAUGMENTS\n                ):\n                    hydra_den.research(UpgradeId.EVOLVEMUSCULARAUGMENTS)\n\n        # If hydra den is ready, train hydra\n        if larvae and self.can_afford(UnitTypeId.HYDRALISK) and self.structures(UnitTypeId.HYDRALISKDEN).ready:\n            larvae.random.train(UnitTypeId.HYDRALISK)\n            return\n\n        # If all our townhalls are dead, send all our units to attack\n        if not self.townhalls:\n            for unit in self.units.of_type(\n                {UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING, UnitTypeId.HYDRALISK}\n            ):\n                unit.attack(self.enemy_start_locations[0])\n            return\n\n        hq: Unit = self.townhalls.first\n\n        # Send idle queens with >=25 energy to inject\n        for queen in self.units(UnitTypeId.QUEEN).idle:\n            # 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\n            # abilities = await self.get_available_abilities(queen)\n            # if AbilityId.EFFECT_INJECTLARVA in abilities:\n            if queen.energy >= 25:\n                queen(AbilityId.EFFECT_INJECTLARVA, hq)\n\n        # Build spawning pool\n        if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0:\n            if self.can_afford(UnitTypeId.SPAWNINGPOOL):\n                await self.build(UnitTypeId.SPAWNINGPOOL, near=hq.position.towards(self.game_info.map_center, 5))\n\n        # Upgrade to lair if spawning pool is complete\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n            if hq.is_idle and not self.townhalls(UnitTypeId.LAIR):\n                if self.can_afford(UnitTypeId.LAIR):\n                    hq.build(UnitTypeId.LAIR)\n\n        # If lair is ready and we have no hydra den on the way: build hydra den\n        if self.townhalls(UnitTypeId.LAIR).ready:\n            if self.structures(UnitTypeId.HYDRALISKDEN).amount + self.already_pending(UnitTypeId.HYDRALISKDEN) == 0:\n                if self.can_afford(UnitTypeId.HYDRALISKDEN):\n                    await self.build(UnitTypeId.HYDRALISKDEN, near=hq.position.towards(self.game_info.map_center, 5))\n\n        # If we have less than 22 drones, build drones\n        if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22:\n            if larvae and self.can_afford(UnitTypeId.DRONE):\n                larva: Unit = larvae.random\n                larva.train(UnitTypeId.DRONE)\n                return\n\n        # If we dont have both extractors: build them\n        if (\n            self.structures(UnitTypeId.SPAWNINGPOOL)\n            and self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2\n        ):\n            if self.can_afford(UnitTypeId.EXTRACTOR):\n                # May crash if we dont have any drones\n                for vespene_geyser in self.vespene_geyser.closer_than(10, hq):\n                    drone: Unit = self.workers.random\n                    drone.build_gas(vespene_geyser)\n                    return\n\n        # Saturate gas\n        for a in self.gas_buildings:\n            if a.assigned_harvesters < a.ideal_harvesters:\n                w: Units = self.workers.closer_than(10, a)\n                if w:\n                    w.random.gather(a)\n\n        # Build queen once the pool is done\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n            if not self.units(UnitTypeId.QUEEN) and hq.is_idle:\n                if self.can_afford(UnitTypeId.QUEEN):\n                    hq.train(UnitTypeId.QUEEN)\n\n        # Train zerglings if we have much more minerals than vespene (not enough gas for hydras)\n        if self.units(UnitTypeId.ZERGLING).amount < 20 and self.minerals > 1000:\n            if larvae and self.can_afford(UnitTypeId.ZERGLING):\n                larvae.random.train(UnitTypeId.ZERGLING)\n\n\ndef main():\n    run_game(\n        maps.get(\"(2)CatalystLE\"),\n        [Bot(Race.Zerg, Hydralisk()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"ZvT.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/onebase_broodlord.py",
    "content": "# noqa: SIM102\nimport random\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass BroodlordBot(BotAI):\n    def select_target(self) -> Point2:\n        if self.enemy_structures:\n            return random.choice(self.enemy_structures).position\n        return self.enemy_start_locations[0]\n\n    async def on_step(self, iteration: int):\n        larvae: Units = self.larva\n        forces: Units = self.units.of_type({UnitTypeId.ZERGLING, UnitTypeId.CORRUPTOR, UnitTypeId.BROODLORD})\n\n        if self.units(UnitTypeId.BROODLORD).amount > 2 and iteration % 50 == 0:\n            for unit in forces:\n                unit.attack(self.select_target())\n\n        if self.supply_left < 2:\n            if larvae and self.can_afford(UnitTypeId.OVERLORD):\n                larvae.random.train(UnitTypeId.OVERLORD)\n                return\n\n        if self.structures(UnitTypeId.GREATERSPIRE).ready:\n            corruptors: Units = self.units(UnitTypeId.CORRUPTOR)\n            # build half-and-half corruptors and broodlords\n            if corruptors and corruptors.amount > self.units(UnitTypeId.BROODLORD).amount:\n                if self.can_afford(UnitTypeId.BROODLORD):\n                    corruptors.random.train(UnitTypeId.BROODLORD)\n            elif larvae and self.can_afford(UnitTypeId.CORRUPTOR):\n                larvae.random.train(UnitTypeId.CORRUPTOR)\n                return\n\n        # Send all units to attack if we dont have any more townhalls\n        if not self.townhalls:\n            all_attack_units: Units = self.units.of_type(\n                {UnitTypeId.DRONE, UnitTypeId.QUEEN, UnitTypeId.ZERGLING, UnitTypeId.CORRUPTOR, UnitTypeId.BROODLORD}\n            )\n            for unit in all_attack_units:\n                unit.attack(self.enemy_start_locations[0])\n            return\n\n        hq: Unit = self.townhalls.first\n\n        # Make idle queens inject\n        for queen in self.units(UnitTypeId.QUEEN).idle:\n            if queen.energy >= 25:\n                queen(AbilityId.EFFECT_INJECTLARVA, hq)\n\n        # Build pool\n        if self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0:\n            if self.can_afford(UnitTypeId.SPAWNINGPOOL):\n                await self.build(UnitTypeId.SPAWNINGPOOL, near=hq)\n\n        # Upgrade to lair\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n            if not self.townhalls(UnitTypeId.LAIR) and not self.townhalls(UnitTypeId.HIVE) and hq.is_idle:\n                if self.can_afford(UnitTypeId.LAIR):\n                    hq.build(UnitTypeId.LAIR)\n\n        # Build infestation pit\n        if self.townhalls(UnitTypeId.LAIR).ready:\n            if self.structures(UnitTypeId.INFESTATIONPIT).amount + self.already_pending(UnitTypeId.INFESTATIONPIT) == 0:\n                if self.can_afford(UnitTypeId.INFESTATIONPIT):\n                    await self.build(UnitTypeId.INFESTATIONPIT, near=hq)\n\n            # Build spire\n            if self.structures(UnitTypeId.SPIRE).amount + self.already_pending(UnitTypeId.SPIRE) == 0:\n                if self.can_afford(UnitTypeId.SPIRE):\n                    await self.build(UnitTypeId.SPIRE, near=hq)\n\n        # Upgrade to hive\n        if self.structures(UnitTypeId.INFESTATIONPIT).ready and not self.townhalls(UnitTypeId.HIVE) and hq.is_idle:\n            if self.can_afford(UnitTypeId.HIVE):\n                hq.build(UnitTypeId.HIVE)\n\n        # Upgrade to greater spire\n        if self.townhalls(UnitTypeId.HIVE).ready:\n            spires: Units = self.structures(UnitTypeId.SPIRE).ready\n            if spires:\n                spire: Unit = spires.random\n                if self.can_afford(UnitTypeId.GREATERSPIRE) and spire.is_idle:\n                    spire.build(UnitTypeId.GREATERSPIRE)\n\n        # Build extractor\n        if self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) < 2:\n            if self.can_afford(UnitTypeId.EXTRACTOR):\n                drone: Unit = self.workers.random\n                target: Unit = self.vespene_geyser.closest_to(drone.position)\n                drone.build_gas(target)\n\n        # Build up to 22 drones\n        if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22:\n            if larvae and self.can_afford(UnitTypeId.DRONE):\n                larva: Unit = larvae.random\n                larva.train(UnitTypeId.DRONE)\n                return\n\n        # Saturate gas\n        for extractor in self.gas_buildings:\n            if extractor.assigned_harvesters < extractor.ideal_harvesters:\n                workers: Units = self.workers.closer_than(20, extractor)\n                if workers:\n                    workers.random.gather(extractor)\n\n        # Build queen\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready:\n            if not self.units(UnitTypeId.QUEEN) and hq.is_idle:\n                if self.can_afford(UnitTypeId.QUEEN):\n                    hq.train(UnitTypeId.QUEEN)\n\n        # Build zerglings if we have not enough gas to build corruptors and broodlords\n        if self.units(UnitTypeId.ZERGLING).amount < 40 and self.minerals > 1000:\n            if larvae and self.can_afford(UnitTypeId.ZERGLING):\n                larvae.random.train(UnitTypeId.ZERGLING)\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Zerg, BroodlordBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"ZvT.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/worker_split.py",
    "content": "\"\"\"\nThis bot is just to demonstrate that you can do worker split\nat game start without having to use 'synchronous_do()'.\nThis is especially important when your bot runs on realtime=True and\nyou want your bot to be reliable against Human opponents.\n\"\"\"\n\nimport asyncio\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.units import Units\n\n\nclass WorkerSplitBot(BotAI):\n    async def on_before_start(self):\n        \"\"\"This function is run before the expansion locations and ramps are calculated. These calculations can take up to a second, depending on the CPU.\"\"\"\n        mf: Units = self.mineral_field\n        for w in self.workers:\n            w.gather(mf.closest_to(w))\n        await self._do_actions(self.actions)\n        self.actions.clear()\n        await asyncio.sleep(3)\n\n    async def on_start(self):\n        \"\"\"This function is run after the expansion locations and ramps are calculated.\"\"\"\n\n    async def on_step(self, iteration: int):\n        if iteration % 10 == 0:\n            await asyncio.sleep(3)\n        # In realtime=False, this should print \"8*x\" and \"x\" if\n        # self.client.game_step is set to 8 (default value)\n        # But if your bot takes too long, it will skip game loops.\n        logger.info(f\"Bot's game loop is {self.state.game_loop} and iteration {iteration}\")\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Zerg, WorkerSplitBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=True,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/zerg/zerg_rush.py",
    "content": "import numpy as np\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race, Result\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.position import Point2, Point3\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass ZergRushBot(BotAI):\n    def __init__(self):\n        self.on_end_called = False\n\n    async def on_start(self):\n        self.client.game_step = 2\n\n    async def on_step(self, iteration: int):\n        if iteration == 0:\n            await self.chat_send(\"(glhf)\")\n\n        # Draw creep pixelmap for debugging\n        # self.draw_creep_pixelmap()\n\n        # If townhall no longer exists: attack move with all units to enemy start location\n        if not self.townhalls:\n            for unit in self.units.exclude_type({UnitTypeId.EGG, UnitTypeId.LARVA}):\n                unit.attack(self.enemy_start_locations[0])\n            return\n\n        hatch: Unit = self.townhalls[0]\n\n        # Pick a target location\n        target_pos: Point2 = self.enemy_structures.not_flying.random_or(self.enemy_start_locations[0]).position\n\n        # Give all zerglings an attack command\n        for zergling in self.units(UnitTypeId.ZERGLING):\n            zergling.attack(target=target_pos)\n\n        # Inject hatchery if queen has more than 25 energy\n        for queen in self.units(UnitTypeId.QUEEN):\n            if queen.energy >= 25 and not hatch.has_buff(BuffId.QUEENSPAWNLARVATIMER):\n                queen(AbilityId.EFFECT_INJECTLARVA, hatch)\n\n        # Pull workers out of gas if we have almost enough gas mined, this will stop mining when we reached 100 gas mined\n        if self.vespene >= 88 or self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) > 0:\n            gas_drones: Units = self.workers.filter(lambda w: w.is_carrying_vespene and len(w.orders) < 2)\n            drone: Unit\n            for drone in gas_drones:\n                minerals: Units = self.mineral_field.closer_than(10, hatch)\n                if minerals:\n                    mineral: Unit = minerals.closest_to(drone)\n                    drone.gather(mineral, queue=True)\n\n        # If we have 100 vespene, this will try to research zergling speed once the spawning pool is at 100% completion\n        if self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 and self.can_afford(\n            UpgradeId.ZERGLINGMOVEMENTSPEED\n        ):\n            spawning_pools_ready: Units = self.structures(UnitTypeId.SPAWNINGPOOL).ready\n            if spawning_pools_ready:\n                self.research(UpgradeId.ZERGLINGMOVEMENTSPEED)\n\n        # If we have less than 2 supply left and no overlord is in the queue: train an overlord\n        if self.supply_left < 2 and self.already_pending(UnitTypeId.OVERLORD) < 1:\n            self.train(UnitTypeId.OVERLORD, 1)\n\n        # While we have less than 88 vespene mined: send drones into extractor one frame at a time\n        if (\n            self.gas_buildings.ready\n            and self.vespene < 88\n            and self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0\n        ):\n            extractor: Unit = self.gas_buildings.first\n            if extractor.surplus_harvesters < 0:\n                self.workers.random.gather(extractor)\n\n        # If we have lost of minerals, make a macro hatchery\n        if self.minerals > 500:\n            for d in range(4, 15):\n                pos: Point2 = hatch.position.towards(self.game_info.map_center, d)\n                if await self.can_place_single(UnitTypeId.HATCHERY, pos):\n                    self.workers.random.build(UnitTypeId.HATCHERY, pos)\n                    break\n\n        # While we have less than 16 drones, make more drones\n        if self.can_afford(UnitTypeId.DRONE) and self.supply_workers < 16:\n            self.train(UnitTypeId.DRONE)\n\n        # If our spawningpool is completed, start making zerglings\n        if self.structures(UnitTypeId.SPAWNINGPOOL).ready and self.larva and self.can_afford(UnitTypeId.ZERGLING):\n            _amount_trained: int = self.train(UnitTypeId.ZERGLING, self.larva.amount)\n\n        # If we have no extractor, build extractor\n        if (\n            self.gas_buildings.amount + self.already_pending(UnitTypeId.EXTRACTOR) == 0\n            and self.can_afford(UnitTypeId.EXTRACTOR)\n            and self.workers\n        ):\n            drone: Unit = self.workers.random\n            target: Unit = self.vespene_geyser.closest_to(drone)\n            drone.build_gas(target)\n\n        # If we have no spawning pool, try to build spawning pool\n        elif self.structures(UnitTypeId.SPAWNINGPOOL).amount + self.already_pending(UnitTypeId.SPAWNINGPOOL) == 0:\n            if self.can_afford(UnitTypeId.SPAWNINGPOOL):\n                for d in range(4, 15):\n                    pos: Point2 = hatch.position.towards(self.game_info.map_center, d)\n                    if await self.can_place_single(UnitTypeId.SPAWNINGPOOL, pos):\n                        drone: Unit = self.workers.closest_to(pos)\n                        drone.build(UnitTypeId.SPAWNINGPOOL, pos)\n\n        # If we have no queen, try to build a queen if we have a spawning pool compelted\n        elif (\n            self.units(UnitTypeId.QUEEN).amount + self.already_pending(UnitTypeId.QUEEN) < self.townhalls.amount\n            and self.structures(UnitTypeId.SPAWNINGPOOL).ready\n        ):\n            if self.can_afford(UnitTypeId.QUEEN):\n                self.train(UnitTypeId.QUEEN)\n\n    def draw_creep_pixelmap(self):\n        for (y, x), value in np.ndenumerate(self.state.creep.data_numpy):\n            p = Point2((x, y))\n            h2 = self.get_terrain_z_height(p)\n            pos = Point3((p.x, p.y, h2))\n            # Red if there is no creep\n            color = Point3((255, 0, 0))\n            if value == 1:\n                # Green if there is creep\n                color = Point3((0, 255, 0))\n            self.client.debug_box2_out(pos, half_vertex_length=0.25, color=color)\n\n    async def on_end(self, game_result: Result):\n        self.on_end_called = True\n        logger.info(f\"{self.time_formatted} On end was called\")\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Zerg, ZergRushBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        save_replay_as=\"ZvT.SC2Replay\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "generate_dicts_from_data_json.py",
    "content": "\"\"\"\nThis script does the following:\n\n- Loop over all abilities, checking what unit they create and if it requires a placement position\n- Loop over all units, checking what abilities they have and which of those create units, and what tech requirements they have\n- Loop over all all upgrades and get their creation ability, which unit can research it and what building requirements there are\n- Loop over all units and get their unit and tech aliases\n\ndata.json origin:\nhttps://github.com/BurnySc2/sc2-techtree/tree/develop/data\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport lzma\nimport pickle\nfrom collections import OrderedDict\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom sc2.game_data import GameData\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\n\n\ndef get_map_file_path() -> Path:\n    return Path(__file__).parent / \"test\" / \"pickle_data\" / \"DeathAuraLE.xz\"\n\n\n# 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\n# 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\nclass OrderedDict2(OrderedDict):\n    def __repr__(self):\n        if not self:\n            return \"{}\"\n        return (\n            \"{\"\n            + \", \".join(f\"{repr(key)}: {repr(value)}\" for key, value in sorted(self.items(), key=lambda u: u[0].name))\n            + \"}\"\n        )\n\n\nclass OrderedSet2(set):\n    def __repr__(self):\n        if not self:\n            return \"set()\"\n        return \"{\" + \", \".join(repr(item) for item in sorted(self, key=lambda u: u.name)) + \"}\"\n\n\ndef dump_dict_to_file(\n    my_dict: OrderedDict2, file_path: Path, dict_name: str, file_header: str = \"\", dict_type_annotation: str = \"\"\n):\n    with file_path.open(\"w\") as f:\n        f.write(file_header)\n        f.write(\"\\n\")\n        f.write(f\"{dict_name}{dict_type_annotation} = \")\n        assert isinstance(my_dict, OrderedDict2)\n        logger.info(my_dict)\n        f.write(repr(my_dict))\n\n\ndef generate_init_file(dict_file_paths: list[Path], file_path: Path, file_header: str):\n    base_file_names = sorted(path.stem for path in dict_file_paths)\n\n    with file_path.open(\"w\") as f:\n        f.write(file_header)\n        f.write(\"\\n\")\n\n        all_line = f\"__all__ = {base_file_names}\"\n        logger.info(all_line)\n        f.write(all_line)\n\n\ndef get_unit_train_build_abilities(data):\n    ability_data = data[\"Ability\"]\n    unit_data = data[\"Unit\"]\n    _upgrade_data = data[\"Upgrade\"]\n\n    # From which abilities can a unit be trained\n    train_abilities: dict[UnitTypeId, set[AbilityId]] = OrderedDict2()\n    # If the ability requires a placement position\n    ability_requires_placement: set[AbilityId] = set()\n    # Map ability to unittypeid\n    ability_to_unittypeid_dict: dict[AbilityId, UnitTypeId] = OrderedDict2()\n\n    # From which abilities can a unit be morphed\n    # unit_morph_abilities: dict[UnitTypeId, set[AbilityId]] = {}\n\n    entry: dict\n    for entry in ability_data:\n        \"\"\"\n        \"target\": \"PointOrUnit\"\n        \"\"\"\n        if isinstance(entry.get(\"target\", {}), str):\n            continue\n        ability_id: AbilityId = AbilityId(entry[\"id\"])\n        created_unit_type_id: UnitTypeId\n\n        # Check if it is a unit train ability\n        requires_placement = False\n        train_unit_type_id_value: int = entry.get(\"target\", {}).get(\"Train\", {}).get(\"produces\", 0)\n        train_place_unit_type_id_value: int = entry.get(\"target\", {}).get(\"TrainPlace\", {}).get(\"produces\", 0)\n        morph_unit_type_id_value: int = entry.get(\"target\", {}).get(\"Morph\", {}).get(\"produces\", 0)\n        build_unit_type_id_value: int = entry.get(\"target\", {}).get(\"Build\", {}).get(\"produces\", 0)\n        build_on_unit_unit_type_id_value: int = entry.get(\"target\", {}).get(\"BuildOnUnit\", {}).get(\"produces\", 0)\n\n        if not train_unit_type_id_value and train_place_unit_type_id_value:\n            train_unit_type_id_value = train_place_unit_type_id_value\n            requires_placement = True\n\n        # Collect larva morph abilities, and one way morphs (exclude burrow, hellbat morph, siege tank siege)\n        # Also doesnt include building addons\n        if not train_unit_type_id_value and (\n            \"LARVATRAIN_\" in ability_id.name\n            or ability_id\n            in {\n                AbilityId.MORPHTOBROODLORD_BROODLORD,\n                AbilityId.MORPHZERGLINGTOBANELING_BANELING,\n                AbilityId.MORPHTORAVAGER_RAVAGER,\n                AbilityId.MORPHTOBANELING_BANELING,\n                AbilityId.MORPH_LURKER,\n                AbilityId.UPGRADETOLAIR_LAIR,\n                AbilityId.UPGRADETOHIVE_HIVE,\n                AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE,\n                AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND,\n                AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS,\n                AbilityId.MORPH_OVERLORDTRANSPORT,\n                AbilityId.MORPH_OVERSEER,\n            }\n        ):\n            # 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:\n            # UnitTypeId.SIEGETANK: {UnitTypeId.SIEGETANKSIEGED, UnitTypeId.FACTORY},\n            # if not train_unit_type_id_value and morph_unit_type_id_value:\n            train_unit_type_id_value = morph_unit_type_id_value\n\n        # Add all build abilities, like construct buildings and train queen (exception)\n        if not train_unit_type_id_value and build_unit_type_id_value:\n            train_unit_type_id_value = build_unit_type_id_value\n            if \"BUILD_\" in entry[\"name\"]:\n                requires_placement = True\n\n        # Add build gas building (refinery, assimilator, extractor)\n        # 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\n        if not train_unit_type_id_value and build_on_unit_unit_type_id_value:\n            train_unit_type_id_value = build_on_unit_unit_type_id_value\n\n        if train_unit_type_id_value:\n            created_unit_type_id = UnitTypeId(train_unit_type_id_value)\n\n            if created_unit_type_id not in train_abilities:\n                train_abilities[created_unit_type_id] = {ability_id}\n            else:\n                train_abilities[created_unit_type_id].add(ability_id)\n            if requires_placement:\n                ability_requires_placement.add(ability_id)\n\n            ability_to_unittypeid_dict[ability_id] = created_unit_type_id\n    \"\"\"\n    unit_train_abilities = {\n        UnitTypeId.GATEWAY: {\n            UnitTypeId.ADEPT: {\n                \"ability\": AbilityId.TRAIN_ADEPT,\n                \"requires_techlab\": False,\n                \"required_building\": UnitTypeId.CYBERNETICSCORE, # Or None\n                \"requires_placement_position\": False, # True for warp gate\n                \"requires_power\": True, # If a pylon nearby is required\n            },\n            UnitTypeId.Zealot: {\n                \"ability\": AbilityId.GATEWAYTRAIN_ZEALOT,\n                ...\n            }\n        }\n    }\n    \"\"\"\n    unit_train_abilities: dict[UnitTypeId, dict[str, AbilityId | bool | UnitTypeId]] = OrderedDict2()\n    for entry in unit_data:\n        unit_abilities = entry.get(\"abilities\", [])\n        unit_type = UnitTypeId(entry[\"id\"])\n        current_unit_train_abilities = OrderedDict2()\n        for ability_info in unit_abilities:\n            ability_id_value: int = ability_info.get(\"ability\", 0)\n            if ability_id_value:\n                ability_id: AbilityId = AbilityId(ability_id_value)\n                # Ability is not a train ability\n                if ability_id not in ability_to_unittypeid_dict:\n                    continue\n\n                requires_techlab: bool = False\n                required_building: UnitTypeId | None = None\n                requires_placement_position: bool = False\n                requires_power: bool = False\n                \"\"\"\n                requirements = [\n                    {\n                        \"addon\": 5\n                    },\n                    {\n                        \"building\": 29\n                    }\n                  ]\n                \"\"\"\n                requirements: list[dict[str, int]] = ability_info.get(\"requirements\", [])\n                if requirements:\n                    # Assume train abilities only have one tech building requirement; thors requiring armory and techlab is seperatedly counted\n                    assert len([req for req in requirements if req.get(\"building\", 0)]) <= 1, (\n                        f\"Error: Building {unit_type} has more than one tech requirements with train ability {ability_id}\"\n                    )\n                    # UnitTypeId 5 == Techlab\n                    requires_techlab: bool = any(req for req in requirements if req.get(\"addon\", 0) == 5)\n                    requires_tech_builing_id_value: int = next(\n                        (req[\"building\"] for req in requirements if req.get(\"building\", 0)), 0\n                    )\n                    if requires_tech_builing_id_value:\n                        required_building = UnitTypeId(requires_tech_builing_id_value)\n\n                if ability_id in ability_requires_placement:\n                    requires_placement_position = True\n\n                requires_power = entry.get(\"needs_power\", False)\n\n                resulting_unit = ability_to_unittypeid_dict[ability_id]\n\n                ability_dict = {\"ability\": ability_id}\n                # Only add boolean values and tech requirement if they actually exist, to make the resulting dict file smaller\n                if requires_techlab:\n                    ability_dict[\"requires_techlab\"] = requires_techlab\n                if required_building:\n                    ability_dict[\"required_building\"] = required_building\n                if requires_placement_position:\n                    ability_dict[\"requires_placement_position\"] = requires_placement_position\n                if requires_power:\n                    ability_dict[\"requires_power\"] = requires_power\n                current_unit_train_abilities[resulting_unit] = ability_dict\n\n        if current_unit_train_abilities:\n            unit_train_abilities[unit_type] = current_unit_train_abilities\n\n    return unit_train_abilities\n\n\ndef get_upgrade_abilities(data):\n    ability_data = data[\"Ability\"]\n    unit_data = data[\"Unit\"]\n    _upgrade_data = data[\"Upgrade\"]\n\n    ability_to_upgrade_dict: dict[AbilityId, UpgradeId] = OrderedDict2()\n    \"\"\"\n    We want to be able to research an upgrade by doing\n    await self.can_research(UpgradeId, return_idle_structures=True) -> returns list of idle structures that can research it\n    So we need to assign each upgrade id one building type, and its research ability and requirements (e.g. armory for infantry level 2)\n    \"\"\"\n\n    # Collect all upgrades and their corresponding abilities\n    entry: dict\n    for entry in ability_data:\n        if isinstance(entry.get(\"target\", {}), str):\n            continue\n        ability_id: AbilityId = AbilityId(entry[\"id\"])\n\n        upgrade_id_value: int = entry.get(\"target\", {}).get(\"Research\", {}).get(\"upgrade\", 0)\n        if upgrade_id_value:\n            upgrade_id: UpgradeId = UpgradeId(upgrade_id_value)\n\n            ability_to_upgrade_dict[ability_id] = upgrade_id\n    \"\"\"\n    unit_research_abilities = {\n        UnitTypeId.ENGINEERINGBAY: {\n            UpgradeId.TERRANINFANTRYWEAPONSLEVEL1:\n            {\n                \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1,\n                \"required_building\": None,\n                \"requires_power\": False, # If a pylon nearby is required\n            },\n            UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: {\n                \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2,\n                \"required_building\": UnitTypeId.ARMORY,\n                \"requires_power\": False, # If a pylon nearby is required\n            },\n        }\n    }\n    \"\"\"\n    unit_research_abilities = OrderedDict2()\n    for entry in unit_data:\n        unit_abilities = entry.get(\"abilities\", [])\n        unit_type = UnitTypeId(entry[\"id\"])\n\n        if unit_type == UnitTypeId.TECHLAB:\n            continue\n\n        current_unit_research_abilities = OrderedDict2()\n        for ability_info in unit_abilities:\n            ability_id_value: int = ability_info.get(\"ability\", 0)\n            if ability_id_value:\n                ability_id: AbilityId = AbilityId(ability_id_value)\n                # Upgrade is not a known upgrade ability\n                if ability_id not in ability_to_upgrade_dict:\n                    continue\n\n                required_building = None\n                required_upgrade = None\n                requirements = ability_info.get(\"requirements\", [])\n                if requirements:\n                    req_building_id_value = next(\n                        (req[\"building\"] for req in requirements if req.get(\"building\", 0)), None\n                    )\n                    if req_building_id_value:\n                        req_building_id = UnitTypeId(req_building_id_value)\n                        required_building = req_building_id\n\n                    req_upgrade_id_value = next((req[\"upgrade\"] for req in requirements if req.get(\"upgrade\", 0)), None)\n                    if req_upgrade_id_value:\n                        req_upgrade_id = UpgradeId(req_upgrade_id_value)\n                        required_upgrade = req_upgrade_id\n\n                requires_power = entry.get(\"needs_power\", False)\n\n                resulting_upgrade = ability_to_upgrade_dict[ability_id]\n\n                research_info = {\"ability\": ability_id}\n                if required_building:\n                    research_info[\"required_building\"] = required_building\n                if required_upgrade:\n                    research_info[\"required_upgrade\"] = required_upgrade\n                if requires_power:\n                    research_info[\"requires_power\"] = requires_power\n                current_unit_research_abilities[resulting_upgrade] = research_info\n\n        if current_unit_research_abilities:\n            unit_research_abilities[unit_type] = current_unit_research_abilities\n\n    return unit_research_abilities\n\n\ndef get_unit_created_from(unit_train_abilities: dict):\n    unit_created_from = OrderedDict2()\n\n    for creator_unit, create_abilities in unit_train_abilities.items():\n        for created_unit, create_info in create_abilities.items():\n            if created_unit not in unit_created_from:\n                unit_created_from[created_unit] = OrderedSet2()\n            unit_created_from[created_unit].add(creator_unit)\n\n    return unit_created_from\n\n\ndef get_upgrade_researched_from(unit_research_abilities: dict):\n    upgrade_researched_from = OrderedDict2()\n\n    for researcher_unit, research_abilities in unit_research_abilities.items():\n        for upgrade, research_info in research_abilities.items():\n            # This if statement is to prevent LAIR and HIVE overriding \"UpgradeId.OVERLORDSPEED\" as well as greater spire overriding upgrade abilities\n            if upgrade not in upgrade_researched_from:\n                upgrade_researched_from[upgrade] = researcher_unit\n\n    return upgrade_researched_from\n\n\ndef get_unit_abilities(data: dict):\n    _ability_data = data[\"Ability\"]\n    unit_data = data[\"Unit\"]\n    _upgrade_data = data[\"Upgrade\"]\n\n    all_unit_abilities: dict[UnitTypeId, set[AbilityId]] = OrderedDict2()\n    entry: dict\n    for entry in unit_data:\n        entry_unit_abilities = entry.get(\"abilities\", [])\n        unit_type = UnitTypeId(entry[\"id\"])\n        current_collected_unit_abilities: set[AbilityId] = OrderedSet2()\n        for ability_info in entry_unit_abilities:\n            ability_id_value: int = ability_info.get(\"ability\", 0)\n            if ability_id_value:\n                ability_id: AbilityId = AbilityId(ability_id_value)\n                current_collected_unit_abilities.add(ability_id)\n\n        # logger.info(unit_type, current_unit_abilities)\n        if current_collected_unit_abilities:\n            all_unit_abilities[unit_type] = current_collected_unit_abilities\n    return all_unit_abilities\n\n\ndef generate_unit_alias_dict(data: dict):\n    _ability_data = data[\"Ability\"]\n    unit_data = data[\"Unit\"]\n    _upgrade_data = data[\"Upgrade\"]\n\n    # Load pickled game data files from one of the test files\n    pickled_file_path = get_map_file_path()\n    assert pickled_file_path.is_file(), f\"Could not find pickled data file {pickled_file_path}\"\n    logger.info(f\"Loading pickled game data file {pickled_file_path}\")\n    with lzma.open(pickled_file_path.absolute(), \"rb\") as f:\n        raw_game_data, raw_game_info, raw_observation = pickle.load(f)\n        game_data = GameData(raw_game_data.data)\n\n    all_unit_aliases: dict[UnitTypeId, UnitTypeId] = OrderedDict2()\n    all_tech_aliases: dict[UnitTypeId, set[UnitTypeId]] = OrderedDict2()\n\n    entry: dict\n    for entry in unit_data:\n        unit_type_value = entry[\"id\"]\n        unit_type = UnitTypeId(entry[\"id\"])\n\n        current_unit_tech_aliases: set[UnitTypeId] = OrderedSet2()\n\n        assert unit_type_value in game_data.units, (\n            f\"Unit {unit_type} not listed in game_data.units - perhaps pickled file {pickled_file_path} is outdated?\"\n        )\n        unit_alias: int = game_data.units[unit_type_value]._proto.unit_alias\n        if unit_alias:\n            # Might be 0 if it has no alias\n            unit_alias_unit_type_id = UnitTypeId(unit_alias)\n            all_unit_aliases[unit_type] = unit_alias_unit_type_id\n\n        tech_aliases: list[int] = game_data.units[unit_type_value]._proto.tech_alias\n\n        for tech_alias in tech_aliases:\n            # Might be 0 if it has no alias\n            unit_alias_unit_type_id = UnitTypeId(tech_alias)\n            current_unit_tech_aliases.add(unit_alias_unit_type_id)\n\n        if current_unit_tech_aliases:\n            all_tech_aliases[unit_type] = current_unit_tech_aliases\n\n    return all_unit_aliases, all_tech_aliases\n\n\ndef generate_redirect_abilities_dict(data: dict):\n    ability_data = data[\"Ability\"]\n    _unit_data = data[\"Unit\"]\n    _upgrade_data = data[\"Upgrade\"]\n\n    all_redirect_abilities: dict[AbilityId, AbilityId] = OrderedDict2()\n\n    entry: dict\n    for entry in ability_data:\n        ability_id_value: int = entry[\"id\"]\n        try:\n            ability_id: AbilityId = AbilityId(ability_id_value)\n        except Exception:\n            logger.info(f\"Error with ability id value {ability_id_value}\")\n            continue\n\n        generic_redirect_ability_value = entry.get(\"remaps_to_ability_id\", 0)\n        if generic_redirect_ability_value == 0:\n            # No generic ability available\n            continue\n        all_redirect_abilities[ability_id] = AbilityId(generic_redirect_ability_value)\n\n    return all_redirect_abilities\n\n\ndef main():\n    path = Path(__file__).parent\n\n    data_path = path / \"data\" / \"data.json\"\n    with data_path.open() as f:\n        data = json.load(f)\n\n    dicts_path = path / \"sc2\" / \"dicts\"\n    Path(dicts_path).mkdir(parents=True, exist_ok=True)\n\n    # All unit train and build abilities\n    unit_train_abilities = get_unit_train_build_abilities(data=data)\n    unit_creation_dict_path = dicts_path / \"unit_train_build_abilities.py\"\n\n    # All upgrades and which building can research which upgrade\n    unit_research_abilities = get_upgrade_abilities(data=data)\n    unit_research_abilities_dict_path = dicts_path / \"unit_research_abilities.py\"\n\n    # All train abilities (where a unit can be trained from)\n    unit_trained_from = get_unit_created_from(unit_train_abilities=unit_train_abilities)\n    unit_trained_from_dict_path = dicts_path / \"unit_trained_from.py\"\n\n    # All research abilities (where an upgrade can be researched from)\n    upgrade_researched_from = get_upgrade_researched_from(unit_research_abilities=unit_research_abilities)\n    upgrade_researched_from_dict_path = dicts_path / \"upgrade_researched_from.py\"\n\n    # All unit abilities without requirements\n    unit_abilities = get_unit_abilities(data=data)\n    unit_abilities_dict_path = dicts_path / \"unit_abilities.py\"\n\n    # All unit_alias and tech_alias of a unit type\n    unit_unit_alias, unit_tech_alias = generate_unit_alias_dict(data=data)\n    unit_unit_alias_dict_path = dicts_path / \"unit_unit_alias.py\"\n    unit_tech_alias_dict_path = dicts_path / \"unit_tech_alias.py\"\n\n    # All redirect (generic) abilities of abilities\n    all_redirect_abilities = generate_redirect_abilities_dict(data=data)\n    all_redirect_abilities_path = dicts_path / \"generic_redirect_abilities.py\"\n\n    file_name = Path(__file__).name\n    file_header = f\"\"\"\n# THIS FILE WAS AUTOMATICALLY GENERATED BY \"{file_name}\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.upgrade_id import UpgradeId\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\nfrom typing import Union\n    \"\"\"\n\n    dict_file_paths = [\n        unit_creation_dict_path,\n        unit_research_abilities_dict_path,\n        unit_trained_from_dict_path,\n        upgrade_researched_from_dict_path,\n        unit_abilities_dict_path,\n        unit_unit_alias_dict_path,\n        unit_tech_alias_dict_path,\n        all_redirect_abilities_path,\n    ]\n    init_file_path = dicts_path / \"__init__.py\"\n    init_header = f\"\"\"# DO NOT EDIT!\n# This file was automatically generated by \"{file_name}\"\n\"\"\"\n    generate_init_file(dict_file_paths=dict_file_paths, file_path=init_file_path, file_header=init_header)\n\n    dump_dict_to_file(\n        unit_train_abilities,\n        unit_creation_dict_path,\n        dict_name=\"TRAIN_INFO\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, dict[UnitTypeId, dict[str, Union[AbilityId, bool, UnitTypeId]]]]\",\n    )\n    dump_dict_to_file(\n        unit_research_abilities,\n        unit_research_abilities_dict_path,\n        dict_name=\"RESEARCH_INFO\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, dict[UpgradeId, dict[str, Union[AbilityId, bool, UnitTypeId, UpgradeId]]]]\",\n    )\n    dump_dict_to_file(\n        unit_trained_from,\n        unit_trained_from_dict_path,\n        dict_name=\"UNIT_TRAINED_FROM\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, set[UnitTypeId]]\",\n    )\n    dump_dict_to_file(\n        upgrade_researched_from,\n        upgrade_researched_from_dict_path,\n        dict_name=\"UPGRADE_RESEARCHED_FROM\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UpgradeId, UnitTypeId]\",\n    )\n    dump_dict_to_file(\n        unit_abilities,\n        unit_abilities_dict_path,\n        dict_name=\"UNIT_ABILITIES\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, set[AbilityId]]\",\n    )\n    dump_dict_to_file(\n        unit_unit_alias,\n        unit_unit_alias_dict_path,\n        dict_name=\"UNIT_UNIT_ALIAS\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, UnitTypeId]\",\n    )\n    dump_dict_to_file(\n        unit_tech_alias,\n        unit_tech_alias_dict_path,\n        dict_name=\"UNIT_TECH_ALIAS\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[UnitTypeId, set[UnitTypeId]]\",\n    )\n    dump_dict_to_file(\n        all_redirect_abilities,\n        all_redirect_abilities_path,\n        dict_name=\"GENERIC_REDIRECT_ABILITIES\",\n        file_header=file_header,\n        dict_type_annotation=\": dict[AbilityId, AbilityId]\",\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "generate_id_constants_from_stableid.py",
    "content": "from sc2.generate_ids import IdGenerator\n\nif __name__ == \"__main__\":\n    id_updater = IdGenerator()\n    id_updater.update_ids_from_stableid_json()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"burnysc2\"\nversion = \"7.2.1\"\ndescription = \"A StarCraft II API Client for Python 3\"\nauthors = [{ name = \"BurnySc2\", email = \"gamingburny@gmail.com\" }]\nrequires-python = \">=3.9, <3.15\"\nkeywords = [\"StarCraft\", \"StarCraft 2\", \"StarCraft II\", \"AI\", \"Bot\"]\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"Intended Audience :: Education\",\n    \"Intended Audience :: Science/Research\",\n    \"Topic :: Games/Entertainment\",\n    \"Topic :: Games/Entertainment :: Real Time Strategy\",\n    \"Topic :: Scientific/Engineering\",\n    \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Operating System :: Microsoft :: Windows\",\n    \"Operating System :: MacOS :: MacOS X\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n]\nreadme = \"README.md\"\nlicense-files = [\"LICENSE\"]\n\ndependencies = [\n    \"aiohttp>=3.11.10\",\n    \"loguru>=0.7.3\",\n    \"mpyq>=0.2.5\",\n    \"numpy>=2.3.3; python_full_version >= '3.14'\",\n    \"numpy>=2.1.0; python_full_version >= '3.13'\",\n    \"numpy>=2.0.0; python_full_version < '3.13'\",\n    \"portpicker>=1.6.0\",\n    \"pys2clientprotocol>=1.0.2\",\n    \"scipy>=1.16.3; python_full_version >= '3.14'\",\n    \"scipy>=1.14.1; python_full_version >= '3.13'\",\n    \"scipy>=1.7.1; python_full_version < '3.13'\",\n]\n\n[dependency-groups]\ndev = [\n    \"coverage>=7.6.9\",\n    \"hypothesis>=6.122.3\",\n    \"matplotlib>=3.9.4\",\n    \"mypy>=1.13.0\",\n    \"pillow>=12.0.0; python_full_version >= '3.14'\",\n    \"pillow>=11.0.0; python_full_version < '3.14'\",\n    \"pre-commit>=4.0.1\",\n    \"protobuf>=6,<8\",\n    \"pyglet>=2.0.20\",\n    \"pylint>=3.3.2\",\n    # Type checker\n    \"pyrefly>=0.58.0\",\n    \"pytest>=8.3.4\",\n    \"pytest-asyncio>=0.25.0\",\n    \"pytest-benchmark>=5.1.0\",\n    \"pytest-cov>=6.0.0\",\n    \"radon>=6.0.1\",\n    # Linter\n    \"ruff>=0.8.3\",\n    \"sphinx-book-theme>=1.1.3\",\n    \"sphinx>=7.4.7\",\n    \"sphinx-autodoc-typehints>=2.3.0\",\n    \"toml>=0.10.2\",\n    \"yapf>=0.43.0\",\n]\n\n[tool.setuptools]\npackage-dir = { sc2 = \"sc2\" }\n\n[tool.setuptools.package-data]\nsc2 = [\"py.typed\", \"*.pyi\"]\n\n[build-system]\n# https://packaging.python.org/en/latest/tutorials/packaging-projects/#choosing-a-build-backend\n# https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#custom-discovery\nrequires = [\"setuptools>=61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project.urls]\nRepository = \"https://github.com/Burnysc2/python-sc2\"\nDocumentation = \"https://burnysc2.github.io/python-sc2\"\n\n[tool.yapf]\nbased_on_style = \"pep8\"\ncolumn_limit = 120\nsplit_arguments_when_comma_terminated = true\ndedent_closing_brackets = true\nallow_split_before_dict_value = false\n\n[tool.pyrefly]\nproject_includes = [\n    \"sc2\",\n    \"examples\",\n    \"test\"\n]\nproject-excludes = [\n    # Disable for those files and folders\n    \"sc2/data.py\",\n]\n\n[tool.pyrefly.errors]\nbad-override = false\ninconsistent-overload = false\n\n[tool.ruff]\ntarget-version = 'py310'\nline-length = 120\n\n[tool.ruff.lint]\n# Allow unused variables when underscore-prefixed.\ndummy-variable-rgx = \"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$\"\nselect = [\n    \"C4\",  # flake8-comprehensions\n    \"E\",   # Error\n    \"F\",   # pyflakes\n    \"BLE\", # flake8-blind-except\n    # \"I\",   # isort\n    \"N\",   # pep8-naming\n    \"PGH\", # pygrep-hooks\n    \"PTH\", # flake8-use-pathlib\n    \"SIM\", # flake8-simplify\n    \"W\",   # Warning\n    \"Q\",   # flake8-quotes\n    \"YTT\", # flake8-2020\n    \"UP\",  # pyupgrade\n    #    \"A\",  # flake8-builtins\n]\n# Allow Pydantic's `@validator` decorator to trigger class method treatment.\npep8-naming.classmethod-decorators = [\"pydantic.validator\", \"classmethod\"]\nignore = [\n    \"E501\",   # Line too long\n    \"E402\",   # Module level import not at top of file\n    \"F841\",   # Local variable `...` is assigned to but never used\n    \"BLE001\", # Do not catch blind exception: `Exception`\n    \"N802\",   # Function name `...` should be lowercase\n    \"N806\",   # Variable `...` in function should be lowercase.\n    \"SIM102\", # Use a single `if` statement instead of nested `if` statements\n    \"UP007\",  # Use `X | Y` for type annotations\n    \"UP038\",  # Use `X | Y` in `isinstance` call instead of `(X, Y)`\n]\n\n[tool.pyupgrade]\n# Preserve types, even if a file imports `from __future__ import annotations`.\n# Remove once support for py3.8 and 3.9 is dropped\nkeep-runtime-typing = true\n\n[tool.pep8-naming]\n# Allow Pydantic's `@validator` decorator to trigger class method treatment.\nclassmethod-decorators = [\"pydantic.validator\", \"classmethod\"]\n"
  },
  {
    "path": "sc2/__init__.py",
    "content": "from pathlib import Path\n\n\ndef is_submodule(path):\n    if path.is_file():\n        return path.suffix == \".py\" and path.stem != \"__init__\"\n    if path.is_dir():\n        return (path / \"__init__.py\").exists()\n    return False\n\n\n__all__ = [p.stem for p in Path(__file__).parent.iterdir() if is_submodule(p)]\n"
  },
  {
    "path": "sc2/action.py",
    "content": "from __future__ import annotations\n\nfrom itertools import groupby\nfrom typing import TYPE_CHECKING\n\nfrom s2clientprotocol import raw_pb2 as raw_pb\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\n\nif TYPE_CHECKING:\n    from sc2.ids.ability_id import AbilityId\n    from sc2.unit_command import UnitCommand\n\n\ndef combine_actions(action_iter: list[UnitCommand]):\n    \"\"\"\n    Example input:\n    [\n        # Each entry in the list is a unit command, with an ability, unit, target, and queue=boolean\n        UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hive', tag=4353687554), None, False),\n        UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Lair', tag=4359979012), None, False),\n        UnitCommand(AbilityId.TRAINQUEEN_QUEEN, Unit(name='Hatchery', tag=4359454723), None, False),\n    ]\n    \"\"\"\n    for key, items in groupby(action_iter, key=lambda a: a.combining_tuple):\n        ability: AbilityId\n        target: None | Point2 | Unit\n        queue: bool\n        # See constants.py for combineable abilities\n        combineable: bool\n        ability, target, queue, combineable = key\n\n        if combineable:\n            # Combine actions with no target, e.g. lift, burrowup, burrowdown, siege, unsiege, uproot spines\n            cmd = raw_pb.ActionRawUnitCommand(\n                ability_id=ability.value,\n                # pyrefly: ignore\n                unit_tags={u.unit.tag for u in items},\n                queue_command=queue,\n            )\n            # Combine actions with target point, e.g. attack_move or move commands on a position\n            if isinstance(target, Point2):\n                cmd.target_world_space_pos.x = target.x\n                cmd.target_world_space_pos.y = target.y\n            # Combine actions with target unit, e.g. attack commands directly on a unit\n            elif isinstance(target, Unit):\n                cmd.target_unit_tag = target.tag\n            elif target is not None:\n                raise RuntimeError(f\"Must target a unit, point or None, found '{target!r}'\")\n\n            yield raw_pb.ActionRaw(unit_command=cmd)\n\n        else:\n            \"\"\"\n            Return one action for each unit; this is required for certain commands that would otherwise be grouped, and only executed once\n            Examples:\n            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\n            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)\n            However, other abilities can and should be grouped, see constants.py 'COMBINEABLE_ABILITIES'\n            \"\"\"\n            if target is None:\n                for u in items:\n                    cmd = raw_pb.ActionRawUnitCommand(\n                        ability_id=ability.value,\n                        # pyrefly: ignore\n                        unit_tags={u.unit.tag},\n                        queue_command=queue,\n                    )\n                    yield raw_pb.ActionRaw(unit_command=cmd)\n            elif isinstance(target, Point2):\n                for u in items:\n                    cmd = raw_pb.ActionRawUnitCommand(\n                        ability_id=ability.value,\n                        # pyrefly: ignore\n                        unit_tags={u.unit.tag},\n                        queue_command=queue,\n                        target_world_space_pos=target.as_Point2D,\n                    )\n                    yield raw_pb.ActionRaw(unit_command=cmd)\n            elif isinstance(target, Unit):\n                for u in items:\n                    cmd = raw_pb.ActionRawUnitCommand(\n                        ability_id=ability.value,\n                        # pyrefly: ignore\n                        unit_tags={u.unit.tag},\n                        queue_command=queue,\n                        target_unit_tag=target.tag,\n                    )\n                    yield raw_pb.ActionRaw(unit_command=cmd)\n            else:\n                raise RuntimeError(f\"Must target a unit, point or None, found '{target!r}'\")\n"
  },
  {
    "path": "sc2/bot_ai.py",
    "content": "from __future__ import annotations\n\nimport math\nimport random\nimport warnings\nfrom collections import Counter\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING\n\nfrom loguru import logger\n\nfrom sc2.bot_ai_internal import BotAIInternal\nfrom sc2.cache import property_cache_once_per_frame\nfrom sc2.constants import (\n    CREATION_ABILITY_FIX,\n    EQUIVALENTS_FOR_TECH_PROGRESS,\n    PROTOSS_TECH_REQUIREMENT,\n    TERRAN_STRUCTURES_REQUIRE_SCV,\n    TERRAN_TECH_REQUIREMENT,\n    ZERG_TECH_REQUIREMENT,\n)\nfrom sc2.data import Alert, Race, Result, Target\nfrom sc2.dicts.unit_research_abilities import RESEARCH_INFO\nfrom sc2.dicts.unit_train_build_abilities import TRAIN_INFO\nfrom sc2.dicts.unit_trained_from import UNIT_TRAINED_FROM\nfrom sc2.dicts.upgrade_researched_from import UPGRADE_RESEARCHED_FROM\nfrom sc2.game_data import AbilityData, Cost\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\nif TYPE_CHECKING:\n    from sc2.game_info import Ramp\n\n\nclass BotAI(BotAIInternal):\n    \"\"\"Base class for bots.\"\"\"\n\n    EXPANSION_GAP_THRESHOLD = 15\n\n    @property\n    def time(self) -> float:\n        \"\"\"Returns time in seconds, assumes the game is played on 'faster'\"\"\"\n        return self.state.game_loop / 22.4  # / (1/1.4) * (1/16)\n\n    @property\n    def time_formatted(self) -> str:\n        \"\"\"Returns time as string in min:sec format\"\"\"\n        t = self.time\n        return f\"{int(t // 60):02}:{int(t % 60):02}\"\n\n    @property\n    def step_time(self) -> tuple[float, float, float, float]:\n        \"\"\"Returns a tuple of step duration in milliseconds.\n        First value is the minimum step duration - the shortest the bot ever took\n        Second value is the average step duration\n        Third value is the maximum step duration - the longest the bot ever took (including on_start())\n        Fourth value is the step duration the bot took last iteration\n        If called in the first iteration, it returns (inf, 0, 0, 0)\"\"\"\n        avg_step_duration = (\n            (self._total_time_in_on_step / self._total_steps_iterations) if self._total_steps_iterations else 0\n        )\n        return (\n            self._min_step_time * 1000,\n            avg_step_duration * 1000,\n            self._max_step_time * 1000,\n            self._last_step_step_time * 1000,\n        )\n\n    def alert(self, alert_code: Alert) -> bool:\n        \"\"\"\n        Check if alert is triggered in the current step.\n        Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702\n\n        Example use::\n\n            from sc2.data import Alert\n            if self.alert(Alert.AddOnComplete):\n                print(\"Addon Complete\")\n\n        Alert codes::\n\n            AlertError\n            AddOnComplete\n            BuildingComplete\n            BuildingUnderAttack\n            LarvaHatched\n            MergeComplete\n            MineralsExhausted\n            MorphComplete\n            MothershipComplete\n            MULEExpired\n            NuclearLaunchDetected\n            NukeComplete\n            NydusWormDetected\n            ResearchComplete\n            TrainError\n            TrainUnitComplete\n            TrainWorkerComplete\n            TransformationComplete\n            UnitUnderAttack\n            UpgradeComplete\n            VespeneExhausted\n            WarpInComplete\n\n        :param alert_code:\n        \"\"\"\n        assert isinstance(alert_code, Alert), f\"alert_code {alert_code} is no Alert\"\n        return alert_code.value in self.state.alerts\n\n    @property\n    def start_location(self) -> Point2:\n        \"\"\"\n        Returns the spawn location of the bot, using the position of the first created townhall.\n        This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start.\n        \"\"\"\n        return self.game_info.player_start_location\n\n    @property\n    def enemy_start_locations(self) -> list[Point2]:\n        \"\"\"Possible start locations for enemies.\"\"\"\n        return self.game_info.start_locations\n\n    @cached_property\n    def main_base_ramp(self) -> Ramp:\n        \"\"\"Returns the Ramp instance of the closest main-ramp to start location.\n        Look in game_info.py for more information about the Ramp class\n\n        Example: See terran ramp wall bot\n        \"\"\"\n        # The reason for len(ramp.upper) in {2, 5} is:\n        # ParaSite map has 5 upper points, and most other maps have 2 upper points at the main ramp.\n        # The map Acolyte has 4 upper points at the wrong ramp (which is closest to the start position).\n        try:\n            found_main_base_ramp = min(\n                (ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {2, 5}),\n                key=lambda r: self.start_location.distance_to(r.top_center),\n            )\n        except ValueError:\n            # Hardcoded hotfix for Honorgrounds LE map, as that map has a large main base ramp with inbase natural\n            found_main_base_ramp = min(\n                (ramp for ramp in self.game_info.map_ramps if len(ramp.upper) in {4, 9}),\n                key=lambda r: self.start_location.distance_to(r.top_center),\n            )\n        return found_main_base_ramp\n\n    @property_cache_once_per_frame\n    def expansion_locations_list(self) -> list[Point2]:\n        \"\"\"Returns a list of expansion positions, not sorted in any way.\"\"\"\n        assert self._expansion_positions_list, (\n            \"self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless.\"\n        )\n        return self._expansion_positions_list\n\n    @property_cache_once_per_frame\n    def expansion_locations_dict(self) -> dict[Point2, Units]:\n        \"\"\"\n        Returns dict with the correct expansion position Point2 object as key,\n        resources as Units (mineral fields and vespene geysers) as value.\n\n        Caution: This function is slow. If you only need the expansion locations, use the property above.\n        \"\"\"\n        assert self._expansion_positions_list, (\n            \"self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless.\"\n        )\n        expansion_locations: dict[Point2, Units] = {pos: Units([], self) for pos in self._expansion_positions_list}\n        for resource in self.resources:\n            # It may be that some resources are not mapped to an expansion location\n            exp_positions: set[Point2] | None = self._resource_location_to_expansion_position_dict.get(\n                resource.position, None\n            )\n            if exp_positions:\n                for exp_position in exp_positions:\n                    assert exp_position in expansion_locations\n                    expansion_locations[exp_position].append(resource)\n        return expansion_locations\n\n    @property\n    def units_created(self) -> Counter[UnitTypeId]:\n        \"\"\"Returns a Counter for all your units and buildings you have created so far.\n\n        This may be used for statistics (at the end of the game) or for strategic decision making.\n\n        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'.\n        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).\n\n        Examples::\n\n            # Give attack command to enemy base every time 10 marines have been trained\n            async def on_unit_created(self, unit: Unit):\n                if unit.type_id == UnitTypeId.MARINE:\n                    if self.units_created[MARINE] % 10 == 0:\n                        for marine in self.units(UnitTypeId.MARINE):\n                            marine.attack(self.enemy_start_locations[0])\n        \"\"\"\n        return self._units_created\n\n    async def get_available_abilities(\n        self, units: list[Unit] | Units, ignore_resource_requirements: bool = False\n    ) -> list[list[AbilityId]]:\n        \"\"\"Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched.\n\n        Examples::\n\n            units_abilities = await self.get_available_abilities(self.units)\n\n        or::\n\n            units_abilities = await self.get_available_abilities([self.units.random])\n\n        :param units:\n        :param ignore_resource_requirements:\"\"\"\n        return await self.client.query_available_abilities(units, ignore_resource_requirements)\n\n    async def expand_now(\n        self,\n        building: UnitTypeId | None = None,\n        max_distance: int = 10,\n        location: Point2 | None = None,\n    ) -> None:\n        \"\"\"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.\n\n        :param building:\n        :param max_distance:\n        :param location:\"\"\"\n\n        if building is None:\n            # self.race is never Race.Random\n            start_townhall_type = {\n                Race.Protoss: UnitTypeId.NEXUS,\n                Race.Terran: UnitTypeId.COMMANDCENTER,\n                Race.Zerg: UnitTypeId.HATCHERY,\n            }\n            building = start_townhall_type[self.race]\n\n        assert isinstance(building, UnitTypeId), f\"{building} is no UnitTypeId\"\n\n        if not location:\n            location = await self.get_next_expansion()\n        if not location:\n            # All expansions are used up or mined out\n            logger.warning(\"Trying to expand_now() but bot is out of locations to expand to\")\n            return\n        await self.build(building, near=location, max_distance=max_distance, random_alternative=False, placement_step=1)\n\n    async def get_next_expansion(self) -> Point2 | None:\n        \"\"\"Find next expansion location.\"\"\"\n\n        closest = None\n        best_distance = math.inf\n        start_position = self.game_info.player_start_location\n        for position in self.expansion_locations_list:\n\n            def is_near_to_expansion(t):\n                return t.distance_to(position) < self.EXPANSION_GAP_THRESHOLD\n\n            if any(map(is_near_to_expansion, self.townhalls)):\n                # already taken\n                continue\n\n            distance = await self.client.query_pathing(start_position, position)\n            if distance is None:\n                continue\n\n            if distance < best_distance:\n                best_distance = distance\n                closest = position\n\n        return closest\n\n    async def distribute_workers(self, resource_ratio: float = 2) -> None:\n        \"\"\"\n        Distributes workers across all the bases taken.\n        Keyword `resource_ratio` takes a float. If the current minerals to gas\n        ratio is bigger than `resource_ratio`, this function prefer filling gas_buildings\n        first, if it is lower, it will prefer sending workers to minerals first.\n\n        NOTE: This function is far from optimal, if you really want to have\n        refined worker control, you should write your own distribution function.\n        For example long distance mining control and moving workers if a base was killed\n        are not being handled.\n\n        WARNING: This is quite slow when there are lots of workers or multiple bases.\n\n        :param resource_ratio:\"\"\"\n        if not self.mineral_field or not self.workers or not self.townhalls.ready:\n            return\n        worker_pool = self.workers.idle\n        bases = self.townhalls.ready\n        gas_buildings = self.gas_buildings.ready\n\n        # list of places that need more workers\n        deficit_mining_places = []\n\n        for mining_place in bases | gas_buildings:\n            difference = mining_place.surplus_harvesters\n            # perfect amount of workers, skip mining place\n            if not difference:\n                continue\n            if mining_place.has_vespene:\n                # get all workers that target the gas extraction site\n                # or are on their way back from it\n                local_workers = self.workers.filter(\n                    lambda unit: unit.order_target == mining_place.tag\n                    or (unit.is_carrying_vespene and unit.order_target == bases.closest_to(mining_place).tag)\n                )\n            else:\n                # get tags of minerals around expansion\n                local_minerals_tags = {\n                    mineral.tag for mineral in self.mineral_field if mineral.distance_to(mining_place) <= 8\n                }\n                # get all target tags a worker can have\n                # tags of the minerals he could mine at that base\n                # get workers that work at that gather site\n                local_workers = self.workers.filter(\n                    lambda unit: unit.order_target in local_minerals_tags\n                    or (unit.is_carrying_minerals and unit.order_target == mining_place.tag)\n                )\n            # too many workers\n            if difference > 0:\n                for worker in local_workers[:difference]:\n                    worker_pool.append(worker)\n            # too few workers\n            # add mining place to deficit bases for every missing worker\n            else:\n                deficit_mining_places += [mining_place for _ in range(-difference)]\n\n        # prepare all minerals near a base if we have too many workers\n        # and need to send them to the closest patch\n        all_minerals_near_base = []\n        if len(worker_pool) > len(deficit_mining_places):\n            all_minerals_near_base = [\n                mineral\n                for mineral in self.mineral_field\n                if any(mineral.distance_to(base) <= 8 for base in self.townhalls.ready)\n            ]\n        # distribute every worker in the pool\n        for worker in worker_pool:\n            # as long as have workers and mining places\n            if deficit_mining_places:\n                # choose only mineral fields first if current mineral to gas ratio is less than target ratio\n                if self.vespene and self.minerals / self.vespene < resource_ratio:\n                    possible_mining_places = [place for place in deficit_mining_places if not place.vespene_contents]\n                # else prefer gas\n                else:\n                    possible_mining_places = [place for place in deficit_mining_places if place.vespene_contents]\n                # if preferred type is not available any more, get all other places\n                if not possible_mining_places:\n                    possible_mining_places = deficit_mining_places\n                # find closest mining place\n                current_place = min(deficit_mining_places, key=lambda place: place.distance_to(worker))\n                # remove it from the list\n                deficit_mining_places.remove(current_place)\n                # if current place is a gas extraction site, go there\n                if current_place.vespene_contents:\n                    worker.gather(current_place)\n                # if current place is a gas extraction site,\n                # go to the mineral field that is near and has the most minerals left\n                else:\n                    local_minerals = (\n                        mineral for mineral in self.mineral_field if mineral.distance_to(current_place) <= 8\n                    )\n                    # local_minerals can be empty if townhall is misplaced\n                    target_mineral = max(local_minerals, key=lambda mineral: mineral.mineral_contents, default=None)\n                    if target_mineral:\n                        worker.gather(target_mineral)\n            # more workers to distribute than free mining spots\n            # send to closest if worker is doing nothing\n            elif worker.is_idle and all_minerals_near_base:\n                target_mineral = min(all_minerals_near_base, key=lambda mineral: mineral.distance_to(worker))\n                worker.gather(target_mineral)\n            else:\n                # there are no deficit mining places and worker is not idle\n                # so dont move him\n                pass\n\n    @property_cache_once_per_frame\n    def owned_expansions(self) -> dict[Point2, Unit]:\n        \"\"\"Dict of expansions owned by the player with mapping {expansion_location: townhall_structure}.\"\"\"\n        owned = {}\n        for el in self.expansion_locations_list:\n\n            def is_near_to_expansion(t):\n                return t.distance_to(el) < self.EXPANSION_GAP_THRESHOLD\n\n            th = next((x for x in self.townhalls if is_near_to_expansion(x)), None)\n            if th:\n                owned[el] = th\n        return owned\n\n    def calculate_supply_cost(self, unit_type: UnitTypeId) -> float:\n        \"\"\"\n        This function calculates the required supply to train or morph a unit.\n        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.\n        The total supply of a ravager is 3, but a roach already uses up 2 supply, so the morph supply cost is 1.\n        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.\n\n        Example::\n\n            roach_supply_cost = self.calculate_supply_cost(UnitTypeId.ROACH) # Is 2\n            ravager_supply_cost = self.calculate_supply_cost(UnitTypeId.RAVAGER) # Is 1\n            baneling_supply_cost = self.calculate_supply_cost(UnitTypeId.BANELING) # Is 0\n\n        :param unit_type:\"\"\"\n        if unit_type in {UnitTypeId.ZERGLING}:\n            return 1\n        if unit_type in {UnitTypeId.BANELING}:\n            return 0\n        unit_supply_cost = self.game_data.units[unit_type.value]._proto.food_required\n        if unit_supply_cost > 0 and unit_type in UNIT_TRAINED_FROM and len(UNIT_TRAINED_FROM[unit_type]) == 1:\n            producer: UnitTypeId\n            for producer in UNIT_TRAINED_FROM[unit_type]:\n                producer_unit_data = self.game_data.units[producer.value]\n                if producer_unit_data._proto.food_required <= unit_supply_cost:\n                    producer_supply_cost = producer_unit_data._proto.food_required\n                    unit_supply_cost -= producer_supply_cost\n        return unit_supply_cost\n\n    def can_feed(self, unit_type: UnitTypeId) -> bool:\n        \"\"\"Checks if you have enough free supply to build the unit\n\n        Example::\n\n            cc = self.townhalls.idle.random_or(None)\n            # self.townhalls can be empty or there are no idle townhalls\n            if cc and self.can_feed(UnitTypeId.SCV):\n                cc.train(UnitTypeId.SCV)\n\n        :param unit_type:\"\"\"\n        required = self.calculate_supply_cost(unit_type)\n        # \"required <= 0\" in case self.supply_left is negative\n        return required <= 0 or self.supply_left >= required\n\n    def calculate_unit_value(self, unit_type: UnitTypeId) -> Cost:\n        \"\"\"\n        Unlike the function below, this function returns the value of a unit given by the API (e.g. the resources lost value on kill).\n\n        Examples::\n\n            self.calculate_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0)\n            self.calculate_value(UnitTypeId.RAVAGER) == Cost(100, 100)\n            self.calculate_value(UnitTypeId.ARCHON) == Cost(175, 275)\n\n        :param unit_type:\n        \"\"\"\n        unit_data = self.game_data.units[unit_type.value]\n        return Cost(unit_data._proto.mineral_cost, unit_data._proto.vespene_cost)\n\n    def calculate_cost(self, item_id: UnitTypeId | UpgradeId | AbilityId) -> Cost:\n        \"\"\"\n        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.\n        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.\n\n        It is adviced to use the UnitTypeId instead of the AbilityId. Instead of::\n\n            self.calculate_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND)\n\n        use::\n\n            self.calculate_cost(UnitTypeId.ORBITALCOMMAND)\n\n        More examples::\n\n            from sc2.game_data import Cost\n\n            self.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150)\n            self.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75)\n            self.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25)\n            self.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0)\n            self.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50)\n            self.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25)\n            self.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0)\n            self.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0)\n            self.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100)\n            self.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150)\n\n        :param item_id:\n        \"\"\"\n        if isinstance(item_id, UnitTypeId):\n            # Fix cost for reactor and techlab where the API returns 0 for both\n            if item_id in {UnitTypeId.REACTOR, UnitTypeId.TECHLAB, UnitTypeId.ARCHON, UnitTypeId.BANELING}:\n                if item_id == UnitTypeId.REACTOR:\n                    return Cost(50, 50)\n                if item_id == UnitTypeId.TECHLAB:\n                    return Cost(50, 25)\n                if item_id == UnitTypeId.BANELING:\n                    return Cost(25, 25)\n                if item_id == UnitTypeId.ARCHON:\n                    return self.calculate_unit_value(UnitTypeId.ARCHON)\n            unit_data = self.game_data.units[item_id.value]\n            # Cost of morphs is automatically correctly calculated by 'calculate_ability_cost'\n            creation_ability = unit_data.creation_ability\n            if creation_ability is None:\n                logger.error(f\"Unknown creation_ability in calculate_cost for item_id: {item_id}\")\n                return Cost(0, 0)\n            return self.game_data.calculate_ability_cost(creation_ability.exact_id)\n\n        if isinstance(item_id, UpgradeId):\n            cost = self.game_data.upgrades[item_id.value].cost\n        else:\n            # Is already AbilityId\n            cost = self.game_data.calculate_ability_cost(item_id)\n        return cost\n\n    def can_afford(self, item_id: UnitTypeId | UpgradeId | AbilityId, check_supply_cost: bool = True) -> bool:\n        \"\"\"Tests if the player has enough resources to build a unit or structure.\n\n        Example::\n\n            cc = self.townhalls.idle.random_or(None)\n            # self.townhalls can be empty or there are no idle townhalls\n            if cc and self.can_afford(UnitTypeId.SCV):\n                cc.train(UnitTypeId.SCV)\n\n        Example::\n\n            # Current state: we have 150 minerals and one command center and a barracks\n            can_afford_morph = self.can_afford(UnitTypeId.ORBITALCOMMAND, check_supply_cost=False)\n            # Will be 'True' although the API reports that an orbital is worth 550 minerals, but the morph cost is only 150 minerals\n\n        :param item_id:\n        :param check_supply_cost:\"\"\"\n        cost = self.calculate_cost(item_id)\n        if cost.minerals > self.minerals or cost.vespene > self.vespene:\n            return False\n        if check_supply_cost and isinstance(item_id, UnitTypeId):\n            supply_cost = self.calculate_supply_cost(item_id)\n            if supply_cost and supply_cost > self.supply_left:\n                return False\n        return True\n\n    async def can_cast(\n        self,\n        unit: Unit,\n        ability_id: AbilityId,\n        target: Unit | Point2 | None = None,\n        only_check_energy_and_cooldown: bool = False,\n        cached_abilities_of_unit: list[AbilityId] | None = None,\n    ) -> bool:\n        \"\"\"Tests if a unit has an ability available and enough energy to cast it.\n\n        Example::\n\n            stalkers = self.units(UnitTypeId.STALKER)\n            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)))\n\n        See data_pb2.py (line 161) for the numbers 1-5 to make sense\n\n        :param unit:\n        :param ability_id:\n        :param target:\n        :param only_check_energy_and_cooldown:\n        :param cached_abilities_of_unit:\"\"\"\n        assert isinstance(unit, Unit), f\"{unit} is no Unit object\"\n        assert isinstance(ability_id, AbilityId), f\"{ability_id} is no AbilityId\"\n        assert isinstance(target, (type(None), Unit, Point2))\n        # check if unit has enough energy to cast or if ability is on cooldown\n        if cached_abilities_of_unit:\n            abilities = cached_abilities_of_unit\n        else:\n            abilities = (await self.get_available_abilities([unit], ignore_resource_requirements=False))[0]\n\n        if ability_id in abilities:\n            if only_check_energy_and_cooldown:\n                return True\n            cast_range = self.game_data.abilities[ability_id.value]._proto.cast_range\n            ability_target: int = self.game_data.abilities[ability_id.value]._proto.target\n            # Check if target is in range (or is a self cast like stimpack)\n            if (\n                # Can't replace 1 with \"Target.None.value\" because \".None\" doesn't seem to be a valid enum name\n                ability_target == 1\n                or ability_target == Target.PointOrNone.value\n                and (\n                    # Target is unit\n                    isinstance(target, Unit)\n                    and unit.distance_to(target) <= unit.radius + target.radius + cast_range\n                    # Target is position\n                    or isinstance(target, Point2)\n                    and unit.distance_to(target) <= unit.radius + cast_range\n                )\n            ):\n                return True\n            # Check if able to use ability on a unit\n            if (\n                ability_target in {Target.Unit.value, Target.PointOrUnit.value}\n                and isinstance(target, Unit)\n                and unit.distance_to(target) <= unit.radius + target.radius + cast_range\n            ):\n                return True\n            # Check if able to use ability on a position\n            if (\n                ability_target in {Target.Point.value, Target.PointOrUnit.value}\n                and isinstance(target, Point2)\n                and unit.distance_to(target) <= unit.radius + cast_range\n            ):\n                return True\n        return False\n\n    def select_build_worker(self, pos: Unit | Point2, force: bool = False) -> Unit | None:\n        \"\"\"Select a worker to build a building with.\n\n        Example::\n\n            barracks_placement_position = self.main_base_ramp.barracks_correct_placement\n            worker = self.select_build_worker(barracks_placement_position)\n            # Can return None\n            if worker:\n                worker.build(UnitTypeId.BARRACKS, barracks_placement_position)\n\n        :param pos:\n        :param force:\"\"\"\n        workers = (\n            self.workers.filter(lambda w: (w.is_gathering or w.is_idle) and w.distance_to(pos) < 20) or self.workers\n        )\n        if workers:\n            for worker in workers.sorted_by_distance_to(pos).prefer_idle:\n                if (\n                    worker not in self.unit_tags_received_action\n                    and not worker.orders\n                    or len(worker.orders) == 1\n                    and worker.orders[0].ability.id in {AbilityId.MOVE, AbilityId.HARVEST_GATHER}\n                ):\n                    return worker\n\n            return workers.random if force else None\n        return None\n\n    async def can_place_single(self, building: AbilityId | UnitTypeId, position: Point2) -> bool:\n        \"\"\"Checks the placement for only one position.\"\"\"\n        if isinstance(building, UnitTypeId):\n            creation_ability = self.game_data.units[building.value].creation_ability\n            if creation_ability is None:\n                logger.error(f\"Unknown creation_ability in can_place_single for building: {building}\")\n                return False\n            creation_ability_id = creation_ability.id\n            return (await self.client._query_building_placement_fast(creation_ability_id, [position]))[0]\n        return (await self.client._query_building_placement_fast(building, [position]))[0]\n\n    async def can_place(self, building: AbilityData | AbilityId | UnitTypeId, positions: list[Point2]) -> list[bool]:\n        \"\"\"Tests if a building can be placed in the given locations.\n\n        Example::\n\n            barracks_placement_position = self.main_base_ramp.barracks_correct_placement\n            worker = self.select_build_worker(barracks_placement_position)\n            # Can return None\n            if worker and (await self.can_place(UnitTypeId.BARRACKS, [barracks_placement_position])[0]:\n                worker.build(UnitTypeId.BARRACKS, barracks_placement_position)\n\n        :param building:\n        :param position:\"\"\"\n        if isinstance(building, UnitTypeId):\n            creation_ability = self.game_data.units[building.value].creation_ability\n            if creation_ability is None:\n                return [False for _ in positions]\n            building = creation_ability.id\n        elif isinstance(building, AbilityData):\n            warnings.warn(\n                \"Using AbilityData is deprecated and may be removed soon. Please use AbilityId or UnitTypeId instead.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            building = building.id\n\n        if isinstance(positions, (Point2, tuple)):\n            warnings.warn(\n                \"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]\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n            # pyrefly: ignore\n            return await self.can_place_single(building, positions)\n        assert isinstance(positions, list), f\"Expected an iterable (list, tuple), but was: {positions}\"\n        assert isinstance(positions[0], Point2), (\n            f\"List is expected to have Point2, but instead had: {positions[0]} {type(positions[0])}\"\n        )\n        return await self.client._query_building_placement_fast(building, positions)\n\n    async def find_placement(\n        self,\n        building: UnitTypeId | AbilityId,\n        near: Point2,\n        max_distance: int = 20,\n        random_alternative: bool = True,\n        placement_step: int = 2,\n        addon_place: bool = False,\n    ) -> Point2 | None:\n        \"\"\"Finds a placement location for building.\n\n        Example::\n\n            if self.townhalls:\n                cc = self.townhalls[0]\n                depot_position = await self.find_placement(UnitTypeId.SUPPLYDEPOT, near=cc)\n\n        :param building:\n        :param near:\n        :param max_distance:\n        :param random_alternative:\n        :param placement_step:\n        :param addon_place:\"\"\"\n\n        assert isinstance(building, (AbilityId, UnitTypeId))\n        assert isinstance(near, Point2), f\"{near} is no Point2 object\"\n\n        if isinstance(building, UnitTypeId):\n            creation_ability = self.game_data.units[building.value].creation_ability\n            if creation_ability is None:\n                return None\n            building = creation_ability.id\n\n        if await self.can_place_single(building, near) and (\n            not addon_place or await self.can_place_single(AbilityId.TERRANBUILD_SUPPLYDEPOT, near.offset((2.5, -0.5)))\n        ):\n            return near\n\n        if max_distance == 0:\n            return None\n\n        for distance in range(placement_step, max_distance, placement_step):\n            possible_positions = [\n                Point2(p).offset(near).to2\n                for p in (\n                    [(dx, -distance) for dx in range(-distance, distance + 1, placement_step)]\n                    + [(dx, distance) for dx in range(-distance, distance + 1, placement_step)]\n                    + [(-distance, dy) for dy in range(-distance, distance + 1, placement_step)]\n                    + [(distance, dy) for dy in range(-distance, distance + 1, placement_step)]\n                )\n            ]\n            res = await self.client._query_building_placement_fast(building, possible_positions)\n            # Filter all positions if building can be placed\n            possible = [p for r, p in zip(res, possible_positions) if r]\n\n            if addon_place:\n                # Filter remaining positions if addon can be placed\n                res = await self.client._query_building_placement_fast(\n                    AbilityId.TERRANBUILD_SUPPLYDEPOT,\n                    [p.offset((2.5, -0.5)) for p in possible],\n                )\n                possible = [p for r, p in zip(res, possible) if r]\n\n            if not possible:\n                continue\n\n            if random_alternative:\n                return random.choice(possible)\n            return min(possible, key=lambda p: p.distance_to_point2(near))\n        return None\n\n    # TODO: improve using cache per frame\n    def already_pending_upgrade(self, upgrade_type: UpgradeId) -> float:\n        \"\"\"Check if an upgrade is being researched\n\n        Returns values are::\n\n            0 # not started\n            0 < x < 1 # researching\n            1 # completed\n\n        Example::\n\n            stim_completion_percentage = self.already_pending_upgrade(UpgradeId.STIMPACK)\n\n        :param upgrade_type:\n        \"\"\"\n        assert isinstance(upgrade_type, UpgradeId), f\"{upgrade_type} is no UpgradeId\"\n        if upgrade_type in self.state.upgrades:\n            return 1\n        research_ability = self.game_data.upgrades[upgrade_type.value].research_ability\n        if research_ability is None:\n            return 0\n        creation_ability_id = research_ability.exact_id\n        for structure in self.structures.filter(lambda unit: unit.is_ready):\n            for order in structure.orders:\n                if order.ability.exact_id == creation_ability_id:\n                    return order.progress\n        return 0\n\n    def structure_type_build_progress(self, structure_type: UnitTypeId | int) -> float:\n        \"\"\"\n        Returns the build progress of a structure type.\n\n        Return range: 0 <= x <= 1 where\n            0: no such structure exists\n            0 < x < 1: at least one structure is under construction, returns the progress of the one with the highest progress\n            1: we have at least one such structure complete\n\n        Example::\n\n            # Assuming you have one barracks building at 0.5 build progress:\n            progress = self.structure_type_build_progress(UnitTypeId.BARRACKS)\n            print(progress)\n            # This prints out 0.5\n\n            # If you want to save up money for mutalisks, you can now save up once the spire is nearly completed:\n            spire_almost_completed: bool = self.structure_type_build_progress(UnitTypeId.SPIRE) > 0.75\n\n            # If you have a Hive completed but no lair, this function returns 1.0 for the following:\n            self.structure_type_build_progress(UnitTypeId.LAIR)\n\n            # Assume you have 2 command centers in production, one has 0.5 build_progress and the other 0.2, the following returns 0.5\n            highest_progress_of_command_center: float = self.structure_type_build_progress(UnitTypeId.COMMANDCENTER)\n\n        :param structure_type:\n        \"\"\"\n        assert isinstance(structure_type, (int, UnitTypeId)), (\n            f\"Needs to be int or UnitTypeId, but was: {type(structure_type)}\"\n        )\n        if isinstance(structure_type, int):\n            structure_type_value: int = structure_type\n            structure_type = UnitTypeId(structure_type_value)\n        else:\n            structure_type_value = structure_type.value\n        assert structure_type_value, f\"structure_type can not be 0 or NOTAUNIT, but was: {structure_type_value}\"\n        equiv_values: set[int] = {structure_type_value} | {\n            s_type.value for s_type in EQUIVALENTS_FOR_TECH_PROGRESS.get(structure_type, set())\n        }\n        # SUPPLYDEPOTDROP is not in self.game_data.units, so bot_ai should not check the build progress via creation ability (worker abilities)\n        if structure_type_value not in self.game_data.units:\n            return max((s.build_progress for s in self.structures if s._proto.unit_type in equiv_values), default=0)\n        creation_ability_data = self.game_data.units[structure_type_value].creation_ability\n        if creation_ability_data is None:\n            return 0\n        creation_ability: AbilityId = creation_ability_data.exact_id\n        max_value = max(\n            [s.build_progress for s in self.structures if s._proto.unit_type in equiv_values]\n            + [self._abilities_count_and_build_progress[1].get(creation_ability, 0)],\n            default=0,\n        )\n        return max_value\n\n    def tech_requirement_progress(self, structure_type: UnitTypeId) -> float:\n        \"\"\"Returns the tech requirement progress for a specific building\n\n        Example::\n\n            # Current state: supply depot is at 50% completion\n            tech_requirement = self.tech_requirement_progress(UnitTypeId.BARRACKS)\n            print(tech_requirement) # Prints 0.5 because supply depot is half way done\n\n        Example::\n\n            # Current state: your bot has one hive, no lair\n            tech_requirement = self.tech_requirement_progress(UnitTypeId.HYDRALISKDEN)\n            print(tech_requirement) # Prints 1 because a hive exists even though only a lair is required\n\n        Example::\n\n            # Current state: One factory is flying and one is half way done\n            tech_requirement = self.tech_requirement_progress(UnitTypeId.STARPORT)\n            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\n\n        :param structure_type:\"\"\"\n        race_dict = {\n            Race.Protoss: PROTOSS_TECH_REQUIREMENT,\n            Race.Terran: TERRAN_TECH_REQUIREMENT,\n            Race.Zerg: ZERG_TECH_REQUIREMENT,\n        }\n        unit_info_id = race_dict[self.race][structure_type]\n        unit_info_id_value = unit_info_id.value\n        # The following commented out line is unreliable for ghost / thor as they return 0 which is incorrect\n        # unit_info_id_value = self.game_data.units[structure_type.value]._proto.tech_requirement\n        if not unit_info_id_value:  # Equivalent to \"if unit_info_id_value == 0:\"\n            return 1\n        progresses: list[float] = [self.structure_type_build_progress(unit_info_id_value)]\n        for equiv_structure in EQUIVALENTS_FOR_TECH_PROGRESS.get(unit_info_id, []):\n            progresses.append(self.structure_type_build_progress(equiv_structure.value))\n        return max(progresses)\n\n    def already_pending(self, unit_type: UpgradeId | UnitTypeId) -> float:\n        \"\"\"\n        Returns a number of buildings or units already in progress, or if a\n        worker is en route to build it. This also includes queued orders for\n        workers and build queues of buildings.\n\n        Example::\n\n            amount_of_scv_in_production: int = self.already_pending(UnitTypeId.SCV)\n            amount_of_CCs_in_queue_and_production: int = self.already_pending(UnitTypeId.COMMANDCENTER)\n            amount_of_lairs_morphing: int = self.already_pending(UnitTypeId.LAIR)\n\n        :param unit_type:\n        \"\"\"\n        if isinstance(unit_type, UpgradeId):\n            return self.already_pending_upgrade(unit_type)\n\n        if unit_type in CREATION_ABILITY_FIX:\n            # Hotfix for checking pending archons and other abilities\n            if unit_type == UnitTypeId.ARCHON:\n                return self._abilities_count_and_build_progress[0][AbilityId.ARCHON_WARP_TARGET] / 2\n            # Hotfix for rich geysirs\n            return self._abilities_count_and_build_progress[0][CREATION_ABILITY_FIX[unit_type]]\n\n        creation_ability = self.game_data.units[unit_type.value].creation_ability\n        if creation_ability is None:\n            logger.error(f\"Unknown creation_ability in already_pending for unit_type: {unit_type}\")\n            return 0\n        ability_id = creation_ability.exact_id\n        return self._abilities_count_and_build_progress[0][ability_id]\n\n    def worker_en_route_to_build(self, unit_type: UnitTypeId) -> float:\n        \"\"\"This function counts how many workers are on the way to start the construction a building.\n\n        :param unit_type:\"\"\"\n        creation_ability = self.game_data.units[unit_type.value].creation_ability\n        if creation_ability is None:\n            logger.error(f\"Unknown creation_ability in worker_en_route_to_build for unit_type: {unit_type}\")\n            return 0\n        ability = creation_ability.exact_id\n        return self._worker_orders[ability]\n\n    @property_cache_once_per_frame\n    def structures_without_construction_SCVs(self) -> Units:\n        \"\"\"Returns all structures that do not have an SCV constructing it.\n        Warning: this function may move to become a Units filter.\"\"\"\n        worker_targets: set[int | Point2] = set()\n        for worker in self.workers:\n            # Ignore repairing workers\n            if not worker.is_constructing_scv:\n                continue\n            for order in worker.orders:\n                # When a construction is resumed, the worker.orders[0].target is the tag of the structure, else it is a Point2\n                worker_targets.add(order.target)  # pyrefly: ignore\n        return self.structures.filter(\n            lambda structure: structure.build_progress < 1\n            # Redundant check?\n            and structure.type_id in TERRAN_STRUCTURES_REQUIRE_SCV\n            and structure.position not in worker_targets\n            and structure.tag not in worker_targets\n            and structure.tag in self._structures_previous_map\n            and self._structures_previous_map[structure.tag].build_progress == structure.build_progress\n        )\n\n    async def build(\n        self,\n        building: UnitTypeId,\n        near: Unit | Point2,\n        max_distance: int = 20,\n        build_worker: Unit | None = None,\n        random_alternative: bool = True,\n        placement_step: int = 2,\n    ) -> bool:\n        \"\"\"Not recommended as this function checks many positions if it \"can place\" on them until it found a valid\n        position. Also if the given position is not placeable, this function tries to find a nearby position to place\n        the structure. Then orders the worker to start the construction.\n\n        :param building:\n        :param near:\n        :param max_distance:\n        :param build_worker:\n        :param random_alternative:\n        :param placement_step:\"\"\"\n\n        assert isinstance(near, (Unit, Point2))\n        if not self.can_afford(building):\n            return False\n        position = None\n        gas_buildings = {UnitTypeId.EXTRACTOR, UnitTypeId.ASSIMILATOR, UnitTypeId.REFINERY}\n        if isinstance(near, Unit) and building not in gas_buildings:\n            near = near.position\n        if isinstance(near, Point2):\n            near = near.to2\n        if isinstance(near, Point2):\n            position = await self.find_placement(building, near, max_distance, random_alternative, placement_step)\n            if position is None:\n                return False\n        builder = build_worker or self.select_build_worker(near)\n        if builder is None:\n            return False\n        if building in gas_buildings:\n            assert isinstance(near, Unit)\n            builder.build_gas(near)\n            return True\n        # pyrefly: ignore\n        self.do(builder.build(building, position), subtract_cost=True, ignore_warning=True)\n        return True\n\n    def train(\n        self,\n        unit_type: UnitTypeId,\n        amount: int = 1,\n        closest_to: Point2 | None = None,\n        train_only_idle_buildings: bool = True,\n    ) -> int:\n        \"\"\"Trains a specified number of units. Trains only one if amount is not specified.\n        Warning: currently has issues with warp gate warp ins\n\n        Very generic function. Please use with caution and report any bugs!\n\n        Example Zerg::\n\n            self.train(UnitTypeId.QUEEN, 5)\n            # This should queue 5 queens in 5 different townhalls if you have enough townhalls, enough minerals and enough free supply left\n\n        Example Terran::\n\n            # Assuming you have 2 idle barracks with reactors, one barracks without addon and one with techlab\n            # It should only queue 4 marines in the 2 idle barracks with reactors\n            self.train(UnitTypeId.MARINE, 4)\n\n        Example distance to::\n\n            # If you want to train based on distance to a certain point, you can use \"closest_to\"\n            self.train(UnitTypeId.MARINE, 4, closest_to = self.game_info.map_center)\n\n\n        :param unit_type:\n        :param amount:\n        :param closest_to:\n        :param train_only_idle_buildings:\"\"\"\n        # Tech requirement not met\n        if self.tech_requirement_progress(unit_type) < 1:\n            race_dict = {\n                Race.Protoss: PROTOSS_TECH_REQUIREMENT,\n                Race.Terran: TERRAN_TECH_REQUIREMENT,\n                Race.Zerg: ZERG_TECH_REQUIREMENT,\n            }\n            unit_info_id = race_dict[self.race][unit_type]\n            logger.warning(\n                f\"{self.time_formatted} Trying to produce unit {unit_type} in self.train() but tech requirement is not met: {unit_info_id}\"\n            )\n            return 0\n\n        # Not affordable\n        if not self.can_afford(unit_type):\n            return 0\n\n        trained_amount = 0\n        # All train structure types: queen can made from hatchery, lair, hive\n        train_structure_type: set[UnitTypeId] = UNIT_TRAINED_FROM[unit_type]\n        train_structures = self.structures if self.race != Race.Zerg else self.structures | self.larva\n        requires_techlab = any(\n            TRAIN_INFO[structure_type][unit_type].get(\"requires_techlab\", False)\n            for structure_type in train_structure_type\n        )\n        is_protoss = self.race == Race.Protoss\n        is_terran = self.race == Race.Terran\n        can_have_addons = any(\n            u in train_structure_type for u in {UnitTypeId.BARRACKS, UnitTypeId.FACTORY, UnitTypeId.STARPORT}\n        )\n        # Sort structures closest to a point\n        if closest_to is not None:\n            train_structures = train_structures.sorted_by_distance_to(closest_to)\n        elif can_have_addons:\n            # This should sort the structures in ascending order: first structures with reactor, then naked, then with techlab\n            train_structures = train_structures.sorted(\n                key=lambda structure: -1 * (structure.add_on_tag in self.reactor_tags)\n                + 1 * (structure.add_on_tag in self.techlab_tags)\n            )\n\n        structure: Unit\n        for structure in train_structures:\n            # Exit early if we can't afford\n            if not self.can_afford(unit_type):\n                return trained_amount\n            if (\n                # If structure hasn't received an action/order this frame\n                structure.tag not in self.unit_tags_received_action\n                # If structure can train this unit at all\n                and structure.type_id in train_structure_type\n                # Structure has to be completed to be able to train\n                and structure.build_progress == 1\n                # If structure is protoss, it needs to be powered to train\n                and (not is_protoss or structure.is_powered or structure.type_id == UnitTypeId.NEXUS)\n                # Either parameter \"train_only_idle_buildings\" is False or structure is idle or structure has less than 2 orders and has reactor\n                and (\n                    not train_only_idle_buildings\n                    or len(structure.orders) < 1 + int(structure.add_on_tag in self.reactor_tags)\n                )\n                # If structure type_id does not accept addons, it cant require a techlab\n                # Else we have to check if building has techlab as addon\n                and (not requires_techlab or structure.add_on_tag in self.techlab_tags)\n            ):\n                # Warp in at location\n                # TODO: find fast warp in locations either random location or closest to the given parameter \"closest_to\"\n                # TODO: find out which pylons have fast warp in by checking distance to nexus and warpgates.ready\n                if structure.type_id == UnitTypeId.WARPGATE:\n                    pylons = self.structures(UnitTypeId.PYLON)\n                    location = pylons.random.position.random_on_distance(4)\n                    successfully_trained = structure.warp_in(unit_type, location)\n                else:\n                    # Normal train a unit from larva or inside a structure\n                    successfully_trained = self.do(\n                        # pyrefly: ignore\n                        structure.train(unit_type),\n                        subtract_cost=True,\n                        subtract_supply=True,\n                        ignore_warning=True,\n                    )\n                    # Check if structure has reactor: queue same unit again\n                    if (\n                        # Only terran can have reactors\n                        is_terran\n                        # Check if we have enough cost or supply for this unit type\n                        and self.can_afford(unit_type)\n                        # Structure needs to be idle in the current frame\n                        and not structure.orders\n                        # We are at least 2 away from goal\n                        and trained_amount + 1 < amount\n                        # Unit type does not require techlab\n                        and not requires_techlab\n                        # Train structure has reactor\n                        and structure.add_on_tag in self.reactor_tags\n                    ):\n                        trained_amount += 1\n                        # With one command queue=False and one queue=True, you can queue 2 marines in a reactored barracks in one frame\n                        successfully_trained = self.do(\n                            # pyrefly: ignore\n                            structure.train(unit_type, queue=True),\n                            subtract_cost=True,\n                            subtract_supply=True,\n                            ignore_warning=True,\n                        )\n\n                if successfully_trained:\n                    trained_amount += 1\n                    if trained_amount == amount:\n                        # Target unit train amount reached\n                        return trained_amount\n                else:\n                    # Some error occured and we couldn't train the unit\n                    return trained_amount\n        return trained_amount\n\n    def research(self, upgrade_type: UpgradeId) -> bool:\n        \"\"\"\n        Researches an upgrade from a structure that can research it, if it is idle and powered (protoss).\n        Returns True if the research was started.\n        Return False if the requirement was not met, or the bot did not have enough resources to start the upgrade,\n        or the building to research the upgrade was missing or not idle.\n\n        New function. Please report any bugs!\n\n        Example::\n\n            # Try to research zergling movement speed if we can afford it\n            # and if at least one pool is at build_progress == 1\n            # and we are not researching it yet\n            if self.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) == 0 and self.can_afford(UpgradeId.ZERGLINGMOVEMENTSPEED):\n                spawning_pools_ready = self.structures(UnitTypeId.SPAWNINGPOOL).ready\n                if spawning_pools_ready:\n                    self.research(UpgradeId.ZERGLINGMOVEMENTSPEED)\n\n        :param upgrade_type:\n        \"\"\"\n        assert upgrade_type in UPGRADE_RESEARCHED_FROM, (\n            f\"Could not find upgrade {upgrade_type} in 'research from'-dictionary\"\n        )\n\n        # Not affordable\n        if not self.can_afford(upgrade_type):\n            return False\n\n        research_structure_type: UnitTypeId = UPGRADE_RESEARCHED_FROM[upgrade_type]\n\n        # pyrefly: ignore\n        required_tech_building: UnitTypeId | None = RESEARCH_INFO[research_structure_type][upgrade_type].get(\n            \"required_building\", None\n        )\n\n        requirement_met = (\n            required_tech_building is None or self.structure_type_build_progress(required_tech_building) == 1\n        )\n        if not requirement_met:\n            return False\n\n        is_protoss = self.race == Race.Protoss\n\n        # All upgrades right now that can be researched in spire and hatch can also be researched in their morphs\n        equiv_structures = {\n            UnitTypeId.SPIRE: {UnitTypeId.SPIRE, UnitTypeId.GREATERSPIRE},\n            UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE, UnitTypeId.GREATERSPIRE},\n            UnitTypeId.HATCHERY: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE},\n            UnitTypeId.LAIR: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE},\n            UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE},\n        }\n        # Convert to a set, or equivalent structures are chosen\n        # Overlord speed upgrade can be researched from hatchery, lair or hive\n        research_structure_types: set[UnitTypeId] = equiv_structures.get(\n            research_structure_type, {research_structure_type}\n        )\n\n        structure: Unit\n        for structure in self.structures:\n            if (\n                # Structure can research this upgrade\n                structure.type_id in research_structure_types\n                # If structure hasn't received an action/order this frame\n                and structure.tag not in self.unit_tags_received_action\n                # Structure is ready / completed\n                and structure.is_ready\n                # Structure is idle\n                and structure.is_idle\n                # Structure belongs to protoss and is powered (near pylon)\n                and (not is_protoss or structure.is_powered)\n            ):\n                # Can_afford check was already done earlier in this function\n                successful_action: bool = self.do(\n                    # pyrefly: ignore\n                    structure.research(upgrade_type),\n                    subtract_cost=True,\n                    ignore_warning=True,\n                )\n                return successful_action\n        return False\n\n    async def chat_send(self, message: str, team_only: bool = False) -> None:\n        \"\"\"Send a chat message to the SC2 Client.\n\n        Example::\n\n            await self.chat_send(\"Hello, this is a message from my bot!\")\n\n        :param message:\n        :param team_only:\"\"\"\n        assert isinstance(message, str), f\"{message} is not a string\"\n        await self.client.chat_send(message, team_only)\n\n    def in_map_bounds(self, pos: Point2 | tuple[float, float] | list[float]) -> bool:\n        \"\"\"Tests if a 2 dimensional point is within the map boundaries of the pixelmaps.\n\n        :param pos:\"\"\"\n        return (\n            self.game_info.playable_area.x\n            <= pos[0]\n            < self.game_info.playable_area.x + self.game_info.playable_area.width\n            and self.game_info.playable_area.y\n            <= pos[1]\n            < self.game_info.playable_area.y + self.game_info.playable_area.height\n        )\n\n    # For the functions below, make sure you are inside the boundaries of the map size.\n    def get_terrain_height(self, pos: Point2 | Unit) -> int:\n        \"\"\"Returns terrain height at a position.\n        Caution: terrain height is different from a unit's z-coordinate.\n\n        :param pos:\"\"\"\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return self.game_info.terrain_height[pos]\n\n    def get_terrain_z_height(self, pos: Point2 | Unit) -> float:\n        \"\"\"Returns terrain z-height at a position.\n\n        :param pos:\"\"\"\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return -16 + 32 * self.game_info.terrain_height[pos] / 255\n\n    def in_placement_grid(self, pos: Point2 | Unit) -> bool:\n        \"\"\"Returns True if you can place something at a position.\n        Remember, buildings usually use 2x2, 3x3 or 5x5 of these grid points.\n        Caution: some x and y offset might be required, see ramp code in game_info.py\n\n        :param pos:\"\"\"\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return self.game_info.placement_grid[pos] == 1\n\n    def in_pathing_grid(self, pos: Point2 | Unit) -> bool:\n        \"\"\"Returns True if a ground unit can pass through a grid point.\n\n        :param pos:\"\"\"\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return self.game_info.pathing_grid[pos] == 1\n\n    def is_visible(self, pos: Point2 | Unit) -> bool:\n        \"\"\"Returns True if you have vision on a grid point.\n\n        :param pos:\"\"\"\n        # more info: https://github.com/Blizzard/s2client-proto/blob/9906df71d6909511907d8419b33acc1a3bd51ec0/s2clientprotocol/spatial.proto#L19\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return self.state.visibility[pos] == 2\n\n    def has_creep(self, pos: Point2 | Unit) -> bool:\n        \"\"\"Returns True if there is creep on the grid point.\n\n        :param pos:\"\"\"\n        assert isinstance(pos, (Point2, Unit)), \"pos is not of type Point2 or Unit\"\n        pos = pos.position.rounded\n        return self.state.creep[pos] == 1\n\n    async def on_unit_destroyed(self, unit_tag: int) -> None:\n        \"\"\"\n        Override this in your bot class.\n        Note that this function uses unit tags and not the unit objects\n        because the unit does not exist any more.\n        This will event will be called when a unit (or structure, friendly or enemy) dies.\n        For enemy units, this only works if the enemy unit was in vision on death.\n\n        :param unit_tag:\n        \"\"\"\n\n    async def on_unit_created(self, unit: Unit) -> None:\n        \"\"\"Override this in your bot class. This function is called when a unit is created.\n\n        :param unit:\"\"\"\n\n    async def on_unit_type_changed(self, unit: Unit, previous_type: UnitTypeId) -> None:\n        \"\"\"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'\n\n        This may happen when a larva morphed to an egg, siege tank sieged, a zerg unit burrowed, a hatchery morphed to lair,\n        a corruptor morphed to broodlordcocoon, etc..\n\n        Examples::\n\n            print(f\"My unit changed type: {unit} from {previous_type} to {unit.type_id}\")\n\n        :param unit:\n        :param previous_type:\n        \"\"\"\n\n    async def on_building_construction_started(self, unit: Unit) -> None:\n        \"\"\"\n        Override this in your bot class.\n        This function is called when a building construction has started.\n\n        :param unit:\n        \"\"\"\n\n    async def on_building_construction_complete(self, unit: Unit) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called when a building\n        construction is completed.\n\n        :param unit:\n        \"\"\"\n\n    async def on_upgrade_complete(self, upgrade: UpgradeId) -> None:\n        \"\"\"\n        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.\n\n        :param upgrade:\n        \"\"\"\n\n    async def on_unit_took_damage(self, unit: Unit, amount_damage_taken: float) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called when your own unit (unit or structure) took damage.\n        It will not be called if the unit died this frame.\n\n        This may be called frequently for terran structures that are burning down, or zerg buildings that are off creep,\n        or terran bio units that just used stimpack ability.\n        TODO: If there is a demand for it, then I can add a similar event for when enemy units took damage\n\n        Examples::\n\n            print(f\"My unit took damage: {unit} took {amount_damage_taken} damage\")\n\n        :param unit:\n        :param amount_damage_taken:\n        \"\"\"\n\n    async def on_enemy_unit_entered_vision(self, unit: Unit) -> None:\n        \"\"\"\n        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).\n\n        :param unit:\n        \"\"\"\n\n    async def on_enemy_unit_left_vision(self, unit_tag: int) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called when an enemy unit (unit or structure) left vision (which was visible last frame).\n        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.\n        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.\n\n        Examples::\n\n            last_known_unit = self._enemy_units_previous_map.get(unit_tag, None) or self._enemy_structures_previous_map[unit_tag]\n            print(f\"Enemy unit left vision, last known location: {last_known_unit.position}\")\n\n        :param unit_tag:\n        \"\"\"\n\n    async def on_before_start(self) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called before \"on_start\"\n        and before \"prepare_first_step\" that calculates expansion locations.\n        Not all data is available yet.\n        This function is useful in realtime=True mode to split your workers or start producing the first worker.\n        \"\"\"\n\n    async def on_start(self) -> None:\n        \"\"\"\n        Override this in your bot class.\n        At this point, game_data, game_info and the first iteration of game_state (self.state) are available.\n        \"\"\"\n\n    async def on_step(self, iteration: int):\n        \"\"\"\n        You need to implement this function!\n        Override this in your bot class.\n        This function is called on every game step (looped in realtime mode).\n\n        :param iteration:\n        \"\"\"\n        raise NotImplementedError\n\n    async def on_end(self, game_result: Result) -> None:\n        \"\"\"Override this in your bot class. This function is called at the end of a game.\n        Unsure if this function will be called on the laddermanager client as the bot process may forcefully be terminated.\n\n        :param game_result:\"\"\"\n"
  },
  {
    "path": "sc2/bot_ai_internal.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport math\nimport time\nimport warnings\nfrom abc import ABC\nfrom collections import Counter\nfrom collections.abc import Generator, Iterable\nfrom contextlib import suppress\nfrom typing import TYPE_CHECKING, Any, final\n\nimport numpy as np\nfrom loguru import logger\n\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2.cache import property_cache_once_per_frame\nfrom sc2.constants import (\n    ALL_GAS,\n    CREATION_ABILITY_FIX,\n    IS_PLACEHOLDER,\n    TERRAN_STRUCTURES_REQUIRE_SCV,\n    FakeEffectID,\n    abilityid_to_unittypeid,\n    geyser_ids,\n    mineral_ids,\n)\nfrom sc2.data import ActionResult, Race, race_townhalls\nfrom sc2.game_data import Cost, GameData\nfrom sc2.game_state import Blip, EffectData, GameState\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.pixel_map import PixelMap\nfrom sc2.position import Point2, _PointLike\nfrom sc2.unit import Unit\nfrom sc2.unit_command import UnitCommand\nfrom sc2.units import Units\n\nwith warnings.catch_warnings():\n    warnings.simplefilter(\"ignore\")\n    from scipy.spatial.distance import cdist, pdist\n\nif TYPE_CHECKING:\n    from sc2.client import Client\n    from sc2.game_info import GameInfo\n    from sc2.bot_ai import BotAI\n\n\nclass BotAIInternal(ABC):\n    \"\"\"Base class for bots.\"\"\"\n\n    def __init__(self: BotAI) -> None:\n        self._initialize_variables()\n\n    @final\n    def _initialize_variables(self: BotAI) -> None:\n        \"\"\"Called from main.py internally\"\"\"\n        self.cache: dict[str, Any] = {}\n        # Specific opponent bot ID used in sc2ai ladder games http://sc2ai.net/ and on ai arena https://aiarena.net\n        # The bot ID will stay the same each game so your bot can \"adapt\" to the opponent\n        if not hasattr(self, \"opponent_id\"):\n            # Prevent overwriting the opponent_id which is set here https://github.com/Hannessa/python-sc2-ladderbot/blob/master/__init__.py#L40\n            # otherwise set it to None\n            self.opponent_id: str | None = None\n        # Select distance calculation method, see _distances_override_functions function\n        if not hasattr(self, \"distance_calculation_method\"):\n            self.distance_calculation_method: int = 2\n        # Select if the Unit.command should return UnitCommand objects. Set this to True if your bot uses 'unit(ability, target)'\n        if not hasattr(self, \"unit_command_uses_self_do\"):\n            self.unit_command_uses_self_do: bool = False\n        # 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)\n        self.realtime: bool = False\n        self.base_build: int = -1\n        self.all_units: Units = Units([], self)\n        self.units: Units = Units([], self)\n        self.workers: Units = Units([], self)\n        self.larva: Units = Units([], self)\n        self.structures: Units = Units([], self)\n        self.townhalls: Units = Units([], self)\n        self.gas_buildings: Units = Units([], self)\n        self.all_own_units: Units = Units([], self)\n        self.enemy_units: Units = Units([], self)\n        self.enemy_structures: Units = Units([], self)\n        self.all_enemy_units: Units = Units([], self)\n        self.resources: Units = Units([], self)\n        self.destructables: Units = Units([], self)\n        self.watchtowers: Units = Units([], self)\n        self.mineral_field: Units = Units([], self)\n        self.vespene_geyser: Units = Units([], self)\n        self.placeholders: Units = Units([], self)\n        self.techlab_tags: set[int] = set()\n        self.reactor_tags: set[int] = set()\n        self.minerals: int = 50\n        self.vespene: int = 0\n        self.supply_army: float = 0\n        self.supply_workers: float = 12  # Doesn't include workers in production\n        self.supply_cap: float = 15\n        self.supply_used: float = 12\n        self.supply_left: float = 3\n        self.idle_worker_count: int = 0\n        self.army_count: int = 0\n        self.warp_gate_count: int = 0\n        self.actions: list[UnitCommand] = []\n        self.blips: set[Blip] = set()\n\n        # Will be set on AbstractPlayer init\n        self.race: Race = None  # pyrefly: ignore\n        self.enemy_race: Race | None = None\n        self._generated_frame = -100\n        self._units_created: Counter = Counter()\n        self._unit_tags_seen_this_game: set[int] = set()\n        self._units_previous_map: dict[int, Unit] = {}\n        self._structures_previous_map: dict[int, Unit] = {}\n        self._enemy_units_previous_map: dict[int, Unit] = {}\n        self._enemy_structures_previous_map: dict[int, Unit] = {}\n        self._all_units_previous_map: dict[int, Unit] = {}\n        self._previous_upgrades: set[UpgradeId] = set()\n        self._expansion_positions_list: list[Point2] = []\n        self._resource_location_to_expansion_position_dict: dict[Point2, set[Point2]] = {}\n        self._time_before_step: float = 0\n        self._time_after_step: float = 0\n        self._min_step_time: float = math.inf\n        self._max_step_time: float = 0\n        self._last_step_step_time: float = 0\n        self._total_time_in_on_step: float = 0\n        self._total_steps_iterations: int = 0\n        # 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\n        self.unit_tags_received_action: set[int] = set()\n\n    @final\n    @property\n    def _game_info(self) -> GameInfo:\n        \"\"\"See game_info.py\"\"\"\n        warnings.warn(\n            \"Using self._game_info is deprecated and may be removed soon. Please use self.game_info directly.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        return self.game_info\n\n    @final\n    @property\n    def _game_data(self) -> GameData:\n        \"\"\"See game_data.py\"\"\"\n        warnings.warn(\n            \"Using self._game_data is deprecated and may be removed soon. Please use self.game_data directly.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        return self.game_data\n\n    @final\n    @property\n    def _client(self) -> Client:\n        \"\"\"See client.py\"\"\"\n        warnings.warn(\n            \"Using self._client is deprecated and may be removed soon. Please use self.client directly.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        return self.client\n\n    @final\n    @property_cache_once_per_frame\n    def expansion_locations(self: BotAI) -> dict[Point2, Units]:\n        \"\"\"Same as the function above.\"\"\"\n        assert self._expansion_positions_list, (\n            \"self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless.\"\n        )\n        warnings.warn(\n            \"You are using 'self.expansion_locations', please use 'self.expansion_locations_list' (fast) or 'self.expansion_locations_dict' (slow) instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        return self.expansion_locations_dict\n\n    def _cluster_center(self, group: list[Unit]) -> Point2:\n        \"\"\"\n        Calculates the geometric center (centroid) of a given group of units.\n\n        Parameters:\n        group: A list of Unit objects representing the group of units for\n                            which the center is to be calculated.\n\n        Raises:\n        ValueError: If the provided group is empty.\n\n        Returns:\n        Point2: The calculated centroid of the group as a Point2 object.\n        \"\"\"\n        if not group:\n            raise ValueError(\"Cannot calculate center of empty group\")\n\n        total_x: float = 0\n        total_y: float = 0\n        for unit in group:\n            total_x += unit.position.x\n            total_y += unit.position.y\n\n        count = len(group)\n        return Point2((total_x / count, total_y / count))\n\n    def _find_expansion_location(\n        self, resources: Units | list[Unit], amount: int, offsets: list[tuple[float, float]]\n    ) -> Point2:\n        \"\"\"\n        Finds the most suitable expansion location for resources.\n\n        Parameters:\n            resources: The list of resource entities or units near which the\n                expansion location needs to be found.\n            amount: The total number of resource entities or units to consider.\n            offsets (list[tuple[float, float]): A list of coordinate pairs denoting position\n                offsets to consider around the center of resources.\n\n        Returns:\n            The calculated optimal expansion Point2 if a suitable position is found;\n                otherwise, None.\n        \"\"\"\n        # Normal single expansion logic for regular bases\n        # Calculate center, round and add 0.5 because expansion location will have (x.5, y.5)\n        # coordinates because bases have size 5.\n        center_x = int(sum(resource.position.x for resource in resources) / amount) + 0.5\n        center_y = int(sum(resource.position.y for resource in resources) / amount) + 0.5\n        possible_points = (Point2((offset[0] + center_x, offset[1] + center_y)) for offset in offsets)\n        # Filter out points that are too near\n        possible_points = [\n            point\n            for point in possible_points\n            # Check if point can be built on\n            if self.game_info.placement_grid[point.rounded] == 1\n            # Check if all resources have enough space to point\n            and all(\n                point.distance_to(resource) >= (7 if resource._proto.unit_type in geyser_ids else 6)\n                for resource in resources\n            )\n        ]\n        # Choose best fitting point\n        result: Point2 = min(\n            possible_points, key=lambda point: sum(point.distance_to(resource_) for resource_ in resources)\n        )\n        return result\n\n    def _has_opposite_side_geyser_layout(self, minerals: list[Unit], gas_geysers: list[Unit]) -> bool:\n        \"\"\"\n        Determines whether the gas geysers have an opposite-side mineral line layout.\n\n        The method evaluates if two gas geysers are located on opposite sides of a\n        mineral line.\n        If this returns True we consider this location has 2 valid expansion locations\n        either side of the mineral line.\n\n        Parameters:\n            minerals:\n                A list of mineral fields at this location.\n            gas_geysers : list[Unit]\n                A list of gas geysers at this location.\n\n        Returns:\n            bool\n                True if the geysers fulfill the opposite-side layout condition with\n                respect to the mineral line, otherwise False.\n        \"\"\"\n        # Need exactly 2 geysers and enough minerals for a line\n        if len(gas_geysers) != 2 or len(minerals) < 6:\n            return False\n\n        # Find the two minerals that are furthest apart\n        max_distance: float = 0.0\n        mineral_1: Unit = minerals[0]\n        mineral_2: Unit = minerals[1]\n\n        for i, m1 in enumerate(minerals):\n            for m2 in minerals[i + 1 :]:\n                distance = m1.distance_to(m2)\n                if distance > max_distance:\n                    max_distance = distance\n                    mineral_1 = m1\n                    mineral_2 = m2\n\n        # ensure line is long enough\n        if max_distance < 4:\n            return False\n\n        # Create line from the two furthest minerals\n        x1, y1 = mineral_1.position.x, mineral_1.position.y\n        x2, y2 = mineral_2.position.x, mineral_2.position.y\n\n        geyser_1, geyser_2 = gas_geysers\n\n        # Check if the mineral line is more vertical than horizontal\n        if abs(x2 - x1) < 0.1:\n            # Vertical line: use x-coordinate to determine sides\n            line_x = (x1 + x2) / 2\n\n            side_1 = geyser_1.position.x - line_x\n            side_2 = geyser_2.position.x - line_x\n\n            # Must be on opposite sides and far enough from the line\n            return side_1 * side_2 < 0 and abs(side_1) > 3 and abs(side_2) > 3\n\n        # Calculate line equation: y = mx + b\n        slope = (y2 - y1) / (x2 - x1)\n        intercept = y1 - slope * x1\n\n        # Function to determine which side of the line a point is on\n        def side_of_line(point: Point2) -> float:\n            return point.y - slope * point.x - intercept\n\n        side_1 = side_of_line(geyser_1.position)\n        side_2 = side_of_line(geyser_2.position)\n\n        # Check if geysers are on opposite sides\n        opposite_sides = side_1 * side_2 < 0\n\n        return opposite_sides\n\n    @final\n    def _find_expansion_locations(self) -> None:\n        \"\"\"Ran once at the start of the game to calculate expansion locations.\"\"\"\n        # Idea: create a group for every resource, then merge these groups if\n        # any resource in a group is closer than a threshold to any resource of another group\n\n        # Distance we group resources by\n        resource_spread_threshold: float = 10.5\n        # Create a group for every resource\n        resource_groups: list[list[Unit]] = [\n            [resource]\n            for resource in self.resources\n            if resource.name != \"MineralField450\"  # dont use low mineral count patches\n        ]\n        # Loop the merging process as long as we change something\n        merged_group = True\n        height_grid: PixelMap = self.game_info.terrain_height\n        while merged_group:\n            merged_group = False\n            # Check every combination of two groups\n            for group_a, group_b in itertools.combinations(resource_groups, 2):\n                # Check if any pair of resource of these groups is closer than threshold together\n                # And that they are on the same terrain level\n                center_a = self._cluster_center(group_a)\n                center_b = self._cluster_center(group_b)\n\n                if center_a.distance_to(center_b) <= resource_spread_threshold and all(\n                    abs(height_grid[res_a.position.rounded] - height_grid[res_b.position.rounded]) <= 10\n                    for res_a in group_a\n                    for res_b in group_b\n                ):\n                    # Remove the single groups and add the merged group\n                    resource_groups.remove(group_a)\n                    resource_groups.remove(group_b)\n                    resource_groups.append(group_a + group_b)\n                    merged_group = True\n                    break\n\n        # Distance offsets we apply to center of each resource group to find expansion position\n        offset_range: int = 7\n        offsets: list[tuple[float, float]] = [\n            (x, y)\n            for x, y in itertools.product(range(-offset_range, offset_range + 1), repeat=2)\n            if 4 < math.hypot(x, y) <= 8\n        ]\n        # Dict we want to return\n        centers = {}\n        # For every resource group:\n        for resources in resource_groups:\n            # Possible expansion points\n            amount = len(resources)\n            # this check is needed for TorchesAIE where the gold mineral wall has a\n            # unit type of `RichMineralField` so we can only filter out by amount of resources\n            if amount > 12:\n                continue\n\n            minerals = [r for r in resources if r._proto.unit_type not in geyser_ids]\n            gas_geysers = [r for r in resources if r._proto.unit_type in geyser_ids]\n\n            # Check if we have exactly 2 gas geysers positioned above/below the mineral line\n            # Needed for TorchesAIE where one gold base has 2 expansion locations\n            if self._has_opposite_side_geyser_layout(minerals, gas_geysers):\n                # Create expansion locations for each geyser + minerals\n                for geyser in gas_geysers:\n                    local_resources = minerals + [geyser]\n                    result: Point2 = self._find_expansion_location(local_resources, len(local_resources), offsets)\n                    centers[result] = local_resources\n                    # Put all expansion locations in a list\n                    self._expansion_positions_list.append(result)\n                    # Maps all resource positions to the expansion position\n                    for resource in local_resources:\n                        if resource.position in self._resource_location_to_expansion_position_dict:\n                            self._resource_location_to_expansion_position_dict[resource.position].add(result)\n                        else:\n                            self._resource_location_to_expansion_position_dict[resource.position] = {result}\n\n                continue\n\n            # Choose best fitting point\n            result: Point2 = self._find_expansion_location(resources, amount, offsets)\n            centers[result] = resources\n            # Put all expansion locations in a list\n            self._expansion_positions_list.append(result)\n            # Maps all resource positions to the expansion position\n            for resource in resources:\n                self._resource_location_to_expansion_position_dict[resource.position] = {result}\n\n    @final\n    def _correct_zerg_supply(self) -> None:\n        \"\"\"The client incorrectly rounds zerg supply down instead of up (see\n        https://github.com/Blizzard/s2client-proto/issues/123), so self.supply_used\n        and friends return the wrong value when there are an odd number of zerglings\n        and banelings. This function corrects the bad values.\"\"\"\n        # TODO: remove when Blizzard/sc2client-proto#123 gets fixed.\n        half_supply_units = {\n            UnitTypeId.ZERGLING,\n            UnitTypeId.ZERGLINGBURROWED,\n            UnitTypeId.BANELING,\n            UnitTypeId.BANELINGBURROWED,\n            UnitTypeId.BANELINGCOCOON,\n        }\n        correction = self.units(half_supply_units).amount % 2\n        self.supply_used += correction\n        self.supply_army += correction\n        self.supply_left -= correction\n\n    @final\n    @property_cache_once_per_frame\n    def _abilities_count_and_build_progress(self) -> tuple[Counter[AbilityId], dict[AbilityId, float]]:\n        \"\"\"Cache for the already_pending function, includes protoss units warping in,\n        all units in production and all structures, and all morphs\"\"\"\n        abilities_amount: Counter[AbilityId] = Counter()\n        max_build_progress: dict[AbilityId, float] = {}\n        unit: Unit\n        for unit in self.units + self.structures:\n            for order in unit.orders:\n                abilities_amount[order.ability.exact_id] += 1\n            if not unit.is_ready and (self.race != Race.Terran or not unit.is_structure):\n                # If an SCV is constructing a building, already_pending would count this structure twice\n                # (once from the SCV order, and once from \"not structure.is_ready\")\n                if unit.type_id in CREATION_ABILITY_FIX:\n                    if unit.type_id == UnitTypeId.ARCHON:\n                        # Hotfix for archons in morph state\n                        creation_ability_id = AbilityId.ARCHON_WARP_TARGET\n                        abilities_amount[creation_ability_id] += 2\n                    else:\n                        # Hotfix for rich geysirs\n                        creation_ability_id = CREATION_ABILITY_FIX[unit.type_id]\n                        abilities_amount[creation_ability_id] += 1\n                else:\n                    creation_ability = self.game_data.units[unit.type_id.value].creation_ability\n                    if creation_ability is None:\n                        continue\n                    creation_ability_id = creation_ability.exact_id\n                    abilities_amount[creation_ability_id] += 1\n                max_build_progress[creation_ability_id] = max(\n                    max_build_progress.get(creation_ability_id, 0), unit.build_progress\n                )\n\n        return abilities_amount, max_build_progress\n\n    @final\n    @property_cache_once_per_frame\n    def _worker_orders(self) -> Counter[AbilityId]:\n        \"\"\"This function is used internally, do not use! It is to store all worker abilities.\"\"\"\n        abilities_amount: Counter[AbilityId] = Counter()\n        structures_in_production: set[Point2 | int] = set()\n        for structure in self.structures:\n            if structure.type_id in TERRAN_STRUCTURES_REQUIRE_SCV:\n                structures_in_production.add(structure.position)\n                structures_in_production.add(structure.tag)\n        for worker in self.workers:\n            for order in worker.orders:\n                # Skip if the SCV is constructing (not isinstance(order.target, int))\n                # or resuming construction (isinstance(order.target, int))\n                if order.target in structures_in_production:\n                    continue\n                abilities_amount[order.ability.exact_id] += 1\n        return abilities_amount\n\n    @final\n    def do(\n        self: BotAI,\n        action: UnitCommand,\n        subtract_cost: bool = False,\n        subtract_supply: bool = False,\n        can_afford_check: bool = False,\n        ignore_warning: bool = False,\n    ) -> bool:\n        \"\"\"Adds a unit action to the 'self.actions' list which is then executed at the end of the frame.\n\n        Training a unit::\n\n            # Train an SCV from a random idle command center\n            cc = self.townhalls.idle.random_or(None)\n            # self.townhalls can be empty or there are no idle townhalls\n            if cc and self.can_afford(UnitTypeId.SCV):\n                cc.train(UnitTypeId.SCV)\n\n        Building a building::\n\n            # Building a barracks at the main ramp, requires 150 minerals and a depot\n            worker = self.workers.random_or(None)\n            barracks_placement_position = self.main_base_ramp.barracks_correct_placement\n            if worker and self.can_afford(UnitTypeId.BARRACKS):\n                worker.build(UnitTypeId.BARRACKS, barracks_placement_position)\n\n        Moving a unit::\n\n            # Move a random worker to the center of the map\n            worker = self.workers.random_or(None)\n            # worker can be None if all are dead\n            if worker:\n                worker.move(self.game_info.map_center)\n\n        :param action:\n        :param subtract_cost:\n        :param subtract_supply:\n        :param can_afford_check:\n        \"\"\"\n        if not self.unit_command_uses_self_do and isinstance(action, bool):\n            if not ignore_warning:\n                warnings.warn(\n                    \"You have used self.do(). Please consider putting 'self.unit_command_uses_self_do = True' in your bot __init__() function or removing self.do().\",\n                    DeprecationWarning,\n                    stacklevel=2,\n                )\n            return action\n\n        assert isinstance(action, UnitCommand), (\n            f\"Given unit command is not a command, but instead of type {type(action)}\"\n        )\n        if subtract_cost:\n            cost: Cost = self.game_data.calculate_ability_cost(action.ability)\n            if can_afford_check and not (self.minerals >= cost.minerals and self.vespene >= cost.vespene):\n                # Dont do action if can't afford\n                return False\n            self.minerals -= cost.minerals\n            self.vespene -= cost.vespene\n        if subtract_supply and action.ability in abilityid_to_unittypeid:\n            unit_type = abilityid_to_unittypeid[action.ability]\n            required_supply = self.calculate_supply_cost(unit_type)\n            # Overlord has -8\n            if required_supply > 0:\n                self.supply_used += required_supply\n                self.supply_left -= required_supply\n        self.actions.append(action)\n        self.unit_tags_received_action.add(action.unit.tag)\n        return True\n\n    @final\n    async def synchronous_do(self: BotAI, action: UnitCommand):\n        \"\"\"\n        Not recommended. Use self.do instead to reduce lag.\n        This function is only useful for realtime=True in the first frame of the game to instantly produce a worker\n        and split workers on the mineral patches.\n        \"\"\"\n        assert isinstance(action, UnitCommand), (\n            f\"Given unit command is not a command, but instead of type {type(action)}\"\n        )\n        if not self.can_afford(action.ability):\n            logger.warning(f\"Cannot afford action {action}\")\n            return ActionResult.Error\n        r: ActionResult = await self.client.actions(action)  # pyrefly: ignore\n        if not r:  # success\n            cost = self.game_data.calculate_ability_cost(action.ability)\n            self.minerals -= cost.minerals\n            self.vespene -= cost.vespene\n            self.unit_tags_received_action.add(action.unit.tag)\n        else:\n            logger.error(f\"Error: {r} (action: {action})\")\n        return r\n\n    @final\n    async def _do_actions(self, actions: list[UnitCommand], prevent_double: bool = True):\n        \"\"\"Used internally by main.py after each step\n\n        :param actions:\n        :param prevent_double:\"\"\"\n        if not actions:\n            return None\n        if prevent_double:\n            actions = list(filter(self.prevent_double_actions, actions))\n        result = await self.client.actions(actions)\n        return result\n\n    @final\n    @staticmethod\n    def prevent_double_actions(action: UnitCommand) -> bool:\n        \"\"\"\n        :param action:\n        \"\"\"\n        # Always add actions if queued\n        if action.queue:\n            return True\n        if action.unit.orders:\n            # action: UnitCommand\n            # current_action: UnitOrder\n            current_action = action.unit.orders[0]\n            if action.ability not in {current_action.ability.id, current_action.ability.exact_id}:\n                # Different action, return True\n                return True\n            with suppress(AttributeError):\n                if current_action.target == action.target.tag:  # pyrefly: ignore\n                    # Same action, remove action if same target unit\n                    return False\n            with suppress(AttributeError):\n                if (\n                    # pyrefly: ignore\n                    action.target.x == current_action.target.x and action.target.y == current_action.target.y\n                ):\n                    # Same action, remove action if same target position\n                    return False\n            return True\n        return True\n\n    @final\n    def _prepare_start(\n        self,\n        client: Client,\n        player_id: int,\n        game_info: GameInfo,\n        game_data: GameData,\n        realtime: bool = False,\n        base_build: int = -1,\n    ) -> None:\n        \"\"\"\n        Ran until game start to set game and player data.\n\n        :param client:\n        :param player_id:\n        :param game_info:\n        :param game_data:\n        :param realtime:\n        \"\"\"\n        self.client: Client = client\n        self.player_id: int = player_id\n        self.game_info: GameInfo = game_info\n        self.game_data: GameData = game_data\n        self.realtime: bool = realtime\n        self.base_build: int = base_build\n\n        # Get the player's race. As observer, get Race.NoRace=0\n        self.race: Race = Race(self.game_info.player_races.get(self.player_id, 0))\n        # Get the enemy's race only if we are not observer (replay) and the game has 2 players\n        if self.player_id > 0 and len(self.game_info.player_races) == 2:\n            self.enemy_race: Race = Race(self.game_info.player_races[3 - self.player_id])\n\n        self._distances_override_functions(self.distance_calculation_method)\n\n    @final\n    def _prepare_first_step(self) -> None:\n        \"\"\"First step extra preparations. Must not be called before _prepare_step.\"\"\"\n        if self.townhalls:\n            self.game_info.player_start_location = self.townhalls.first.position\n            # 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\n            self._find_expansion_locations()\n        self.game_info.map_ramps, self.game_info.vision_blockers = self.game_info._find_ramps_and_vision_blockers()\n        self._time_before_step: float = time.perf_counter()\n\n    @final\n    def _prepare_step(self: BotAI, state: GameState, proto_game_info: sc_pb.Response) -> None:\n        \"\"\"\n        :param state:\n        :param proto_game_info:\n        \"\"\"\n        # Set attributes from new state before on_step.\"\"\"\n        self.state = state  # See game_state.py\n        # update pathing grid, which unfortunately is in GameInfo instead of GameState\n        self.game_info.pathing_grid = PixelMap(proto_game_info.game_info.start_raw.pathing_grid, in_bits=True)\n        # Required for events, needs to be before self.units are initialized so the old units are stored\n        self._units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.units}\n        self._structures_previous_map: dict[int, Unit] = {structure.tag: structure for structure in self.structures}\n        self._enemy_units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.enemy_units}\n        self._enemy_structures_previous_map: dict[int, Unit] = {\n            structure.tag: structure for structure in self.enemy_structures\n        }\n        self._all_units_previous_map: dict[int, Unit] = {unit.tag: unit for unit in self.all_units}\n\n        self._prepare_units()\n        self.minerals: int = state.common.minerals\n        self.vespene: int = state.common.vespene\n        self.supply_army: int = state.common.food_army\n        self.supply_workers: int = state.common.food_workers  # Doesn't include workers in production\n        self.supply_cap: int = state.common.food_cap\n        self.supply_used: int = state.common.food_used\n        self.supply_left: int = self.supply_cap - self.supply_used\n\n        if self.race == Race.Zerg:\n            # Workaround Zerg supply rounding bug\n            self._correct_zerg_supply()\n        elif self.race == Race.Protoss:\n            self.warp_gate_count: int = state.common.warp_gate_count\n\n        self.idle_worker_count: int = state.common.idle_worker_count\n        self.army_count: int = state.common.army_count\n        self._time_before_step: float = time.perf_counter()\n\n        if self.enemy_race == Race.Random and self.all_enemy_units:\n            self.enemy_race = Race(self.all_enemy_units.first.race)\n\n    @final\n    def _prepare_units(self: BotAI) -> None:\n        # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units\n        self.blips: set[Blip] = set()\n        self.all_units: Units = Units([], self)\n        self.units: Units = Units([], self)\n        self.workers: Units = Units([], self)\n        self.larva: Units = Units([], self)\n        self.structures: Units = Units([], self)\n        self.townhalls: Units = Units([], self)\n        self.gas_buildings: Units = Units([], self)\n        self.all_own_units: Units = Units([], self)\n        self.enemy_units: Units = Units([], self)\n        self.enemy_structures: Units = Units([], self)\n        self.all_enemy_units: Units = Units([], self)\n        self.resources: Units = Units([], self)\n        self.destructables: Units = Units([], self)\n        self.watchtowers: Units = Units([], self)\n        self.mineral_field: Units = Units([], self)\n        self.vespene_geyser: Units = Units([], self)\n        self.placeholders: Units = Units([], self)\n        self.techlab_tags: set[int] = set()\n        self.reactor_tags: set[int] = set()\n\n        worker_types: set[UnitTypeId] = {UnitTypeId.DRONE, UnitTypeId.DRONEBURROWED, UnitTypeId.SCV, UnitTypeId.PROBE}\n\n        index: int = 0\n        for unit in self.state.observation_raw.units:\n            if unit.is_blip:\n                self.blips.add(Blip(unit))\n            else:\n                unit_type: int = unit.unit_type\n                # Convert these units to effects: reaper grenade, parasitic bomb dummy, forcefield\n                if unit_type in FakeEffectID:\n                    self.state.effects.add(EffectData(unit, fake=True))\n                    continue\n                unit_obj = Unit(unit, self, distance_calculation_index=index, base_build=self.base_build)\n                index += 1\n                self.all_units.append(unit_obj)\n                if unit.display_type == IS_PLACEHOLDER:\n                    self.placeholders.append(unit_obj)\n                    continue\n                alliance = unit.alliance\n                # Alliance.Neutral.value = 3\n                if alliance == 3:\n                    # XELNAGATOWER = 149\n                    if unit_type == 149:\n                        self.watchtowers.append(unit_obj)\n                    # mineral field enums\n                    elif unit_type in mineral_ids:\n                        self.mineral_field.append(unit_obj)\n                        self.resources.append(unit_obj)\n                    # geyser enums\n                    elif unit_type in geyser_ids:\n                        self.vespene_geyser.append(unit_obj)\n                        self.resources.append(unit_obj)\n                    # all destructable rocks\n                    else:\n                        self.destructables.append(unit_obj)\n                # Alliance.Self.value = 1\n                elif alliance == 1:\n                    self.all_own_units.append(unit_obj)\n                    unit_id: UnitTypeId = unit_obj.type_id\n                    if unit_obj.is_structure:\n                        self.structures.append(unit_obj)\n                        if unit_id in race_townhalls[self.race]:\n                            self.townhalls.append(unit_obj)\n                        elif unit_id in ALL_GAS or unit_obj.vespene_contents:\n                            # TODO: remove \"or unit_obj.vespene_contents\" when a new linux client newer than version 4.10.0 is released\n                            self.gas_buildings.append(unit_obj)\n                        elif unit_id in {\n                            UnitTypeId.TECHLAB,\n                            UnitTypeId.BARRACKSTECHLAB,\n                            UnitTypeId.FACTORYTECHLAB,\n                            UnitTypeId.STARPORTTECHLAB,\n                        }:\n                            self.techlab_tags.add(unit_obj.tag)\n                        elif unit_id in {\n                            UnitTypeId.REACTOR,\n                            UnitTypeId.BARRACKSREACTOR,\n                            UnitTypeId.FACTORYREACTOR,\n                            UnitTypeId.STARPORTREACTOR,\n                        }:\n                            self.reactor_tags.add(unit_obj.tag)\n                    else:\n                        self.units.append(unit_obj)\n                        if unit_id in worker_types:\n                            self.workers.append(unit_obj)\n                        elif unit_id == UnitTypeId.LARVA:\n                            self.larva.append(unit_obj)\n                # Alliance.Enemy.value = 4\n                elif alliance == 4:\n                    self.all_enemy_units.append(unit_obj)\n                    if unit_obj.is_structure:\n                        self.enemy_structures.append(unit_obj)\n                    else:\n                        self.enemy_units.append(unit_obj)\n\n        # Force distance calculation and caching on all units using scipy pdist or cdist\n        if self.distance_calculation_method == 1:\n            _ = self._pdist\n        elif self.distance_calculation_method in {2, 3}:\n            _ = self._cdist\n\n    @final\n    async def _after_step(self) -> int:\n        \"\"\"Executed by main.py after each on_step function.\"\"\"\n        # Keep track of the bot on_step duration\n        self._time_after_step: float = time.perf_counter()\n        step_duration = self._time_after_step - self._time_before_step\n        self._min_step_time = min(step_duration, self._min_step_time)\n        self._max_step_time = max(step_duration, self._max_step_time)\n        self._last_step_step_time = step_duration\n        self._total_time_in_on_step += step_duration\n        self._total_steps_iterations += 1\n        # Commit and clear bot actions\n        if self.actions:\n            await self._do_actions(self.actions)\n            self.actions.clear()\n        # Clear set of unit tags that were given an order this frame\n        self.unit_tags_received_action.clear()\n        # Commit debug queries\n        await self.client._send_debug()\n\n        return self.state.game_loop\n\n    @final\n    async def _advance_steps(self: BotAI, steps: int) -> None:\n        \"\"\"Advances the game loop by amount of 'steps'. This function is meant to be used as a debugging and testing tool only.\n        If you are using this, please be aware of the consequences, e.g. 'self.units' will be filled with completely new data.\"\"\"\n        await self._after_step()\n        # Advance simulation by exactly \"steps\" frames\n        await self.client.step(steps)\n        state = await self.client.observation()\n        gs = GameState(state.observation)\n        proto_game_info = await self.client._execute(game_info=sc_pb.RequestGameInfo())\n        self._prepare_step(gs, proto_game_info)\n        await self.issue_events()\n\n    @final\n    async def issue_events(self: BotAI) -> None:\n        \"\"\"This function will be automatically run from main.py and triggers the following functions:\n        - on_unit_created\n        - on_unit_destroyed\n        - on_building_construction_started\n        - on_building_construction_complete\n        - on_upgrade_complete\n        \"\"\"\n        await self._issue_unit_dead_events()\n        await self._issue_unit_added_events()\n        await self._issue_building_events()\n        await self._issue_upgrade_events()\n        await self._issue_vision_events()\n\n    @final\n    async def _issue_unit_added_events(self: BotAI) -> None:\n        for unit in self.units:\n            if unit.tag not in self._units_previous_map and unit.tag not in self._unit_tags_seen_this_game:\n                self._unit_tags_seen_this_game.add(unit.tag)\n                self._units_created[unit.type_id] += 1\n                await self.on_unit_created(unit)\n            elif unit.tag in self._units_previous_map:\n                previous_frame_unit: Unit = self._units_previous_map[unit.tag]\n                # Check if a unit took damage this frame and then trigger event\n                if unit.health < previous_frame_unit.health or unit.shield < previous_frame_unit.shield:\n                    damage_amount = previous_frame_unit.health - unit.health + previous_frame_unit.shield - unit.shield\n                    await self.on_unit_took_damage(unit, damage_amount)\n                # Check if a unit type has changed\n                if previous_frame_unit.type_id != unit.type_id:\n                    await self.on_unit_type_changed(unit, previous_frame_unit.type_id)\n\n    @final\n    async def _issue_upgrade_events(self: BotAI) -> None:\n        difference = self.state.upgrades - self._previous_upgrades\n        for upgrade_completed in difference:\n            await self.on_upgrade_complete(upgrade_completed)\n        self._previous_upgrades = self.state.upgrades\n\n    @final\n    async def _issue_building_events(self: BotAI) -> None:\n        for structure in self.structures:\n            if structure.tag not in self._structures_previous_map:\n                if structure.build_progress < 1:\n                    await self.on_building_construction_started(structure)\n                else:\n                    # Include starting townhall\n                    self._units_created[structure.type_id] += 1\n                    await self.on_building_construction_complete(structure)\n            elif structure.tag in self._structures_previous_map:\n                # Check if a structure took damage this frame and then trigger event\n                previous_frame_structure: Unit = self._structures_previous_map[structure.tag]\n                if (\n                    structure.health < previous_frame_structure.health\n                    or structure.shield < previous_frame_structure.shield\n                ):\n                    damage_amount = (\n                        previous_frame_structure.health\n                        - structure.health\n                        + previous_frame_structure.shield\n                        - structure.shield\n                    )\n                    await self.on_unit_took_damage(structure, damage_amount)\n                # Check if a structure changed its type\n                if previous_frame_structure.type_id != structure.type_id:\n                    await self.on_unit_type_changed(structure, previous_frame_structure.type_id)\n                # Check if structure completed\n                if structure.build_progress == 1 and previous_frame_structure.build_progress < 1:\n                    self._units_created[structure.type_id] += 1\n                    await self.on_building_construction_complete(structure)\n\n    @final\n    async def _issue_vision_events(self: BotAI) -> None:\n        # Call events for enemy unit entered vision\n        for enemy_unit in self.enemy_units:\n            if enemy_unit.tag not in self._enemy_units_previous_map:\n                await self.on_enemy_unit_entered_vision(enemy_unit)\n        for enemy_structure in self.enemy_structures:\n            if enemy_structure.tag not in self._enemy_structures_previous_map:\n                await self.on_enemy_unit_entered_vision(enemy_structure)\n\n        # Call events for enemy unit left vision\n        enemy_units_left_vision: set[int] = set(self._enemy_units_previous_map) - self.enemy_units.tags\n        for enemy_unit_tag in enemy_units_left_vision:\n            await self.on_enemy_unit_left_vision(enemy_unit_tag)\n        enemy_structures_left_vision: set[int] = set(self._enemy_structures_previous_map) - self.enemy_structures.tags\n        for enemy_structure_tag in enemy_structures_left_vision:\n            await self.on_enemy_unit_left_vision(enemy_structure_tag)\n\n    @final\n    async def _issue_unit_dead_events(self: BotAI) -> None:\n        for unit_tag in self.state.dead_units & set(self._all_units_previous_map):\n            await self.on_unit_destroyed(unit_tag)\n\n    # DISTANCE CALCULATION\n\n    @final\n    @property\n    def _units_count(self) -> int:\n        return len(self.all_units)\n\n    @final\n    @property\n    def _pdist(self) -> np.ndarray:\n        \"\"\"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.\"\"\"\n        if self._generated_frame != self.state.game_loop:\n            return self.calculate_distances()\n        return self._cached_pdist\n\n    @final\n    @property\n    def _cdist(self) -> np.ndarray:\n        \"\"\"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.\"\"\"\n        if self._generated_frame != self.state.game_loop:\n            return self.calculate_distances()\n        return self._cached_cdist\n\n    @final\n    def _calculate_distances_method1(self) -> np.ndarray:\n        self._generated_frame = self.state.game_loop\n        # Converts tuple [(1, 2), (3, 4)] to flat list like [1, 2, 3, 4]\n        flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple)\n        # Converts to numpy array, then converts the flat array back to shape (n, 2): [[1, 2], [3, 4]]\n        positions_array: np.ndarray = np.fromiter(\n            flat_positions,\n            dtype=float,\n            count=2 * self._units_count,\n        ).reshape((self._units_count, 2))\n        assert len(positions_array) == self._units_count\n        # See performance benchmarks\n        self._cached_pdist = pdist(positions_array, \"sqeuclidean\")\n\n        return self._cached_pdist\n\n    @final\n    def _calculate_distances_method2(self) -> np.ndarray:\n        self._generated_frame = self.state.game_loop\n        # Converts tuple [(1, 2), (3, 4)] to flat list like [1, 2, 3, 4]\n        flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple)\n        # Converts to numpy array, then converts the flat array back to shape (n, 2): [[1, 2], [3, 4]]\n        positions_array: np.ndarray = np.fromiter(\n            flat_positions,\n            dtype=float,\n            count=2 * self._units_count,\n        ).reshape((self._units_count, 2))\n        assert len(positions_array) == self._units_count\n        # See performance benchmarks\n        self._cached_cdist = cdist(positions_array, positions_array, \"sqeuclidean\")\n\n        return self._cached_cdist\n\n    @final\n    def _calculate_distances_method3(self) -> np.ndarray:\n        \"\"\"Nearly same as above, but without asserts\"\"\"\n        self._generated_frame = self.state.game_loop\n        flat_positions = (coord for unit in self.all_units for coord in unit.position_tuple)\n        positions_array: np.ndarray = np.fromiter(\n            flat_positions,\n            dtype=float,\n            count=2 * self._units_count,\n        ).reshape((-1, 2))\n        # See performance benchmarks\n        self._cached_cdist = cdist(positions_array, positions_array, \"sqeuclidean\")\n\n        return self._cached_cdist\n\n    # Helper functions\n\n    @final\n    def square_to_condensed(self, i, j) -> int:\n        # Converts indices of a square matrix to condensed matrix\n        # https://stackoverflow.com/a/36867493/10882657\n        assert i != j, \"No diagonal elements in condensed matrix! Diagonal elements are zero\"\n        if i < j:\n            i, j = j, i\n        return self._units_count * j - j * (j + 1) // 2 + i - 1 - j\n\n    @final\n    @staticmethod\n    def convert_tuple_to_numpy_array(pos: tuple[float, float]) -> np.ndarray:\n        \"\"\"Converts a single position to a 2d numpy array with 1 row and 2 columns.\"\"\"\n        return np.fromiter(pos, dtype=float, count=2).reshape((1, 2))\n\n    # Fast and simple calculation functions\n\n    @final\n    @staticmethod\n    def distance_math_hypot(\n        p1: _PointLike,\n        p2: _PointLike,\n    ) -> float:\n        return math.hypot(p1[0] - p2[0], p1[1] - p2[1])\n\n    @final\n    @staticmethod\n    def distance_math_hypot_squared(\n        p1: _PointLike,\n        p2: _PointLike,\n    ) -> float:\n        return pow(p1[0] - p2[0], 2) + pow(p1[1] - p2[1], 2)\n\n    @final\n    def _distance_squared_unit_to_unit_method0(self, unit1: Unit, unit2: Unit) -> float:\n        return self.distance_math_hypot_squared(unit1.position_tuple, unit2.position_tuple)\n\n    # Distance calculation using the pre-calculated matrix above\n\n    @final\n    def _distance_squared_unit_to_unit_method1(self, unit1: Unit, unit2: Unit) -> float:\n        # 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\n        if unit1.tag == unit2.tag:\n            return 0\n        # Calculate index, needs to be after pdist has been calculated and cached\n        condensed_index = self.square_to_condensed(unit1.distance_calculation_index, unit2.distance_calculation_index)\n        assert condensed_index < len(self._cached_pdist), (\n            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}\"\n        )\n        distance = self._pdist[condensed_index]\n        return distance\n\n    @final\n    def _distance_squared_unit_to_unit_method2(self, unit1: Unit, unit2: Unit) -> float:\n        # Calculate index, needs to be after cdist has been calculated and cached\n        return self._cdist[unit1.distance_calculation_index, unit2.distance_calculation_index]\n\n    # Distance calculation using the fastest distance calculation functions\n\n    @final\n    def _distance_pos_to_pos(\n        self,\n        pos1: tuple[float, float] | Point2,\n        pos2: tuple[float, float] | Point2,\n    ) -> float:\n        return self.distance_math_hypot(pos1, pos2)\n\n    @final\n    def _distance_units_to_pos(\n        self,\n        units: Units,\n        pos: tuple[float, float] | Point2,\n    ) -> Generator[float, None, None]:\n        \"\"\"This function does not scale well, if len(units) > 100 it gets fairly slow\"\"\"\n        return (self.distance_math_hypot(u.position_tuple, pos) for u in units)\n\n    @final\n    def _distance_unit_to_points(\n        self,\n        unit: Unit,\n        points: Iterable[tuple[float, float]],\n    ) -> Generator[float, None, None]:\n        \"\"\"This function does not scale well, if len(points) > 100 it gets fairly slow\"\"\"\n        pos = unit.position_tuple\n        return (self.distance_math_hypot(p, pos) for p in points)\n\n    @final\n    def _distances_override_functions(self, method: int = 0) -> None:\n        \"\"\"Overrides the internal distance calculation functions at game start in bot_ai.py self._prepare_start() function\n        method 0: Use python's math.hypot\n        The following methods calculate the distances between all units once:\n        method 1: Use scipy's pdist condensed matrix (1d array)\n        method 2: Use scipy's cidst square matrix (2d array)\n        method 3: Use scipy's cidst square matrix (2d array) without asserts (careful: very weird error messages, but maybe slightly faster)\"\"\"\n        assert 0 <= method <= 3, f\"Selected method was: {method}\"\n        if method == 0:\n            self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method0\n        elif method == 1:\n            self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method1\n            self.calculate_distances = self._calculate_distances_method1\n        elif method == 2:\n            self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method2\n            self.calculate_distances = self._calculate_distances_method2\n        elif method == 3:\n            self._distance_squared_unit_to_unit = self._distance_squared_unit_to_unit_method2\n            self.calculate_distances = self._calculate_distances_method3\n"
  },
  {
    "path": "sc2/cache.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable, Hashable\nfrom typing import TYPE_CHECKING, Any, TypeVar\n\nif TYPE_CHECKING:\n    from sc2.bot_ai import BotAI\n\nT = TypeVar(\"T\")\n\n\nclass CacheDict(dict[Hashable, Any]):\n    def retrieve_and_set(self, key: Hashable, func: Callable[[], T]) -> T:\n        \"\"\"Either return the value at a certain key,\n        or set the return value of a function to that key, then return that value.\"\"\"\n        if key not in self:\n            self[key] = func()\n        return self[key]\n\n\nclass property_cache_once_per_frame(property):  # noqa: N801\n    \"\"\"This decorator caches the return value for one game loop,\n    then clears it if it is accessed in a different game loop.\n    Only works on properties of the bot object, because it requires\n    access to self.state.game_loop\n\n    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.\n\n    Copied and modified from https://tedboy.github.io/flask/_modules/werkzeug/utils.html#cached_property\n    #\"\"\"\n\n    def __init__(self, func: Callable[[BotAI], T], name: str | None = None) -> None:\n        self.__name__ = name or func.__name__\n        self.__frame__ = f\"__frame__{self.__name__}\"\n        # pyrefly: ignore\n        self.func = func\n\n    def __set__(self, obj: BotAI, value: T) -> None:\n        obj.cache[self.__name__] = value\n\n        obj.cache[self.__frame__] = obj.state.game_loop\n\n    # pyre-fixme[34]\n    def __get__(self, obj: BotAI, _type=None) -> T:\n        value = obj.cache.get(self.__name__, None)\n\n        bot_frame = obj.state.game_loop\n        if value is None or obj.cache[self.__frame__] < bot_frame:\n            value = self.func(obj)\n            obj.cache[self.__name__] = value\n            obj.cache[self.__frame__] = bot_frame\n        return value\n"
  },
  {
    "path": "sc2/client.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom pathlib import Path\nfrom typing import Any, Literal\n\nfrom aiohttp import ClientWebSocketResponse\nfrom loguru import logger\n\nfrom s2clientprotocol import debug_pb2 as debug_pb\nfrom s2clientprotocol import query_pb2 as query_pb\nfrom s2clientprotocol import raw_pb2 as raw_pb\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom s2clientprotocol import spatial_pb2 as spatial_pb\nfrom sc2.action import combine_actions\nfrom sc2.data import ActionResult, ChatChannel, Race, Result, Status\nfrom sc2.game_data import AbilityData, GameData\nfrom sc2.game_info import GameInfo\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.portconfig import Portconfig\nfrom sc2.position import Point2, Point3\nfrom sc2.protocol import ConnectionAlreadyClosedError, Protocol, ProtocolError\nfrom sc2.renderer import Renderer\nfrom sc2.unit import Unit\nfrom sc2.unit_command import UnitCommand\nfrom sc2.units import Units\n\n\nclass Client(Protocol):\n    def __init__(self, ws: ClientWebSocketResponse, save_replay_path: str | None = None) -> None:\n        \"\"\"\n        :param ws:\n        \"\"\"\n        super().__init__(ws)\n        # How many frames will be waited between iterations before the next one is called\n        self.game_step: int = 4\n        self.save_replay_path: str | None = save_replay_path\n        # The following will be set on join_game()\n        self._player_id: int = None  # pyrefly: ignore\n        # The following will be set on leave()\n        self._game_result: dict[int, Result] = None  # pyrefly: ignore\n        # Store a hash value of all the debug requests to prevent sending the same ones again if they haven't changed last frame\n        self._debug_hash_tuple_last_iteration: tuple[int, int, int, int] = (0, 0, 0, 0)\n        self._debug_draw_last_frame = False\n        self._debug_texts = []\n        self._debug_lines = []\n        self._debug_boxes = []\n        self._debug_spheres = []\n\n        self._renderer = None\n        self.raw_affects_selection = False\n\n    @property\n    def in_game(self) -> bool:\n        return self._status in {Status.in_game, Status.in_replay}\n\n    async def join_game(\n        self,\n        name: str | None = None,\n        race: Race | None = None,\n        observed_player_id: int | None = None,\n        portconfig: Portconfig | None = None,\n        rgb_render_config: dict[str, Any] | None = None,\n    ):\n        ifopts = sc_pb.InterfaceOptions(\n            raw=True,\n            score=True,\n            show_cloaked=True,\n            show_burrowed_shadows=True,\n            raw_affects_selection=self.raw_affects_selection,\n            raw_crop_to_playable_area=False,\n            show_placeholders=True,\n        )\n\n        if rgb_render_config:\n            assert isinstance(rgb_render_config, dict)\n            assert \"window_size\" in rgb_render_config and \"minimap_size\" in rgb_render_config\n            window_size = rgb_render_config[\"window_size\"]\n            minimap_size = rgb_render_config[\"minimap_size\"]\n            self._renderer = Renderer(self, window_size, minimap_size)\n            map_width, map_height = window_size\n            minimap_width, minimap_height = minimap_size\n\n            ifopts.render.resolution.x = map_width\n            ifopts.render.resolution.y = map_height\n            ifopts.render.minimap_resolution.x = minimap_width\n            ifopts.render.minimap_resolution.y = minimap_height\n\n        if race is None:\n            assert isinstance(observed_player_id, int), f\"observed_player_id is of type {type(observed_player_id)}\"\n            # join as observer\n            request = sc_pb.RequestJoinGame(observed_player_id=observed_player_id, options=ifopts)\n        else:\n            assert isinstance(race, Race)\n            request = sc_pb.RequestJoinGame(race=race.value, options=ifopts)  # pyrefly: ignore[bad-argument-type]\n\n        if portconfig:\n            request.server_ports.game_port = portconfig.server[0]\n            request.server_ports.base_port = portconfig.server[1]\n\n            for ppc in portconfig.players:\n                p = request.client_ports.add()  # pyrefly: ignore\n                p.game_port = ppc[0]\n                p.base_port = ppc[1]\n\n        if name is not None:\n            assert isinstance(name, str), f\"name is of type {type(name)}\"\n            request.player_name = name\n\n        result = await self._execute(join_game=request)\n        self._game_result = None  # pyrefly: ignore\n        self._player_id = result.join_game.player_id\n        return result.join_game.player_id\n\n    async def leave(self) -> None:\n        \"\"\"You can use 'await self.client.leave()' to surrender midst game.\"\"\"\n        is_resign = self._game_result is None\n\n        if is_resign:\n            # For all clients that can leave, result of leaving the game either\n            # loss, or the client will ignore the result\n            self._game_result = {self._player_id: Result.Defeat}\n\n        try:\n            if self.save_replay_path is not None:\n                await self.save_replay(self.save_replay_path)\n                self.save_replay_path = None\n            await self._execute(leave_game=sc_pb.RequestLeaveGame())\n        except (ProtocolError, ConnectionAlreadyClosedError):\n            if is_resign:\n                raise\n\n    async def save_replay(self, path: str) -> None:\n        logger.debug(\"Requesting replay from server\")\n        result = await self._execute(save_replay=sc_pb.RequestSaveReplay())\n        with Path(path).open(\"wb\") as f:\n            f.write(result.save_replay.data)\n        logger.info(f\"Saved replay to {path}\")\n\n    async def observation(self, game_loop: int | None = None):\n        if game_loop is not None:\n            result = await self._execute(observation=sc_pb.RequestObservation(game_loop=game_loop))\n        else:\n            result = await self._execute(observation=sc_pb.RequestObservation())\n        assert result.HasField(\"observation\")\n\n        if not self.in_game or result.observation.player_result:\n            # Sometimes game ends one step before results are available\n            if not result.observation.player_result:\n                result = await self._execute(observation=sc_pb.RequestObservation())\n                assert result.observation.player_result\n\n            player_id_to_result = dict[int, Result]()\n            for pr in result.observation.player_result:\n                player_id_to_result[pr.player_id] = Result(pr.result)\n            self._game_result = player_id_to_result\n\n        # if render_data is available, then RGB rendering was requested\n        if self._renderer and result.observation.observation.HasField(\"render_data\"):\n            await self._renderer.render(result.observation)\n\n        return result\n\n    async def step(self, step_size: int | None = None):\n        \"\"\"EXPERIMENTAL: Change self._client.game_step during the step function to increase or decrease steps per second\"\"\"\n        step_size = step_size or self.game_step\n        return await self._execute(step=sc_pb.RequestStep(count=step_size))\n\n    async def get_game_data(self) -> GameData:\n        result: sc_pb.Response = await self._execute(\n            data=sc_pb.RequestData(ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True)\n        )\n        return GameData(result.data)\n\n    async def dump_data(\n        self,\n        ability_id: bool = True,\n        unit_type_id: bool = True,\n        upgrade_id: bool = True,\n        buff_id: bool = True,\n        effect_id: bool = True,\n    ) -> None:\n        \"\"\"\n        Dump the game data files\n        choose what data to dump in the keywords\n        this function writes to a text file\n        call it one time in on_step with:\n        await self._client.dump_data()\n        \"\"\"\n        result = await self._execute(\n            data=sc_pb.RequestData(\n                ability_id=ability_id,\n                unit_type_id=unit_type_id,\n                upgrade_id=upgrade_id,\n                buff_id=buff_id,\n                effect_id=effect_id,\n            )\n        )\n        with Path(\"data_dump.txt\").open(\"a\") as file:\n            file.write(str(result.data))\n\n    async def get_game_info(self) -> GameInfo:\n        result = await self._execute(game_info=sc_pb.RequestGameInfo())\n        return GameInfo(result.game_info)\n\n    async def actions(self, actions: list[UnitCommand], return_successes: bool = False) -> list[ActionResult]:\n        if not actions:\n            return []\n        if not isinstance(actions, list):\n            actions = [actions]\n\n        # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game']\n        try:\n            response = await self._execute(\n                action=sc_pb.RequestAction(\n                    # pyrefly: ignore\n                    actions=(sc_pb.Action(action_raw=action) for action in combine_actions(actions))\n                )\n            )\n        except ProtocolError:\n            return []\n        if return_successes:\n            return [ActionResult(result) for result in response.action.result]\n        return [\n            ActionResult(result) for result in response.action.result if ActionResult(result) != ActionResult.Success\n        ]\n\n    async def query_pathing(self, start: Unit | Point2 | Point3, end: Point2 | Point3) -> float | None:\n        \"\"\"Caution: returns \"None\" when path not found\n        Try to combine queries with the function below because the pathing query is generally slow.\n\n        :param start:\n        :param end:\"\"\"\n        assert isinstance(start, (Point2, Unit))\n        assert isinstance(end, Point2)\n        if isinstance(start, Point2):\n            path = [query_pb.RequestQueryPathing(start_pos=start.as_Point2D, end_pos=end.as_Point2D)]\n        else:\n            path = [query_pb.RequestQueryPathing(unit_tag=start.tag, end_pos=end.as_Point2D)]\n        result = await self._execute(query=query_pb.RequestQuery(pathing=path))\n        distance = float(result.query.pathing[0].distance)\n        if distance <= 0.0:\n            return None\n        return distance\n\n    async def query_pathings(self, zipped_list: list[tuple[Unit | Point2 | Point3, Point2 | Point3]]) -> list[float]:\n        \"\"\"Usage: await self.query_pathings([[unit1, target2], [unit2, target2]])\n        -> returns [distance1, distance2]\n        Caution: returns 0 when path not found\n\n        :param zipped_list:\n        \"\"\"\n        assert zipped_list, \"No entry in zipped_list\"\n        path = (\n            query_pb.RequestQueryPathing(\n                # pyrefly: ignore\n                unit_tag=p1.tag if isinstance(p1, Unit) else None,\n                # pyrefly: ignore\n                start_pos=None if isinstance(p1, Unit) else p1.as_Point2D,\n                end_pos=p2.as_Point2D,\n            )\n            for p1, p2 in zipped_list\n        )\n        # pyrefly: ignore\n        results = await self._execute(query=query_pb.RequestQuery(pathing=path))\n        return [float(d.distance) for d in results.query.pathing]\n\n    async def _query_building_placement_fast(\n        self, ability: AbilityId, positions: list[Point2 | Point3], ignore_resources: bool = True\n    ) -> list[bool]:\n        \"\"\"\n        Returns a list of booleans. Return True for positions that are valid, False otherwise.\n\n        :param ability:\n        :param positions:\n        :param ignore_resources:\n        \"\"\"\n        result = await self._execute(\n            query=query_pb.RequestQuery(\n                # pyrefly: ignore\n                placements=(\n                    query_pb.RequestQueryBuildingPlacement(ability_id=ability.value, target_pos=position.as_Point2D)\n                    for position in positions\n                ),\n                ignore_resource_requirements=ignore_resources,\n            )\n        )\n        # Success enum value is 1, see https://github.com/Blizzard/s2client-proto/blob/9906df71d6909511907d8419b33acc1a3bd51ec0/s2clientprotocol/error.proto#L7\n        return [p.result == 1 for p in result.query.placements]\n\n    async def query_building_placement(\n        self,\n        ability: AbilityData,\n        positions: list[Point2 | Point3],\n        ignore_resources: bool = True,\n        # pyre-fixme[11]\n    ) -> list[ActionResult]:\n        \"\"\"This function might be deleted in favor of the function above (_query_building_placement_fast).\n\n        :param ability:\n        :param positions:\n        :param ignore_resources:\"\"\"\n        assert isinstance(ability, AbilityData)\n        result = await self._execute(\n            query=query_pb.RequestQuery(\n                # pyrefly: ignore\n                placements=(\n                    query_pb.RequestQueryBuildingPlacement(ability_id=ability.id.value, target_pos=position.as_Point2D)\n                    for position in positions\n                ),\n                ignore_resource_requirements=ignore_resources,\n            )\n        )\n        # Unnecessary converting to ActionResult?\n        return [ActionResult(p.result) for p in result.query.placements]\n\n    async def query_available_abilities(\n        self, units: list[Unit] | Units, ignore_resource_requirements: bool = False\n    ) -> list[list[AbilityId]]:\n        \"\"\"Query abilities of multiple units\"\"\"\n        input_was_a_list = True\n        if not isinstance(units, list):\n            \"\"\" Deprecated, accepting a single unit may be removed in the future, query a list of units instead \"\"\"\n            assert isinstance(units, Unit)\n            units = [units]\n            input_was_a_list = False\n        assert units\n        result = await self._execute(\n            query=query_pb.RequestQuery(\n                # pyrefly: ignore\n                abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units),\n                ignore_resource_requirements=ignore_resource_requirements,\n            )\n        )\n        \"\"\" Fix for bots that only query a single unit, may be removed soon \"\"\"\n        if not input_was_a_list:\n            # pyrefly: ignore\n            return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities][0]\n        return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities]\n\n    async def query_available_abilities_with_tag(\n        self, units: list[Unit] | Units, ignore_resource_requirements: bool = False\n    ) -> dict[int, set[AbilityId]]:\n        \"\"\"Query abilities of multiple units\"\"\"\n        result = await self._execute(\n            query=query_pb.RequestQuery(\n                # pyrefly: ignore\n                abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units),\n                ignore_resource_requirements=ignore_resource_requirements,\n            )\n        )\n        return {b.unit_tag: {AbilityId(a.ability_id) for a in b.abilities} for b in result.query.abilities}\n\n    async def chat_send(self, message: str, team_only: bool) -> None:\n        \"\"\"Writes a message to the chat\"\"\"\n        ch = ChatChannel.Team if team_only else ChatChannel.Broadcast\n        await self._execute(\n            action=sc_pb.RequestAction(\n                actions=[\n                    sc_pb.Action(\n                        action_chat=sc_pb.ActionChat(channel=ch.value, message=message)  # type: ignore[bad-argument-type]\n                    )\n                ]\n            )\n        )\n\n    async def toggle_autocast(self, units: list[Unit] | Units, ability: AbilityId) -> None:\n        \"\"\"Toggle autocast of all specified units\n\n        :param units:\n        :param ability:\"\"\"\n        assert units\n        assert isinstance(units, list)\n        assert all(isinstance(u, Unit) for u in units)\n        assert isinstance(ability, AbilityId)\n\n        await self._execute(\n            action=sc_pb.RequestAction(\n                actions=[\n                    sc_pb.Action(\n                        action_raw=raw_pb.ActionRaw(\n                            toggle_autocast=raw_pb.ActionRawToggleAutocast(\n                                ability_id=ability.value,\n                                # pyrefly: ignore\n                                unit_tags=(u.tag for u in units),\n                            )\n                        )\n                    )\n                ]\n            )\n        )\n\n    async def debug_create_unit(\n        self, unit_spawn_commands: list[tuple[UnitTypeId, int, Point2 | Point3, Literal[1, 2]]]\n    ) -> None:\n        \"\"\"Usage example (will spawn 5 marines in the center of the map for player ID 1):\n        await self._client.debug_create_unit([[UnitTypeId.MARINE, 5, self._game_info.map_center, 1]])\n\n        :param unit_spawn_commands:\"\"\"\n        assert unit_spawn_commands, \"List is empty\"\n\n        await self._execute(\n            debug=sc_pb.RequestDebug(\n                # pyrefly: ignore\n                debug=(\n                    debug_pb.DebugCommand(\n                        create_unit=debug_pb.DebugCreateUnit(\n                            unit_type=unit_type.value,\n                            owner=owner_id,\n                            pos=position.as_Point2D,\n                            quantity=amount_of_units,\n                        )\n                    )\n                    for unit_type, amount_of_units, position, owner_id in unit_spawn_commands\n                )\n            )\n        )\n\n    async def debug_kill_unit(self, unit_tags: Unit | Units | list[int] | set[int]) -> None:\n        \"\"\"\n        :param unit_tags:\n        \"\"\"\n        if isinstance(unit_tags, Units):\n            unit_tags = unit_tags.tags\n        if isinstance(unit_tags, Unit):\n            unit_tags = [unit_tags.tag]\n        assert unit_tags\n\n        await self._execute(\n            # pyrefly: ignore\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(kill_unit=debug_pb.DebugKillUnit(tag=unit_tags))])\n        )\n\n    async def move_camera(self, position: Unit | Units | Point2 | Point3) -> None:\n        \"\"\"Moves camera to the target position\n\n        :param position:\"\"\"\n        assert isinstance(position, (Unit, Units, Point2, Point3))\n        if isinstance(position, Units):\n            position = position.center\n        if isinstance(position, Unit):\n            position = position.position\n        await self._execute(\n            action=sc_pb.RequestAction(\n                actions=[\n                    sc_pb.Action(\n                        action_raw=raw_pb.ActionRaw(\n                            camera_move=raw_pb.ActionRawCameraMove(center_world_space=position.to3.as_Point)\n                        )\n                    )\n                ]\n            )\n        )\n\n    async def obs_move_camera(self, position: Unit | Units | Point2 | Point3, distance: float = 0) -> None:\n        \"\"\"Moves observer camera to the target position. Only works when observing (e.g. watching the replay).\n\n        :param position:\"\"\"\n        assert isinstance(position, (Unit, Units, Point2, Point3))\n        if isinstance(position, Units):\n            position = position.center\n        if isinstance(position, Unit):\n            position = position.position\n        await self._execute(\n            obs_action=sc_pb.RequestObserverAction(\n                actions=[\n                    sc_pb.ObserverAction(\n                        camera_move=sc_pb.ActionObserverCameraMove(world_pos=position.as_Point2D, distance=distance)\n                    )\n                ]\n            )\n        )\n\n    async def move_camera_spatial(self, position: Point2 | Point3) -> None:\n        \"\"\"Moves camera to the target position using the spatial aciton interface\n\n        :param position:\"\"\"\n        assert isinstance(position, (Point2, Point3))\n        action = sc_pb.Action(\n            action_render=spatial_pb.ActionSpatial(\n                camera_move=spatial_pb.ActionSpatialCameraMove(center_minimap=position.as_PointI)\n            )\n        )\n        await self._execute(action=sc_pb.RequestAction(actions=[action]))\n\n    def debug_text_simple(self, text: str) -> None:\n        \"\"\"Draws a text in the top left corner of the screen (up to a max of 6 messages fit there).\"\"\"\n        self._debug_texts.append(DrawItemScreenText(text=text, color=None, start_point=Point2((0, 0)), font_size=8))\n\n    def debug_text_screen(\n        self,\n        text: str,\n        pos: Point2 | Point3 | tuple[float, float] | list[float],\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n        size: int = 8,\n    ) -> None:\n        \"\"\"\n        Draws a text on the screen (monitor / game window) with coordinates 0 <= x, y <= 1.\n\n        :param text:\n        :param pos:\n        :param color:\n        :param size:\n        \"\"\"\n        assert len(pos) >= 2\n        assert 0 <= pos[0] <= 1\n        assert 0 <= pos[1] <= 1\n        pos = Point2((pos[0], pos[1]))\n        self._debug_texts.append(DrawItemScreenText(text=text, color=color, start_point=pos, font_size=size))\n\n    def debug_text_2d(\n        self,\n        text: str,\n        pos: Point2 | Point3 | tuple[float, float] | list[float],\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n        size: int = 8,\n    ):\n        return self.debug_text_screen(text, pos, color, size)\n\n    def debug_text_world(\n        self,\n        text: str,\n        pos: Unit | Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n        size: int = 8,\n    ) -> None:\n        \"\"\"\n        Draws a text at Point3 position in the game world.\n        To grab a unit's 3d position, use unit.position3d\n        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.\n\n        :param text:\n        :param color:\n        :param size:\n        \"\"\"\n        if isinstance(pos, Unit):\n            pos = pos.position3d\n        assert isinstance(pos, Point3)\n        self._debug_texts.append(DrawItemWorldText(text=text, color=color, start_point=pos, font_size=size))\n\n    def debug_text_3d(\n        self,\n        text: str,\n        pos: Unit | Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n        size: int = 8,\n    ):\n        return self.debug_text_world(text, pos, color, size)\n\n    def debug_line_out(\n        self,\n        p0: Unit | Point3,\n        p1: Unit | Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        \"\"\"\n        Draws a line from p0 to p1.\n\n        :param p0:\n        :param p1:\n        :param color:\n        \"\"\"\n        if isinstance(p0, Unit):\n            p0 = p0.position3d\n        assert isinstance(p0, Point3)\n        if isinstance(p1, Unit):\n            p1 = p1.position3d\n        assert isinstance(p1, Point3)\n        self._debug_lines.append(DrawItemLine(color=color, start_point=p0, end_point=p1))\n\n    def debug_box_out(\n        self,\n        p_min: Unit | Point3,\n        p_max: Unit | Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        \"\"\"\n        Draws a box with p_min and p_max as corners of the box.\n\n        :param p_min:\n        :param p_max:\n        :param color:\n        \"\"\"\n        if isinstance(p_min, Unit):\n            p_min = p_min.position3d\n        assert isinstance(p_min, Point3)\n        if isinstance(p_max, Unit):\n            p_max = p_max.position3d\n        assert isinstance(p_max, Point3)\n        self._debug_boxes.append(DrawItemBox(start_point=p_min, end_point=p_max, color=color))\n\n    def debug_box2_out(\n        self,\n        pos: Unit | Point3,\n        half_vertex_length: float = 0.25,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        \"\"\"\n        Draws a box center at a position 'pos', with box side lengths (vertices) of two times 'half_vertex_length'.\n\n        :param pos:\n        :param half_vertex_length:\n        :param color:\n        \"\"\"\n        if isinstance(pos, Unit):\n            pos = pos.position3d\n        assert isinstance(pos, Point3)\n        p0 = pos + Point3((-half_vertex_length, -half_vertex_length, -half_vertex_length))\n        p1 = pos + Point3((half_vertex_length, half_vertex_length, half_vertex_length))\n        self._debug_boxes.append(DrawItemBox(start_point=p0, end_point=p1, color=color))\n\n    def debug_sphere_out(\n        self,\n        p: Unit | Point3,\n        r: float,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        \"\"\"\n        Draws a sphere at point p with radius r.\n\n        :param p:\n        :param r:\n        :param color:\n        \"\"\"\n        if isinstance(p, Unit):\n            p = p.position3d\n        assert isinstance(p, Point3)\n        self._debug_spheres.append(DrawItemSphere(start_point=p, radius=r, color=color))\n\n    async def _send_debug(self) -> None:\n        \"\"\"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.\n        Check examples/terran/ramp_wall.py for example drawing. Each draw request needs to be sent again in every single on_step iteration.\n        \"\"\"\n        debug_hash = (\n            sum(hash(item) for item in self._debug_texts),\n            sum(hash(item) for item in self._debug_lines),\n            sum(hash(item) for item in self._debug_boxes),\n            sum(hash(item) for item in self._debug_spheres),\n        )\n        if debug_hash != (0, 0, 0, 0):\n            if debug_hash != self._debug_hash_tuple_last_iteration:\n                # 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)\n                self._debug_hash_tuple_last_iteration = debug_hash\n                try:\n                    await self._execute(\n                        debug=sc_pb.RequestDebug(\n                            debug=[\n                                debug_pb.DebugCommand(\n                                    draw=debug_pb.DebugDraw(\n                                        # pyrefly: ignore\n                                        text=[text.to_proto() for text in self._debug_texts]\n                                        if self._debug_texts\n                                        else None,\n                                        # pyrefly: ignore\n                                        lines=[line.to_proto() for line in self._debug_lines]\n                                        if self._debug_lines\n                                        else None,\n                                        # pyrefly: ignore\n                                        boxes=[box.to_proto() for box in self._debug_boxes]\n                                        if self._debug_boxes\n                                        else None,\n                                        # pyrefly: ignore\n                                        spheres=[sphere.to_proto() for sphere in self._debug_spheres]\n                                        if self._debug_spheres\n                                        else None,\n                                    )\n                                )\n                            ]\n                        )\n                    )\n                except ProtocolError:\n                    return\n            self._debug_draw_last_frame = True\n            self._debug_texts.clear()\n            self._debug_lines.clear()\n            self._debug_boxes.clear()\n            self._debug_spheres.clear()\n        elif self._debug_draw_last_frame:\n            # Clear drawing if we drew last frame but nothing to draw this frame\n            self._debug_hash_tuple_last_iteration = (0, 0, 0, 0)\n            await self._execute(\n                debug=sc_pb.RequestDebug(\n                    debug=[\n                        # pyrefly: ignore\n                        debug_pb.DebugCommand(draw=debug_pb.DebugDraw(text=None, lines=None, boxes=None, spheres=None))\n                    ]\n                )\n            )\n            self._debug_draw_last_frame = False\n\n    async def debug_leave(self) -> None:\n        await self._execute(debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(end_game=debug_pb.DebugEndGame())]))\n\n    async def debug_set_unit_value(\n        self, unit_tags: Iterable[int] | Units | Unit, unit_value: int, value: float\n    ) -> None:\n        \"\"\"Sets a \"unit value\" (Energy, Life or Shields) of the given units to the given value.\n        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.\n        The following example sets the health of all your workers to 1:\n        await self.debug_set_unit_value(self.workers, 2, value=1)\"\"\"\n        if isinstance(unit_tags, Units):\n            unit_tags = unit_tags.tags\n        if isinstance(unit_tags, Unit):\n            unit_tags = [unit_tags.tag]\n        assert hasattr(unit_tags, \"__iter__\"), (\n            f\"unit_tags argument needs to be an iterable (list, dict, set, Units), given argument is {type(unit_tags).__name__}\"\n        )\n        assert 1 <= unit_value <= 3, (\n            f\"unit_value needs to be between 1 and 3 (1 for energy, 2 for life, 3 for shields), given argument is {unit_value}\"\n        )\n        assert all(tag > 0 for tag in unit_tags), f\"Unit tags have invalid value: {unit_tags}\"\n        assert isinstance(value, (int, float)), \"Value needs to be of type int or float\"\n        assert value >= 0, \"Value can't be negative\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(\n                debug=(\n                    debug_pb.DebugCommand(\n                        unit_value=debug_pb.DebugSetUnitValue(\n                            unit_value=unit_value,  # pyrefly: ignore[bad-argument-type]\n                            value=float(value),\n                            unit_tag=unit_tag,\n                        )\n                    )\n                    for unit_tag in unit_tags\n                )\n            )\n        )\n\n    async def debug_hang(self, delay_in_seconds: float) -> None:\n        \"\"\"Freezes the SC2 client. Not recommended to be used.\"\"\"\n        delay_in_ms = int(round(delay_in_seconds * 1000))\n        await self._execute(\n            debug=sc_pb.RequestDebug(\n                debug=[\n                    debug_pb.DebugCommand(\n                        test_process=debug_pb.DebugTestProcess(\n                            test=1,  # pyrefly: ignore\n                            delay_ms=delay_in_ms,\n                        )\n                    )\n                ]\n            )\n        )\n\n    async def debug_show_map(self) -> None:\n        \"\"\"Reveals the whole map for the bot. Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=1)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_control_enemy(self) -> None:\n        \"\"\"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.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=2)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_food(self) -> None:\n        \"\"\"Should disable food usage (does not seem to work?). Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=3)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_free(self) -> None:\n        \"\"\"Units, structures and upgrades are free of mineral and gas cost. Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=4)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_all_resources(self) -> None:\n        \"\"\"Gives 5000 minerals and 5000 vespene to the bot.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=5)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_god(self) -> None:\n        \"\"\"Your units and structures no longer take any damage. Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=6)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_minerals(self) -> None:\n        \"\"\"Gives 5000 minerals to the bot.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=7)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_gas(self) -> None:\n        \"\"\"Gives 5000 vespene to the bot. This does not seem to be working.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=8)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_cooldown(self) -> None:\n        \"\"\"Disables cooldowns of unit abilities for the bot. Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=9)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_tech_tree(self) -> None:\n        \"\"\"Removes all tech requirements (e.g. can build a factory without having a barracks). Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=10)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_upgrade(self) -> None:\n        \"\"\"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.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=11)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def debug_fast_build(self) -> None:\n        \"\"\"Sets the build time of units and structures and upgrades to zero. Using it a second time disables it again.\"\"\"\n        await self._execute(\n            debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(game_state=12)])  # pyrefly: ignore[bad-argument-type]\n        )\n\n    async def quick_save(self) -> None:\n        \"\"\"Saves the current game state to an in-memory bookmark.\n        See: https://github.com/Blizzard/s2client-proto/blob/eeaf5efaea2259d7b70247211dff98da0a2685a2/s2clientprotocol/sc2api.proto#L93\"\"\"\n        await self._execute(quick_save=sc_pb.RequestQuickSave())\n\n    async def quick_load(self) -> None:\n        \"\"\"Loads the game state from the previously stored in-memory bookmark.\n        Caution:\n            - The SC2 Client will crash if the game wasn't quicksaved\n            - The bot step iteration counter will not reset\n            - self.state.game_loop will be set to zero after the quickload, and self.time is dependant on it\"\"\"\n        await self._execute(quick_load=sc_pb.RequestQuickLoad())\n\n\nclass DrawItem:\n    @staticmethod\n    def to_debug_color(color: tuple[float, float, float] | list[float] | Point3 | None = None) -> debug_pb.Color:\n        \"\"\"Helper function for color conversion\"\"\"\n        if color is None:\n            return debug_pb.Color(r=255, g=255, b=255)\n        # Need to check if not of type Point3 because Point3 inherits from tuple\n        if isinstance(color, (tuple, list)) or isinstance(color, Point3) and len(color) == 3:\n            return debug_pb.Color(r=int(color[0]), g=int(color[1]), b=int(color[2]))\n        # In case color is of type Point3\n        r = getattr(color, \"r\", getattr(color, \"x\", 255))\n        g = getattr(color, \"g\", getattr(color, \"y\", 255))\n        b = getattr(color, \"b\", getattr(color, \"z\", 255))\n\n        if max(r, g, b) <= 1:\n            r *= 255\n            g *= 255\n            b *= 255\n\n        return debug_pb.Color(r=int(r), g=int(g), b=int(b))\n\n\nclass DrawItemScreenText(DrawItem):\n    def __init__(\n        self,\n        start_point: Point2,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n        text: str = \"\",\n        font_size: int = 8,\n    ) -> None:\n        self._start_point = start_point\n        self._color = color\n        self._text = text\n        self._font_size = font_size\n\n    def to_proto(self):\n        return debug_pb.DebugText(\n            color=self.to_debug_color(self._color),\n            text=self._text,\n            virtual_pos=self._start_point.to3.as_Point,\n            # pyrefly: ignore\n            world_pos=None,\n            size=self._font_size,\n        )\n\n    def __hash__(self) -> int:\n        return hash((self._start_point, self._color, self._text, self._font_size))\n\n\nclass DrawItemWorldText(DrawItem):\n    def __init__(\n        self,\n        start_point: Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None,\n        text: str = \"\",\n        font_size: int = 8,\n    ) -> None:\n        self._start_point = start_point\n        self._color = color\n        self._text = text\n        self._font_size = font_size\n\n    def to_proto(self):\n        return debug_pb.DebugText(\n            color=self.to_debug_color(self._color),\n            text=self._text,\n            # pyrefly: ignore\n            virtual_pos=None,\n            world_pos=self._start_point.as_Point,\n            size=self._font_size,\n        )\n\n    def __hash__(self) -> int:\n        return hash((self._start_point, self._text, self._font_size, self._color))\n\n\nclass DrawItemLine(DrawItem):\n    def __init__(\n        self,\n        start_point: Point3,\n        end_point: Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        self._start_point = start_point\n        self._end_point = end_point\n        self._color = color\n\n    def to_proto(self):\n        return debug_pb.DebugLine(\n            line=debug_pb.Line(p0=self._start_point.as_Point, p1=self._end_point.as_Point),\n            color=self.to_debug_color(self._color),\n        )\n\n    def __hash__(self) -> int:\n        return hash((self._start_point, self._end_point, self._color))\n\n\nclass DrawItemBox(DrawItem):\n    def __init__(\n        self,\n        start_point: Point3,\n        end_point: Point3,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        self._start_point = start_point\n        self._end_point = end_point\n        self._color = color\n\n    def to_proto(self):\n        return debug_pb.DebugBox(\n            min=self._start_point.as_Point,\n            max=self._end_point.as_Point,\n            color=self.to_debug_color(self._color),\n        )\n\n    def __hash__(self) -> int:\n        return hash((self._start_point, self._end_point, self._color))\n\n\nclass DrawItemSphere(DrawItem):\n    def __init__(\n        self,\n        start_point: Point3,\n        radius: float,\n        color: tuple[float, float, float] | list[float] | Point3 | None = None,\n    ) -> None:\n        self._start_point = start_point\n        self._radius = radius\n        self._color = color\n\n    def to_proto(self):\n        return debug_pb.DebugSphere(\n            p=self._start_point.as_Point, r=self._radius, color=self.to_debug_color(self._color)\n        )\n\n    def __hash__(self) -> int:\n        return hash((self._start_point, self._radius, self._color))\n"
  },
  {
    "path": "sc2/constants.py",
    "content": "from __future__ import annotations\n\nfrom collections import defaultdict\nfrom typing import Any\n\nfrom sc2.data import Alliance, Attribute, CloakState, DisplayType, TargetType\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\n\nmineral_ids: set[int] = {\n    UnitTypeId.RICHMINERALFIELD.value,\n    UnitTypeId.RICHMINERALFIELD750.value,\n    UnitTypeId.MINERALFIELD.value,\n    UnitTypeId.MINERALFIELD450.value,\n    UnitTypeId.MINERALFIELD750.value,\n    UnitTypeId.LABMINERALFIELD.value,\n    UnitTypeId.LABMINERALFIELD750.value,\n    UnitTypeId.PURIFIERRICHMINERALFIELD.value,\n    UnitTypeId.PURIFIERRICHMINERALFIELD750.value,\n    UnitTypeId.PURIFIERMINERALFIELD.value,\n    UnitTypeId.PURIFIERMINERALFIELD750.value,\n    UnitTypeId.BATTLESTATIONMINERALFIELD.value,\n    UnitTypeId.BATTLESTATIONMINERALFIELD750.value,\n    UnitTypeId.MINERALFIELDOPAQUE.value,\n    UnitTypeId.MINERALFIELDOPAQUE900.value,\n}\ngeyser_ids: set[int] = {\n    UnitTypeId.VESPENEGEYSER.value,\n    UnitTypeId.SPACEPLATFORMGEYSER.value,\n    UnitTypeId.RICHVESPENEGEYSER.value,\n    UnitTypeId.PROTOSSVESPENEGEYSER.value,\n    UnitTypeId.PURIFIERVESPENEGEYSER.value,\n    UnitTypeId.SHAKURASVESPENEGEYSER.value,\n}\ntransforming: dict[UnitTypeId, AbilityId] = {\n    # Terran structures\n    UnitTypeId.BARRACKS: AbilityId.LAND_BARRACKS,\n    UnitTypeId.BARRACKSFLYING: AbilityId.LAND_BARRACKS,\n    UnitTypeId.COMMANDCENTER: AbilityId.LAND_COMMANDCENTER,\n    UnitTypeId.COMMANDCENTERFLYING: AbilityId.LAND_COMMANDCENTER,\n    UnitTypeId.ORBITALCOMMAND: AbilityId.LAND_ORBITALCOMMAND,\n    UnitTypeId.ORBITALCOMMANDFLYING: AbilityId.LAND_ORBITALCOMMAND,\n    UnitTypeId.FACTORY: AbilityId.LAND_FACTORY,\n    UnitTypeId.FACTORYFLYING: AbilityId.LAND_FACTORY,\n    UnitTypeId.STARPORT: AbilityId.LAND_STARPORT,\n    UnitTypeId.STARPORTFLYING: AbilityId.LAND_STARPORT,\n    UnitTypeId.SUPPLYDEPOT: AbilityId.MORPH_SUPPLYDEPOT_RAISE,\n    UnitTypeId.SUPPLYDEPOTLOWERED: AbilityId.MORPH_SUPPLYDEPOT_LOWER,\n    # Terran units\n    UnitTypeId.HELLION: AbilityId.MORPH_HELLION,\n    UnitTypeId.HELLIONTANK: AbilityId.MORPH_HELLBAT,\n    UnitTypeId.LIBERATOR: AbilityId.MORPH_LIBERATORAAMODE,\n    UnitTypeId.LIBERATORAG: AbilityId.MORPH_LIBERATORAGMODE,\n    UnitTypeId.SIEGETANK: AbilityId.UNSIEGE_UNSIEGE,\n    UnitTypeId.SIEGETANKSIEGED: AbilityId.SIEGEMODE_SIEGEMODE,\n    UnitTypeId.THOR: AbilityId.MORPH_THOREXPLOSIVEMODE,\n    UnitTypeId.THORAP: AbilityId.MORPH_THORHIGHIMPACTMODE,\n    UnitTypeId.VIKINGASSAULT: AbilityId.MORPH_VIKINGASSAULTMODE,\n    UnitTypeId.VIKINGFIGHTER: AbilityId.MORPH_VIKINGFIGHTERMODE,\n    UnitTypeId.WIDOWMINE: AbilityId.BURROWUP,\n    UnitTypeId.WIDOWMINEBURROWED: AbilityId.BURROWDOWN,\n    # Protoss structures\n    UnitTypeId.GATEWAY: AbilityId.MORPH_GATEWAY,\n    UnitTypeId.WARPGATE: AbilityId.MORPH_WARPGATE,\n    # Protoss units\n    UnitTypeId.OBSERVER: AbilityId.MORPH_OBSERVERMODE,\n    UnitTypeId.OBSERVERSIEGEMODE: AbilityId.MORPH_SURVEILLANCEMODE,\n    UnitTypeId.WARPPRISM: AbilityId.MORPH_WARPPRISMTRANSPORTMODE,\n    UnitTypeId.WARPPRISMPHASING: AbilityId.MORPH_WARPPRISMPHASINGMODE,\n    # Zerg structures\n    UnitTypeId.SPINECRAWLER: AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT,\n    UnitTypeId.SPINECRAWLERUPROOTED: AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT,\n    UnitTypeId.SPORECRAWLER: AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT,\n    UnitTypeId.SPORECRAWLERUPROOTED: AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT,\n    # Zerg units\n    UnitTypeId.BANELING: AbilityId.BURROWUP_BANELING,\n    UnitTypeId.BANELINGBURROWED: AbilityId.BURROWDOWN_BANELING,\n    UnitTypeId.DRONE: AbilityId.BURROWUP_DRONE,\n    UnitTypeId.DRONEBURROWED: AbilityId.BURROWDOWN_DRONE,\n    UnitTypeId.HYDRALISK: AbilityId.BURROWUP_HYDRALISK,\n    UnitTypeId.HYDRALISKBURROWED: AbilityId.BURROWDOWN_HYDRALISK,\n    UnitTypeId.INFESTOR: AbilityId.BURROWUP_INFESTOR,\n    UnitTypeId.INFESTORBURROWED: AbilityId.BURROWDOWN_INFESTOR,\n    UnitTypeId.INFESTORTERRAN: AbilityId.BURROWUP_INFESTORTERRAN,\n    UnitTypeId.INFESTORTERRANBURROWED: AbilityId.BURROWDOWN_INFESTORTERRAN,\n    UnitTypeId.LURKERMP: AbilityId.BURROWUP_LURKER,\n    UnitTypeId.LURKERMPBURROWED: AbilityId.BURROWDOWN_LURKER,\n    UnitTypeId.OVERSEER: AbilityId.MORPH_OVERSEERMODE,\n    UnitTypeId.OVERSEERSIEGEMODE: AbilityId.MORPH_OVERSIGHTMODE,\n    UnitTypeId.QUEEN: AbilityId.BURROWUP_QUEEN,\n    UnitTypeId.QUEENBURROWED: AbilityId.BURROWDOWN_QUEEN,\n    UnitTypeId.ROACH: AbilityId.BURROWUP_ROACH,\n    UnitTypeId.ROACHBURROWED: AbilityId.BURROWDOWN_ROACH,\n    UnitTypeId.SWARMHOSTBURROWEDMP: AbilityId.BURROWDOWN_SWARMHOST,\n    UnitTypeId.SWARMHOSTMP: AbilityId.BURROWUP_SWARMHOST,\n    UnitTypeId.ULTRALISK: AbilityId.BURROWUP_ULTRALISK,\n    UnitTypeId.ULTRALISKBURROWED: AbilityId.BURROWDOWN_ULTRALISK,\n    UnitTypeId.ZERGLING: AbilityId.BURROWUP_ZERGLING,\n    UnitTypeId.ZERGLINGBURROWED: AbilityId.BURROWDOWN_ZERGLING,\n}\n# For now only contains units that cost supply\nabilityid_to_unittypeid: dict[AbilityId, UnitTypeId] = {\n    # Protoss\n    AbilityId.NEXUSTRAIN_PROBE: UnitTypeId.PROBE,\n    AbilityId.GATEWAYTRAIN_ZEALOT: UnitTypeId.ZEALOT,\n    AbilityId.WARPGATETRAIN_ZEALOT: UnitTypeId.ZEALOT,\n    AbilityId.TRAIN_ADEPT: UnitTypeId.ADEPT,\n    AbilityId.TRAINWARP_ADEPT: UnitTypeId.ADEPT,\n    AbilityId.GATEWAYTRAIN_STALKER: UnitTypeId.STALKER,\n    AbilityId.WARPGATETRAIN_STALKER: UnitTypeId.STALKER,\n    AbilityId.GATEWAYTRAIN_SENTRY: UnitTypeId.SENTRY,\n    AbilityId.WARPGATETRAIN_SENTRY: UnitTypeId.SENTRY,\n    AbilityId.GATEWAYTRAIN_DARKTEMPLAR: UnitTypeId.DARKTEMPLAR,\n    AbilityId.WARPGATETRAIN_DARKTEMPLAR: UnitTypeId.DARKTEMPLAR,\n    AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: UnitTypeId.HIGHTEMPLAR,\n    AbilityId.WARPGATETRAIN_HIGHTEMPLAR: UnitTypeId.HIGHTEMPLAR,\n    AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER: UnitTypeId.OBSERVER,\n    AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS: UnitTypeId.COLOSSUS,\n    AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL: UnitTypeId.IMMORTAL,\n    AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM: UnitTypeId.WARPPRISM,\n    AbilityId.STARGATETRAIN_CARRIER: UnitTypeId.CARRIER,\n    AbilityId.STARGATETRAIN_ORACLE: UnitTypeId.ORACLE,\n    AbilityId.STARGATETRAIN_PHOENIX: UnitTypeId.PHOENIX,\n    AbilityId.STARGATETRAIN_TEMPEST: UnitTypeId.TEMPEST,\n    AbilityId.STARGATETRAIN_VOIDRAY: UnitTypeId.VOIDRAY,\n    AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP: UnitTypeId.MOTHERSHIP,\n    # Terran\n    AbilityId.COMMANDCENTERTRAIN_SCV: UnitTypeId.SCV,\n    AbilityId.BARRACKSTRAIN_MARINE: UnitTypeId.MARINE,\n    AbilityId.BARRACKSTRAIN_GHOST: UnitTypeId.GHOST,\n    AbilityId.BARRACKSTRAIN_MARAUDER: UnitTypeId.MARAUDER,\n    AbilityId.BARRACKSTRAIN_REAPER: UnitTypeId.REAPER,\n    AbilityId.FACTORYTRAIN_HELLION: UnitTypeId.HELLION,\n    AbilityId.FACTORYTRAIN_SIEGETANK: UnitTypeId.SIEGETANK,\n    AbilityId.FACTORYTRAIN_THOR: UnitTypeId.THOR,\n    AbilityId.FACTORYTRAIN_WIDOWMINE: UnitTypeId.WIDOWMINE,\n    AbilityId.TRAIN_HELLBAT: UnitTypeId.HELLIONTANK,\n    AbilityId.TRAIN_CYCLONE: UnitTypeId.CYCLONE,\n    AbilityId.STARPORTTRAIN_RAVEN: UnitTypeId.RAVEN,\n    AbilityId.STARPORTTRAIN_VIKINGFIGHTER: UnitTypeId.VIKINGFIGHTER,\n    AbilityId.STARPORTTRAIN_MEDIVAC: UnitTypeId.MEDIVAC,\n    AbilityId.STARPORTTRAIN_BATTLECRUISER: UnitTypeId.BATTLECRUISER,\n    AbilityId.STARPORTTRAIN_BANSHEE: UnitTypeId.BANSHEE,\n    AbilityId.STARPORTTRAIN_LIBERATOR: UnitTypeId.LIBERATOR,\n    # Zerg\n    AbilityId.LARVATRAIN_DRONE: UnitTypeId.DRONE,\n    AbilityId.LARVATRAIN_OVERLORD: UnitTypeId.OVERLORD,\n    AbilityId.LARVATRAIN_ZERGLING: UnitTypeId.ZERGLING,\n    AbilityId.LARVATRAIN_ROACH: UnitTypeId.ROACH,\n    AbilityId.LARVATRAIN_HYDRALISK: UnitTypeId.HYDRALISK,\n    AbilityId.LARVATRAIN_MUTALISK: UnitTypeId.MUTALISK,\n    AbilityId.LARVATRAIN_CORRUPTOR: UnitTypeId.CORRUPTOR,\n    AbilityId.LARVATRAIN_ULTRALISK: UnitTypeId.ULTRALISK,\n    AbilityId.LARVATRAIN_INFESTOR: UnitTypeId.INFESTOR,\n    AbilityId.LARVATRAIN_VIPER: UnitTypeId.VIPER,\n    AbilityId.LOCUSTTRAIN_SWARMHOST: UnitTypeId.SWARMHOSTMP,\n    AbilityId.TRAINQUEEN_QUEEN: UnitTypeId.QUEEN,\n}\n\nIS_STRUCTURE: int = Attribute.Structure.value\nIS_LIGHT: int = Attribute.Light.value\nIS_ARMORED: int = Attribute.Armored.value\nIS_BIOLOGICAL: int = Attribute.Biological.value\nIS_MECHANICAL: int = Attribute.Mechanical.value\nIS_MASSIVE: int = Attribute.Massive.value\nIS_PSIONIC: int = Attribute.Psionic.value\nUNIT_BATTLECRUISER: UnitTypeId = UnitTypeId.BATTLECRUISER\nUNIT_ORACLE: UnitTypeId = UnitTypeId.ORACLE\nTARGET_GROUND: set[int] = {TargetType.Ground.value, TargetType.Any.value}\nTARGET_AIR: set[int] = {TargetType.Air.value, TargetType.Any.value}\nTARGET_BOTH: set[int] = TARGET_GROUND | TARGET_AIR\nIS_SNAPSHOT = DisplayType.Snapshot.value\nIS_VISIBLE = DisplayType.Visible.value\nIS_PLACEHOLDER = DisplayType.Placeholder.value\nIS_MINE = Alliance.Self.value\nIS_ENEMY = Alliance.Enemy.value\nIS_CLOAKED: set[int] = {CloakState.Cloaked.value, CloakState.CloakedDetected.value, CloakState.CloakedAllied.value}\nIS_REVEALED: int = CloakState.CloakedDetected.value\nCAN_BE_ATTACKED: set[int] = {CloakState.NotCloaked.value, CloakState.CloakedDetected.value}\nIS_CARRYING_MINERALS: set[BuffId] = {BuffId.CARRYMINERALFIELDMINERALS, BuffId.CARRYHIGHYIELDMINERALFIELDMINERALS}\nIS_CARRYING_VESPENE: set[BuffId] = {\n    BuffId.CARRYHARVESTABLEVESPENEGEYSERGAS,\n    BuffId.CARRYHARVESTABLEVESPENEGEYSERGASPROTOSS,\n    BuffId.CARRYHARVESTABLEVESPENEGEYSERGASZERG,\n}\nIS_CARRYING_RESOURCES: set[BuffId] = IS_CARRYING_MINERALS | IS_CARRYING_VESPENE\nIS_ATTACKING: set[AbilityId] = {\n    AbilityId.ATTACK,\n    AbilityId.ATTACK_ATTACK,\n    AbilityId.ATTACK_ATTACKTOWARDS,\n    AbilityId.ATTACK_ATTACKBARRAGE,\n    AbilityId.SCAN_MOVE,\n}\nIS_PATROLLING: AbilityId = AbilityId.PATROL_PATROL\nIS_GATHERING: AbilityId = AbilityId.HARVEST_GATHER\nIS_RETURNING: AbilityId = AbilityId.HARVEST_RETURN\nIS_COLLECTING: set[AbilityId] = {IS_GATHERING, IS_RETURNING}\nIS_CONSTRUCTING_SCV: set[AbilityId] = {\n    AbilityId.TERRANBUILD_ARMORY,\n    AbilityId.TERRANBUILD_BARRACKS,\n    AbilityId.TERRANBUILD_BUNKER,\n    AbilityId.TERRANBUILD_COMMANDCENTER,\n    AbilityId.TERRANBUILD_ENGINEERINGBAY,\n    AbilityId.TERRANBUILD_FACTORY,\n    AbilityId.TERRANBUILD_FUSIONCORE,\n    AbilityId.TERRANBUILD_GHOSTACADEMY,\n    AbilityId.TERRANBUILD_MISSILETURRET,\n    AbilityId.TERRANBUILD_REFINERY,\n    AbilityId.TERRANBUILD_SENSORTOWER,\n    AbilityId.TERRANBUILD_STARPORT,\n    AbilityId.TERRANBUILD_SUPPLYDEPOT,\n}\nIS_REPAIRING: set[AbilityId] = {AbilityId.EFFECT_REPAIR, AbilityId.EFFECT_REPAIR_MULE, AbilityId.EFFECT_REPAIR_SCV}\nIS_DETECTOR: set[UnitTypeId] = {\n    UnitTypeId.OBSERVER,\n    UnitTypeId.OBSERVERSIEGEMODE,\n    UnitTypeId.RAVEN,\n    UnitTypeId.MISSILETURRET,\n    UnitTypeId.OVERSEER,\n    UnitTypeId.OVERSEERSIEGEMODE,\n    UnitTypeId.SPORECRAWLER,\n}\nSPEED_UPGRADE_DICT: dict[UnitTypeId, UpgradeId] = {\n    # Terran\n    UnitTypeId.MEDIVAC: UpgradeId.MEDIVACRAPIDDEPLOYMENT,\n    UnitTypeId.BANSHEE: UpgradeId.BANSHEESPEED,\n    # Protoss\n    UnitTypeId.ZEALOT: UpgradeId.CHARGE,\n    UnitTypeId.OBSERVER: UpgradeId.OBSERVERGRAVITICBOOSTER,\n    UnitTypeId.WARPPRISM: UpgradeId.GRAVITICDRIVE,\n    UnitTypeId.VOIDRAY: UpgradeId.VOIDRAYSPEEDUPGRADE,\n    # Zerg\n    UnitTypeId.OVERLORD: UpgradeId.OVERLORDSPEED,\n    UnitTypeId.OVERSEER: UpgradeId.OVERLORDSPEED,\n    UnitTypeId.ZERGLING: UpgradeId.ZERGLINGMOVEMENTSPEED,\n    UnitTypeId.BANELING: UpgradeId.CENTRIFICALHOOKS,\n    UnitTypeId.ROACH: UpgradeId.GLIALRECONSTITUTION,\n    UnitTypeId.LURKERMP: UpgradeId.DIGGINGCLAWS,\n}\nSPEED_INCREASE_DICT: dict[UnitTypeId, float] = {\n    # Terran\n    UnitTypeId.MEDIVAC: 1.18,\n    UnitTypeId.BANSHEE: 1.3636,\n    # Protoss\n    UnitTypeId.ZEALOT: 1.5,\n    UnitTypeId.OBSERVER: 2,\n    UnitTypeId.WARPPRISM: 1.3,\n    UnitTypeId.VOIDRAY: 1.328,\n    # Zerg\n    UnitTypeId.OVERLORD: 2.915,\n    UnitTypeId.OVERSEER: 1.8015,\n    UnitTypeId.ZERGLING: 1.6,\n    UnitTypeId.BANELING: 1.18,\n    UnitTypeId.ROACH: 1.3333333333,\n    UnitTypeId.LURKERMP: 1.1,\n}\ntemp1 = set(SPEED_UPGRADE_DICT)\ntemp2 = set(SPEED_INCREASE_DICT)\nassert temp1 == temp2, f\"{temp1.symmetric_difference(temp2)}\"\ndel temp1\ndel temp2\nSPEED_INCREASE_ON_CREEP_DICT: dict[UnitTypeId, float] = {\n    UnitTypeId.QUEEN: 2.67,\n    UnitTypeId.ZERGLING: 1.3,\n    UnitTypeId.BANELING: 1.3,\n    UnitTypeId.ROACH: 1.3,\n    UnitTypeId.RAVAGER: 1.3,\n    UnitTypeId.HYDRALISK: 1.3,\n    UnitTypeId.LURKERMP: 1.3,\n    UnitTypeId.ULTRALISK: 1.3,\n    UnitTypeId.INFESTOR: 1.3,\n    UnitTypeId.INFESTORTERRAN: 1.3,\n    UnitTypeId.SWARMHOSTMP: 1.3,\n    UnitTypeId.LOCUSTMP: 1.4,\n    UnitTypeId.SPINECRAWLER: 2.5,\n    UnitTypeId.SPORECRAWLER: 2.5,\n}\nOFF_CREEP_SPEED_UPGRADE_DICT: dict[UnitTypeId, UpgradeId] = {\n    UnitTypeId.HYDRALISK: UpgradeId.EVOLVEMUSCULARAUGMENTS,\n    UnitTypeId.ULTRALISK: UpgradeId.ANABOLICSYNTHESIS,\n}\nOFF_CREEP_SPEED_INCREASE_DICT: dict[UnitTypeId, float] = {\n    UnitTypeId.HYDRALISK: 1.25,\n    UnitTypeId.ULTRALISK: 1.2,\n}\ntemp1 = set(OFF_CREEP_SPEED_UPGRADE_DICT)\ntemp2 = set(OFF_CREEP_SPEED_INCREASE_DICT)\nassert temp1 == temp2, f\"{temp1.symmetric_difference(temp2)}\"\ndel temp1\ndel temp2\n# Movement speed gets altered by this factor if it is affected by this buff\nSPEED_ALTERING_BUFFS: dict[BuffId, float] = {\n    # Stimpack increases speed by 1.5\n    BuffId.STIMPACK: 1.5,\n    BuffId.STIMPACKMARAUDER: 1.5,\n    BuffId.CHARGEUP: 2.2,  # x2.8 speed up in pre version 4.11\n    # Concussive shells of Marauder reduce speed by 50%\n    BuffId.DUTCHMARAUDERSLOW: 0.5,\n    # Time Warp of Mothership reduces speed by 50%\n    BuffId.TIMEWARPPRODUCTION: 0.5,\n    # Fungal Growth of Infestor reduces speed by 75%\n    BuffId.FUNGALGROWTH: 0.25,\n    # Inhibitor Zones reduce speed by 35%\n    BuffId.INHIBITORZONETEMPORALFIELD: 0.65,\n    # TODO there is a new zone coming (acceleration zone) which increase movement speed, ultralisk will be affected by this\n}\nUNIT_PHOTONCANNON: UnitTypeId = UnitTypeId.PHOTONCANNON\nUNIT_COLOSSUS: UnitTypeId = UnitTypeId.COLOSSUS\n# Used in unit_command.py and action.py to combine only certain abilities\nCOMBINEABLE_ABILITIES: set[AbilityId] = {\n    AbilityId.MOVE,\n    AbilityId.ATTACK,\n    AbilityId.SCAN_MOVE,\n    AbilityId.STOP,\n    AbilityId.HOLDPOSITION,\n    AbilityId.PATROL,\n    AbilityId.HARVEST_GATHER,\n    AbilityId.HARVEST_RETURN,\n    AbilityId.EFFECT_REPAIR,\n    AbilityId.LIFT,\n    AbilityId.BURROWDOWN,\n    AbilityId.BURROWUP,\n    AbilityId.SIEGEMODE_SIEGEMODE,\n    AbilityId.UNSIEGE_UNSIEGE,\n    AbilityId.MORPH_LIBERATORAAMODE,\n    AbilityId.EFFECT_STIM,\n    AbilityId.MORPH_UPROOT,\n    AbilityId.EFFECT_BLINK,\n    AbilityId.MORPH_ARCHON,\n}\nFakeEffectRadii: dict[int, float] = {\n    UnitTypeId.KD8CHARGE.value: 2,\n    UnitTypeId.PARASITICBOMBDUMMY.value: 3,\n    UnitTypeId.FORCEFIELD.value: 1.5,\n}\nFakeEffectID: dict[int, str] = {\n    UnitTypeId.KD8CHARGE.value: \"KD8CHARGE\",\n    UnitTypeId.PARASITICBOMBDUMMY.value: \"PARASITICBOMB\",\n    UnitTypeId.FORCEFIELD.value: \"FORCEFIELD\",\n}\n\nTERRAN_STRUCTURES_REQUIRE_SCV: set[UnitTypeId] = {\n    UnitTypeId.ARMORY,\n    UnitTypeId.BARRACKS,\n    UnitTypeId.BUNKER,\n    UnitTypeId.COMMANDCENTER,\n    UnitTypeId.ENGINEERINGBAY,\n    UnitTypeId.FACTORY,\n    UnitTypeId.FUSIONCORE,\n    UnitTypeId.GHOSTACADEMY,\n    UnitTypeId.MISSILETURRET,\n    UnitTypeId.REFINERY,\n    UnitTypeId.REFINERYRICH,\n    UnitTypeId.SENSORTOWER,\n    UnitTypeId.STARPORT,\n    UnitTypeId.SUPPLYDEPOT,\n}\n\n\ndef return_NOTAUNIT() -> UnitTypeId:\n    # NOTAUNIT = 0\n    return UnitTypeId.NOTAUNIT\n\n\n# 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\nTERRAN_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict(\n    return_NOTAUNIT,\n    {\n        UnitTypeId.MISSILETURRET: UnitTypeId.ENGINEERINGBAY,\n        UnitTypeId.SENSORTOWER: UnitTypeId.ENGINEERINGBAY,\n        UnitTypeId.PLANETARYFORTRESS: UnitTypeId.ENGINEERINGBAY,\n        UnitTypeId.BARRACKS: UnitTypeId.SUPPLYDEPOT,\n        UnitTypeId.ORBITALCOMMAND: UnitTypeId.BARRACKS,\n        UnitTypeId.BUNKER: UnitTypeId.BARRACKS,\n        UnitTypeId.GHOST: UnitTypeId.GHOSTACADEMY,\n        UnitTypeId.GHOSTACADEMY: UnitTypeId.BARRACKS,\n        UnitTypeId.FACTORY: UnitTypeId.BARRACKS,\n        UnitTypeId.ARMORY: UnitTypeId.FACTORY,\n        UnitTypeId.HELLIONTANK: UnitTypeId.ARMORY,\n        UnitTypeId.THOR: UnitTypeId.ARMORY,\n        UnitTypeId.STARPORT: UnitTypeId.FACTORY,\n        UnitTypeId.FUSIONCORE: UnitTypeId.STARPORT,\n        UnitTypeId.BATTLECRUISER: UnitTypeId.FUSIONCORE,\n    },\n)\nPROTOSS_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict(\n    return_NOTAUNIT,\n    {\n        UnitTypeId.PHOTONCANNON: UnitTypeId.FORGE,\n        UnitTypeId.CYBERNETICSCORE: UnitTypeId.GATEWAY,\n        UnitTypeId.SENTRY: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.STALKER: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.ADEPT: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.TWILIGHTCOUNCIL: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.SHIELDBATTERY: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.TEMPLARARCHIVE: UnitTypeId.TWILIGHTCOUNCIL,\n        UnitTypeId.DARKSHRINE: UnitTypeId.TWILIGHTCOUNCIL,\n        UnitTypeId.HIGHTEMPLAR: UnitTypeId.TEMPLARARCHIVE,\n        UnitTypeId.DARKTEMPLAR: UnitTypeId.DARKSHRINE,\n        UnitTypeId.STARGATE: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.TEMPEST: UnitTypeId.FLEETBEACON,\n        UnitTypeId.CARRIER: UnitTypeId.FLEETBEACON,\n        UnitTypeId.MOTHERSHIP: UnitTypeId.FLEETBEACON,\n        UnitTypeId.ROBOTICSFACILITY: UnitTypeId.CYBERNETICSCORE,\n        UnitTypeId.ROBOTICSBAY: UnitTypeId.ROBOTICSFACILITY,\n        UnitTypeId.COLOSSUS: UnitTypeId.ROBOTICSBAY,\n        UnitTypeId.DISRUPTOR: UnitTypeId.ROBOTICSBAY,\n        UnitTypeId.FLEETBEACON: UnitTypeId.STARGATE,\n    },\n)\nZERG_TECH_REQUIREMENT: dict[UnitTypeId, UnitTypeId] = defaultdict(\n    return_NOTAUNIT,\n    {\n        UnitTypeId.ZERGLING: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.QUEEN: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.ROACHWARREN: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.BANELINGNEST: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.SPINECRAWLER: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.SPORECRAWLER: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.ROACH: UnitTypeId.ROACHWARREN,\n        UnitTypeId.BANELING: UnitTypeId.BANELINGNEST,\n        UnitTypeId.LAIR: UnitTypeId.SPAWNINGPOOL,\n        UnitTypeId.OVERSEER: UnitTypeId.LAIR,\n        UnitTypeId.OVERLORDTRANSPORT: UnitTypeId.LAIR,\n        UnitTypeId.INFESTATIONPIT: UnitTypeId.LAIR,\n        UnitTypeId.INFESTOR: UnitTypeId.INFESTATIONPIT,\n        UnitTypeId.SWARMHOSTMP: UnitTypeId.INFESTATIONPIT,\n        UnitTypeId.HYDRALISKDEN: UnitTypeId.LAIR,\n        UnitTypeId.HYDRALISK: UnitTypeId.HYDRALISKDEN,\n        UnitTypeId.LURKERDENMP: UnitTypeId.HYDRALISKDEN,\n        UnitTypeId.LURKERMP: UnitTypeId.LURKERDENMP,\n        UnitTypeId.SPIRE: UnitTypeId.LAIR,\n        UnitTypeId.MUTALISK: UnitTypeId.SPIRE,\n        UnitTypeId.CORRUPTOR: UnitTypeId.SPIRE,\n        UnitTypeId.NYDUSNETWORK: UnitTypeId.LAIR,\n        UnitTypeId.HIVE: UnitTypeId.INFESTATIONPIT,\n        UnitTypeId.VIPER: UnitTypeId.HIVE,\n        UnitTypeId.ULTRALISKCAVERN: UnitTypeId.HIVE,\n        UnitTypeId.GREATERSPIRE: UnitTypeId.HIVE,\n        UnitTypeId.BROODLORD: UnitTypeId.GREATERSPIRE,\n    },\n)\n# Required in 'tech_requirement_progress' bot_ai.py function\nEQUIVALENTS_FOR_TECH_PROGRESS: dict[UnitTypeId, set[UnitTypeId]] = {\n    # Protoss\n    UnitTypeId.GATEWAY: {UnitTypeId.WARPGATE},\n    UnitTypeId.WARPPRISM: {UnitTypeId.WARPPRISMPHASING},\n    UnitTypeId.OBSERVER: {UnitTypeId.OBSERVERSIEGEMODE},\n    # Terran\n    UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SUPPLYDEPOTLOWERED, UnitTypeId.SUPPLYDEPOTDROP},\n    UnitTypeId.BARRACKS: {UnitTypeId.BARRACKSFLYING},\n    UnitTypeId.FACTORY: {UnitTypeId.FACTORYFLYING},\n    UnitTypeId.STARPORT: {UnitTypeId.STARPORTFLYING},\n    UnitTypeId.COMMANDCENTER: {\n        UnitTypeId.COMMANDCENTERFLYING,\n        UnitTypeId.PLANETARYFORTRESS,\n        UnitTypeId.ORBITALCOMMAND,\n        UnitTypeId.ORBITALCOMMANDFLYING,\n    },\n    UnitTypeId.ORBITALCOMMAND: {UnitTypeId.ORBITALCOMMANDFLYING},\n    UnitTypeId.HELLION: {UnitTypeId.HELLIONTANK},\n    UnitTypeId.WIDOWMINE: {UnitTypeId.WIDOWMINEBURROWED},\n    UnitTypeId.SIEGETANK: {UnitTypeId.SIEGETANKSIEGED},\n    UnitTypeId.THOR: {UnitTypeId.THORAP},\n    UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKINGASSAULT},\n    UnitTypeId.LIBERATOR: {UnitTypeId.LIBERATORAG},\n    # Zerg\n    UnitTypeId.LAIR: {UnitTypeId.HIVE},\n    UnitTypeId.HATCHERY: {UnitTypeId.LAIR, UnitTypeId.HIVE},\n    UnitTypeId.SPIRE: {UnitTypeId.GREATERSPIRE},\n    UnitTypeId.SPINECRAWLER: {UnitTypeId.SPINECRAWLERUPROOTED},\n    UnitTypeId.SPORECRAWLER: {UnitTypeId.SPORECRAWLERUPROOTED},\n    UnitTypeId.OVERLORD: {UnitTypeId.OVERLORDTRANSPORT},\n    UnitTypeId.OVERSEER: {UnitTypeId.OVERSEERSIEGEMODE},\n    UnitTypeId.DRONE: {UnitTypeId.DRONEBURROWED},\n    UnitTypeId.ZERGLING: {UnitTypeId.ZERGLINGBURROWED},\n    UnitTypeId.ROACH: {UnitTypeId.ROACHBURROWED},\n    UnitTypeId.RAVAGER: {UnitTypeId.RAVAGERBURROWED},\n    UnitTypeId.HYDRALISK: {UnitTypeId.HYDRALISKBURROWED},\n    UnitTypeId.LURKERMP: {UnitTypeId.LURKERMPBURROWED},\n    UnitTypeId.SWARMHOSTMP: {UnitTypeId.SWARMHOSTBURROWEDMP},\n    UnitTypeId.INFESTOR: {UnitTypeId.INFESTORBURROWED},\n    UnitTypeId.ULTRALISK: {UnitTypeId.ULTRALISKBURROWED},\n    # TODO What about morphing untis? E.g. roach to ravager, overlord to drop-overlord or overseer\n}\nALL_GAS: set[UnitTypeId] = {\n    UnitTypeId.ASSIMILATOR,\n    UnitTypeId.ASSIMILATORRICH,\n    UnitTypeId.REFINERY,\n    UnitTypeId.REFINERYRICH,\n    UnitTypeId.EXTRACTOR,\n    UnitTypeId.EXTRACTORRICH,\n}\n\nDAMAGE_BONUS_PER_UPGRADE: dict[UnitTypeId, dict[int, Any]] = {\n    #\n    # Protoss\n    #\n    UnitTypeId.PROBE: {TargetType.Ground.value: {None: 0}},\n    # Gateway Units\n    UnitTypeId.ADEPT: {TargetType.Ground.value: {IS_LIGHT: 1}},\n    UnitTypeId.STALKER: {TargetType.Any.value: {IS_ARMORED: 1}},\n    UnitTypeId.DARKTEMPLAR: {TargetType.Ground.value: {None: 5}},\n    UnitTypeId.ARCHON: {TargetType.Any.value: {None: 3, IS_BIOLOGICAL: 1}},\n    # Robo Units\n    UnitTypeId.IMMORTAL: {TargetType.Ground.value: {None: 2, IS_ARMORED: 3}},\n    UnitTypeId.COLOSSUS: {TargetType.Ground.value: {IS_LIGHT: 1}},\n    # Stargate Units\n    UnitTypeId.ORACLE: {TargetType.Ground.value: {None: 0}},\n    UnitTypeId.TEMPEST: {TargetType.Ground.value: {None: 4}, TargetType.Air.value: {None: 3, IS_MASSIVE: 2}},\n    #\n    # Terran\n    #\n    UnitTypeId.SCV: {TargetType.Ground.value: {None: 0}},\n    # Barracks Units\n    UnitTypeId.MARAUDER: {TargetType.Ground.value: {IS_ARMORED: 1}},\n    UnitTypeId.GHOST: {TargetType.Any.value: {IS_LIGHT: 1}},\n    # Factory Units\n    UnitTypeId.HELLION: {TargetType.Ground.value: {IS_LIGHT: 1}},\n    UnitTypeId.HELLIONTANK: {TargetType.Ground.value: {None: 2, IS_LIGHT: 1}},\n    UnitTypeId.CYCLONE: {TargetType.Any.value: {None: 2}},\n    UnitTypeId.SIEGETANK: {TargetType.Ground.value: {None: 2, IS_ARMORED: 1}},\n    UnitTypeId.SIEGETANKSIEGED: {TargetType.Ground.value: {None: 4, IS_ARMORED: 1}},\n    UnitTypeId.THOR: {TargetType.Ground.value: {None: 3}, TargetType.Air.value: {IS_LIGHT: 1}},\n    UnitTypeId.THORAP: {TargetType.Ground.value: {None: 3}, TargetType.Air.value: {None: 3, IS_MASSIVE: 1}},\n    # Starport Units\n    UnitTypeId.VIKINGASSAULT: {TargetType.Ground.value: {IS_MECHANICAL: 1}},\n    UnitTypeId.LIBERATORAG: {TargetType.Ground.value: {None: 5}},\n    #\n    # Zerg\n    #\n    UnitTypeId.DRONE: {TargetType.Ground.value: {None: 0}},\n    # Hatch Tech Units (Queen, Ling, Bane, Roach, Ravager)\n    UnitTypeId.BANELING: {TargetType.Ground.value: {None: 2, IS_LIGHT: 2, IS_STRUCTURE: 3}},\n    UnitTypeId.ROACH: {TargetType.Ground.value: {None: 2}},\n    UnitTypeId.RAVAGER: {TargetType.Ground.value: {None: 2}},\n    # Lair Tech Units (Hydra, Lurker, Ultra)\n    UnitTypeId.LURKERMPBURROWED: {TargetType.Ground.value: {None: 2, IS_ARMORED: 1}},\n    UnitTypeId.ULTRALISK: {TargetType.Ground.value: {None: 3}},\n    # Spire Units (Muta, Corruptor, BL)\n    UnitTypeId.CORRUPTOR: {TargetType.Air.value: {IS_MASSIVE: 1}},\n    UnitTypeId.BROODLORD: {TargetType.Ground.value: {None: 2}},\n}\nTARGET_HELPER = {\n    1: \"no target\",\n    2: \"Point2\",\n    3: \"Unit\",\n    4: \"Point2 or Unit\",\n    5: \"Point2 or no target\",\n}\nCREATION_ABILITY_FIX: dict[UnitTypeId, AbilityId] = {\n    UnitTypeId.ARCHON: AbilityId.ARCHON_WARP_TARGET,\n    UnitTypeId.ASSIMILATORRICH: AbilityId.PROTOSSBUILD_ASSIMILATOR,\n    UnitTypeId.BANELINGCOCOON: AbilityId.MORPHZERGLINGTOBANELING_BANELING,\n    UnitTypeId.CHANGELING: AbilityId.SPAWNCHANGELING_SPAWNCHANGELING,\n    UnitTypeId.EXTRACTORRICH: AbilityId.ZERGBUILD_EXTRACTOR,\n    UnitTypeId.INTERCEPTOR: AbilityId.BUILD_INTERCEPTORS,\n    UnitTypeId.LURKERMPEGG: AbilityId.MORPH_LURKER,\n    UnitTypeId.MULE: AbilityId.CALLDOWNMULE_CALLDOWNMULE,\n    UnitTypeId.RAVAGERCOCOON: AbilityId.MORPHTORAVAGER_RAVAGER,\n    UnitTypeId.REFINERYRICH: AbilityId.TERRANBUILD_REFINERY,\n    UnitTypeId.TECHLAB: AbilityId.BUILD_TECHLAB,\n}\n"
  },
  {
    "path": "sc2/controller.py",
    "content": "from __future__ import annotations\n\nimport platform\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom aiohttp import ClientWebSocketResponse\nfrom loguru import logger\n\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2.player import Computer\nfrom sc2.protocol import Protocol\n\nif TYPE_CHECKING:\n    from sc2.sc2process import SC2Process\n\n\nclass Controller(Protocol):\n    def __init__(self, ws: ClientWebSocketResponse, process: SC2Process) -> None:\n        super().__init__(ws)\n        self._process = process\n\n    @property\n    def running(self) -> bool:\n        return self._process._process is not None\n\n    async def create_game(self, game_map, players, realtime: bool, random_seed=None, disable_fog=None):\n        req = sc_pb.RequestCreateGame(\n            local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)),\n            realtime=realtime,\n            # pyrefly: ignore\n            disable_fog=disable_fog,\n        )\n        if random_seed is not None:\n            req.random_seed = random_seed\n\n        for player in players:\n            # pyrefly: ignore\n            p = req.player_setup.add()\n            p.type = player.type.value\n            if isinstance(player, Computer):\n                # pyrefly: ignore[bad-assignment]\n                p.race = player.race.value\n                # pyrefly: ignore[bad-assignment]\n                p.difficulty = player.difficulty.value\n                if player.ai_build is not None:\n                    # pyrefly: ignore[bad-assignment]\n                    p.ai_build = player.ai_build.value\n\n        logger.info(\"Creating new game\")\n        logger.info(f\"Map:     {game_map.name}\")\n        logger.info(f\"Players: {', '.join(str(p) for p in players)}\")\n        result = await self._execute(create_game=req)\n        return result\n\n    async def request_available_maps(self):\n        req = sc_pb.RequestAvailableMaps()\n        result = await self._execute(available_maps=req)\n        return result\n\n    async def request_save_map(self, download_path: str):\n        \"\"\"Not working on linux.\"\"\"\n        req = sc_pb.RequestSaveMap(map_path=download_path)\n        result = await self._execute(save_map=req)\n        return result\n\n    async def request_replay_info(self, replay_path: str):\n        \"\"\"Not working on linux.\"\"\"\n        req = sc_pb.RequestReplayInfo(replay_path=replay_path, download_data=False)\n        result = await self._execute(replay_info=req)\n        return result\n\n    async def start_replay(self, replay_path: str, realtime: bool, observed_id: int = 0):\n        ifopts = sc_pb.InterfaceOptions(\n            raw=True, score=True, show_cloaked=True, raw_affects_selection=True, raw_crop_to_playable_area=False\n        )\n        if platform.system() == \"Linux\":\n            replay_name = Path(replay_path).name\n            home_replay_folder = Path.home() / \"Documents\" / \"StarCraft II\" / \"Replays\"\n            if str(home_replay_folder / replay_name) != replay_path:\n                logger.warning(\n                    f\"Linux detected, please put your replay in your home directory at {home_replay_folder}. It was detected at {replay_path}\"\n                )\n                raise FileNotFoundError\n            replay_path = replay_name\n\n        req = sc_pb.RequestStartReplay(\n            replay_path=replay_path, observed_player_id=observed_id, realtime=realtime, options=ifopts\n        )\n\n        result = await self._execute(start_replay=req)\n        assert result.status == 4, f\"{result.start_replay.error} - {result.start_replay.error_details}\"\n        return result\n"
  },
  {
    "path": "sc2/data.py",
    "content": "\"\"\"For the list of enums, see here\n\nhttps://github.com/Blizzard/s2client-proto/tree/bff45dae1fc685e6acbaae084670afb7d1c0832c/s2clientprotocol\n\"\"\"\n\nfrom __future__ import annotations\n\nimport enum\n\nfrom s2clientprotocol import common_pb2 as common_pb\nfrom s2clientprotocol import data_pb2 as data_pb\nfrom s2clientprotocol import error_pb2 as error_pb\nfrom s2clientprotocol import raw_pb2 as raw_pb\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\n\nCreateGameError = enum.Enum(\"CreateGameError\", sc_pb.ResponseCreateGame.Error.items())\n\nPlayerType = enum.Enum(\"PlayerType\", sc_pb.PlayerType.items())\nDifficulty = enum.Enum(\"Difficulty\", sc_pb.Difficulty.items())\nAIBuild = enum.Enum(\"AIBuild\", sc_pb.AIBuild.items())\nStatus = enum.Enum(\"Status\", sc_pb.Status.items())\nResult = enum.Enum(\"Result\", sc_pb.Result.items())\nAlert = enum.Enum(\"Alert\", sc_pb.Alert.items())\nChatChannel = enum.Enum(\"ChatChannel\", sc_pb.ActionChat.Channel.items())\n\nRace = enum.Enum(\"Race\", common_pb.Race.items())\n\nDisplayType = enum.Enum(\"DisplayType\", raw_pb.DisplayType.items())\nAlliance = enum.Enum(\"Alliance\", raw_pb.Alliance.items())\nCloakState = enum.Enum(\"CloakState\", raw_pb.CloakState.items())\n\nAttribute = enum.Enum(\"Attribute\", data_pb.Attribute.items())\nTargetType = enum.Enum(\"TargetType\", data_pb.Weapon.TargetType.items())\nTarget = enum.Enum(\"Target\", data_pb.AbilityData.Target.items())\n\nActionResult = enum.Enum(\"ActionResult\", error_pb.ActionResult.items())\n\n\nrace_worker: dict[Race, UnitTypeId] = {\n    Race.Protoss: UnitTypeId.PROBE,\n    Race.Terran: UnitTypeId.SCV,\n    Race.Zerg: UnitTypeId.DRONE,\n}\n\nrace_townhalls: dict[Race, set[UnitTypeId]] = {\n    Race.Protoss: {UnitTypeId.NEXUS},\n    Race.Terran: {\n        UnitTypeId.COMMANDCENTER,\n        UnitTypeId.ORBITALCOMMAND,\n        UnitTypeId.PLANETARYFORTRESS,\n        UnitTypeId.COMMANDCENTERFLYING,\n        UnitTypeId.ORBITALCOMMANDFLYING,\n    },\n    Race.Zerg: {UnitTypeId.HATCHERY, UnitTypeId.LAIR, UnitTypeId.HIVE},\n    Race.Random: {\n        # Protoss\n        UnitTypeId.NEXUS,\n        # Terran\n        UnitTypeId.COMMANDCENTER,\n        UnitTypeId.ORBITALCOMMAND,\n        UnitTypeId.PLANETARYFORTRESS,\n        UnitTypeId.COMMANDCENTERFLYING,\n        UnitTypeId.ORBITALCOMMANDFLYING,\n        # Zerg\n        UnitTypeId.HATCHERY,\n        UnitTypeId.LAIR,\n        UnitTypeId.HIVE,\n    },\n}\n\nwarpgate_abilities: dict[AbilityId, AbilityId] = {\n    AbilityId.GATEWAYTRAIN_ZEALOT: AbilityId.WARPGATETRAIN_ZEALOT,\n    AbilityId.GATEWAYTRAIN_STALKER: AbilityId.WARPGATETRAIN_STALKER,\n    AbilityId.GATEWAYTRAIN_HIGHTEMPLAR: AbilityId.WARPGATETRAIN_HIGHTEMPLAR,\n    AbilityId.GATEWAYTRAIN_DARKTEMPLAR: AbilityId.WARPGATETRAIN_DARKTEMPLAR,\n    AbilityId.GATEWAYTRAIN_SENTRY: AbilityId.WARPGATETRAIN_SENTRY,\n    AbilityId.TRAIN_ADEPT: AbilityId.TRAINWARP_ADEPT,\n}\n\nrace_gas: dict[Race, UnitTypeId] = {\n    Race.Protoss: UnitTypeId.ASSIMILATOR,\n    Race.Terran: UnitTypeId.REFINERY,\n    Race.Zerg: UnitTypeId.EXTRACTOR,\n}\n"
  },
  {
    "path": "sc2/data.pyi",
    "content": "\"\"\"Type stubs for sc2.data module\n\nThis stub provides static type information for dynamically generated enums.\nThe enums in sc2.data are created at runtime using enum.Enum() with protobuf\nenum descriptors, which makes them invisible to static type checkers.\n\nThis stub file (PEP 561 compliant) allows type checkers like Pylance, Pyright,\nand mypy to understand the structure and members of these enums.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom enum import Enum\n\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\n\nclass CreateGameError(Enum):\n    MissingMap = 1\n    InvalidMapPath = 2\n    InvalidMapData = 3\n    InvalidMapName = 4\n    InvalidMapHandle = 5\n    MissingPlayerSetup = 6\n    InvalidPlayerSetup = 7\n    MultiplayerUnsupported = 8\n\nclass PlayerType(Enum):\n    Participant = 1\n    Computer = 2\n    Observer = 3\n\nclass Difficulty(Enum):\n    VeryEasy = 1\n    Easy = 2\n    Medium = 3\n    MediumHard = 4\n    Hard = 5\n    Harder = 6\n    VeryHard = 7\n    CheatVision = 8\n    CheatMoney = 9\n    CheatInsane = 10\n\nclass AIBuild(Enum):\n    RandomBuild = 1\n    Rush = 2\n    Timing = 3\n    Power = 4\n    Macro = 5\n    Air = 6\n\nclass Status(Enum):\n    launched = 1\n    init_game = 2\n    in_game = 3\n    in_replay = 4\n    ended = 5\n    quit = 6\n    unknown = 7\n\nclass Result(Enum):\n    Victory = 1\n    Defeat = 2\n    Tie = 3\n    Undecided = 4\n\nclass Alert(Enum):\n    AlertError = 1\n    AddOnComplete = 2\n    BuildingComplete = 3\n    BuildingUnderAttack = 4\n    LarvaHatched = 5\n    MergeComplete = 6\n    MineralsExhausted = 7\n    MorphComplete = 8\n    MothershipComplete = 9\n    MULEExpired = 10\n    NuclearLaunchDetected = 11\n    NukeComplete = 12\n    NydusWormDetected = 13\n    ResearchComplete = 14\n    TrainError = 15\n    TrainUnitComplete = 16\n    TrainWorkerComplete = 17\n    TransformationComplete = 18\n    UnitUnderAttack = 19\n    UpgradeComplete = 20\n    VespeneExhausted = 21\n    WarpInComplete = 22\n\nclass ChatChannel(Enum):\n    Broadcast = 1\n    Team = 2\n\nclass Race(Enum):\n    \"\"\"StarCraft II race enum.\n\n    Members:\n        NoRace: No race specified\n        Terran: Terran race\n        Zerg: Zerg race\n        Protoss: Protoss race\n        Random: Random race selection\n    \"\"\"\n\n    NoRace = 0\n    Terran = 1\n    Zerg = 2\n    Protoss = 3\n    Random = 4\n\n# Enums created from raw_pb2\nclass DisplayType(Enum):\n    Visible = 1\n    Snapshot = 2\n    Hidden = 3\n    Placeholder = 4\n\nclass Alliance(Enum):\n    Self = 1\n    Ally = 2\n    Neutral = 3\n    Enemy = 4\n\nclass CloakState(Enum):\n    CloakedUnknown = 1\n    Cloaked = 2\n    CloakedDetected = 3\n    NotCloaked = 4\n    CloakedAllied = 5\n\nclass Attribute(Enum):\n    Light = 1\n    Armored = 2\n    Biological = 3\n    Mechanical = 4\n    Robotic = 5\n    Psionic = 6\n    Massive = 7\n    Structure = 8\n    Hover = 9\n    Heroic = 10\n    Summoned = 11\n\nclass TargetType(Enum):\n    Ground = 1\n    Air = 2\n    Any = 3\n    Invalid = 4\n\nclass Target(Enum):\n    # Note: The protobuf enum member 'None' is a Python keyword,\n    # so at runtime it may need special handling\n    Point = 1\n    Unit = 2\n    PointOrUnit = 3\n    PointOrNone = 4\n\nclass ActionResult(Enum):\n    \"\"\"Action result codes from game engine.\n\n    This enum contains a large number of members (~200+) representing\n    various action results and error conditions.\n    \"\"\"\n\n    Success = 1\n    NotSupported = 2\n    Error = 3\n    CantQueueThatOrder = 4\n    Retry = 5\n    Cooldown = 6\n    QueueIsFull = 7\n    RallyQueueIsFull = 8\n    NotEnoughMinerals = 9\n    NotEnoughVespene = 10\n    NotEnoughTerrazine = 11\n    NotEnoughCustom = 12\n    NotEnoughFood = 13\n    FoodUsageImpossible = 14\n    NotEnoughLife = 15\n    NotEnoughShields = 16\n    NotEnoughEnergy = 17\n    LifeSuppressed = 18\n    ShieldsSuppressed = 19\n    EnergySuppressed = 20\n    NotEnoughCharges = 21\n    CantAddMoreCharges = 22\n    TooMuchMinerals = 23\n    TooMuchVespene = 24\n    TooMuchTerrazine = 25\n    TooMuchCustom = 26\n    TooMuchFood = 27\n    TooMuchLife = 28\n    TooMuchShields = 29\n    TooMuchEnergy = 30\n    MustTargetUnitWithLife = 31\n    MustTargetUnitWithShields = 32\n    MustTargetUnitWithEnergy = 33\n    CantTrade = 34\n    CantSpend = 35\n    CantTargetThatUnit = 36\n    CouldntAllocateUnit = 37\n    UnitCantMove = 38\n    TransportIsHoldingPosition = 39\n    BuildTechRequirementsNotMet = 40\n    CantFindPlacementLocation = 41\n    CantBuildOnThat = 42\n    CantBuildTooCloseToDropOff = 43\n    CantBuildLocationInvalid = 44\n    CantSeeBuildLocation = 45\n    CantBuildTooCloseToCreepSource = 46\n    CantBuildTooCloseToResources = 47\n    CantBuildTooFarFromWater = 48\n    CantBuildTooFarFromCreepSource = 49\n    CantBuildTooFarFromBuildPowerSource = 50\n    CantBuildOnDenseTerrain = 51\n    CantTrainTooFarFromTrainPowerSource = 52\n    CantLandLocationInvalid = 53\n    CantSeeLandLocation = 54\n    CantLandTooCloseToCreepSource = 55\n    CantLandTooCloseToResources = 56\n    CantLandTooFarFromWater = 57\n    CantLandTooFarFromCreepSource = 58\n    CantLandTooFarFromBuildPowerSource = 59\n    CantLandTooFarFromTrainPowerSource = 60\n    CantLandOnDenseTerrain = 61\n    AddOnTooFarFromBuilding = 62\n    MustBuildRefineryFirst = 63\n    BuildingIsUnderConstruction = 64\n    CantFindDropOff = 65\n    CantLoadOtherPlayersUnits = 66\n    NotEnoughRoomToLoadUnit = 67\n    CantUnloadUnitsThere = 68\n    CantWarpInUnitsThere = 69\n    CantLoadImmobileUnits = 70\n    CantRechargeImmobileUnits = 71\n    CantRechargeUnderConstructionUnits = 72\n    CantLoadThatUnit = 73\n    NoCargoToUnload = 74\n    LoadAllNoTargetsFound = 75\n    NotWhileOccupied = 76\n    CantAttackWithoutAmmo = 77\n    CantHoldAnyMoreAmmo = 78\n    TechRequirementsNotMet = 79\n    MustLockdownUnitFirst = 80\n    MustTargetUnit = 81\n    MustTargetInventory = 82\n    MustTargetVisibleUnit = 83\n    MustTargetVisibleLocation = 84\n    MustTargetWalkableLocation = 85\n    MustTargetPawnableUnit = 86\n    YouCantControlThatUnit = 87\n    YouCantIssueCommandsToThatUnit = 88\n    MustTargetResources = 89\n    RequiresHealTarget = 90\n    RequiresRepairTarget = 91\n    NoItemsToDrop = 92\n    CantHoldAnyMoreItems = 93\n    CantHoldThat = 94\n    TargetHasNoInventory = 95\n    CantDropThisItem = 96\n    CantMoveThisItem = 97\n    CantPawnThisUnit = 98\n    MustTargetCaster = 99\n    CantTargetCaster = 100\n    MustTargetOuter = 101\n    CantTargetOuter = 102\n    MustTargetYourOwnUnits = 103\n    CantTargetYourOwnUnits = 104\n    MustTargetFriendlyUnits = 105\n    CantTargetFriendlyUnits = 106\n    MustTargetNeutralUnits = 107\n    CantTargetNeutralUnits = 108\n    MustTargetEnemyUnits = 109\n    CantTargetEnemyUnits = 110\n    MustTargetAirUnits = 111\n    CantTargetAirUnits = 112\n    MustTargetGroundUnits = 113\n    CantTargetGroundUnits = 114\n    MustTargetStructures = 115\n    CantTargetStructures = 116\n    MustTargetLightUnits = 117\n    CantTargetLightUnits = 118\n    MustTargetArmoredUnits = 119\n    CantTargetArmoredUnits = 120\n    MustTargetBiologicalUnits = 121\n    CantTargetBiologicalUnits = 122\n    MustTargetHeroicUnits = 123\n    CantTargetHeroicUnits = 124\n    MustTargetRoboticUnits = 125\n    CantTargetRoboticUnits = 126\n    MustTargetMechanicalUnits = 127\n    CantTargetMechanicalUnits = 128\n    MustTargetPsionicUnits = 129\n    CantTargetPsionicUnits = 130\n    MustTargetMassiveUnits = 131\n    CantTargetMassiveUnits = 132\n    MustTargetMissile = 133\n    CantTargetMissile = 134\n    MustTargetWorkerUnits = 135\n    CantTargetWorkerUnits = 136\n    MustTargetEnergyCapableUnits = 137\n    CantTargetEnergyCapableUnits = 138\n    MustTargetShieldCapableUnits = 139\n    CantTargetShieldCapableUnits = 140\n    MustTargetFlyers = 141\n    CantTargetFlyers = 142\n    MustTargetBuriedUnits = 143\n    CantTargetBuriedUnits = 144\n    MustTargetCloakedUnits = 145\n    CantTargetCloakedUnits = 146\n    MustTargetUnitsInAStasisField = 147\n    CantTargetUnitsInAStasisField = 148\n    MustTargetUnderConstructionUnits = 149\n    CantTargetUnderConstructionUnits = 150\n    MustTargetDeadUnits = 151\n    CantTargetDeadUnits = 152\n    MustTargetRevivableUnits = 153\n    CantTargetRevivableUnits = 154\n    MustTargetHiddenUnits = 155\n    CantTargetHiddenUnits = 156\n    CantRechargeOtherPlayersUnits = 157\n    MustTargetHallucinations = 158\n    CantTargetHallucinations = 159\n    MustTargetInvulnerableUnits = 160\n    CantTargetInvulnerableUnits = 161\n    MustTargetDetectedUnits = 162\n    CantTargetDetectedUnits = 163\n    CantTargetUnitWithEnergy = 164\n    CantTargetUnitWithShields = 165\n    MustTargetUncommandableUnits = 166\n    CantTargetUncommandableUnits = 167\n    MustTargetPreventDefeatUnits = 168\n    CantTargetPreventDefeatUnits = 169\n    MustTargetPreventRevealUnits = 170\n    CantTargetPreventRevealUnits = 171\n    MustTargetPassiveUnits = 172\n    CantTargetPassiveUnits = 173\n    MustTargetStunnedUnits = 174\n    CantTargetStunnedUnits = 175\n    MustTargetSummonedUnits = 176\n    CantTargetSummonedUnits = 177\n    MustTargetUser1 = 178\n    CantTargetUser1 = 179\n    MustTargetUnstoppableUnits = 180\n    CantTargetUnstoppableUnits = 181\n    MustTargetResistantUnits = 182\n    CantTargetResistantUnits = 183\n    MustTargetDazedUnits = 184\n    CantTargetDazedUnits = 185\n    CantLockdown = 186\n    CantMindControl = 187\n    MustTargetDestructibles = 188\n    CantTargetDestructibles = 189\n    MustTargetItems = 190\n    CantTargetItems = 191\n    NoCalldownAvailable = 192\n    WaypointListFull = 193\n    MustTargetRace = 194\n    CantTargetRace = 195\n    MustTargetSimilarUnits = 196\n    CantTargetSimilarUnits = 197\n    CantFindEnoughTargets = 198\n    AlreadySpawningLarva = 199\n    CantTargetExhaustedResources = 200\n    CantUseMinimap = 201\n    CantUseInfoPanel = 202\n    OrderQueueIsFull = 203\n    CantHarvestThatResource = 204\n    HarvestersNotRequired = 205\n    AlreadyTargeted = 206\n    CantAttackWeaponsDisabled = 207\n    CouldntReachTarget = 208\n    TargetIsOutOfRange = 209\n    TargetIsTooClose = 210\n    TargetIsOutOfArc = 211\n    CantFindTeleportLocation = 212\n    InvalidItemClass = 213\n    CantFindCancelOrder = 214\n\n# Module-level dictionaries\nrace_worker: dict[Race, UnitTypeId]\nrace_townhalls: dict[Race, set[UnitTypeId]]\nwarpgate_abilities: dict[AbilityId, AbilityId]\nrace_gas: dict[Race, UnitTypeId]\n"
  },
  {
    "path": "sc2/dicts/__init__.py",
    "content": "# DO NOT EDIT!\n# This file was automatically generated by \"generate_dicts_from_data_json.py\"\n\n__all__ = [\n    \"generic_redirect_abilities\",\n    \"unit_abilities\",\n    \"unit_research_abilities\",\n    \"unit_tech_alias\",\n    \"unit_train_build_abilities\",\n    \"unit_trained_from\",\n    \"unit_unit_alias\",\n    \"upgrade_researched_from\",\n]\n"
  },
  {
    "path": "sc2/dicts/generic_redirect_abilities.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.ability_id import AbilityId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nGENERIC_REDIRECT_ABILITIES: dict[AbilityId, AbilityId] = {\n    AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANSHIPWEAPONS,\n    AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANSHIPWEAPONS,\n    AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANSHIPWEAPONS,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3: AbilityId.RESEARCH_TERRANVEHICLEANDSHIPPLATING,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS,\n    AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANVEHICLEWEAPONS,\n    AbilityId.ATTACKPROTOSSBUILDING_ATTACKBUILDING: AbilityId.ATTACK,\n    AbilityId.ATTACK_ATTACK: AbilityId.ATTACK,\n    AbilityId.ATTACK_BATTLECRUISER: AbilityId.ATTACK,\n    AbilityId.ATTACK_REDIRECT: AbilityId.ATTACK,\n    AbilityId.BEHAVIOR_CLOAKOFF_BANSHEE: AbilityId.BEHAVIOR_CLOAKOFF,\n    AbilityId.BEHAVIOR_CLOAKOFF_GHOST: AbilityId.BEHAVIOR_CLOAKOFF,\n    AbilityId.BEHAVIOR_CLOAKON_BANSHEE: AbilityId.BEHAVIOR_CLOAKON,\n    AbilityId.BEHAVIOR_CLOAKON_GHOST: AbilityId.BEHAVIOR_CLOAKON,\n    AbilityId.BEHAVIOR_HOLDFIREOFF_GHOST: AbilityId.BEHAVIOR_HOLDFIREOFF,\n    AbilityId.BEHAVIOR_HOLDFIREOFF_LURKER: AbilityId.BEHAVIOR_HOLDFIREOFF,\n    AbilityId.BEHAVIOR_HOLDFIREON_GHOST: AbilityId.BEHAVIOR_HOLDFIREON,\n    AbilityId.BEHAVIOR_HOLDFIREON_LURKER: AbilityId.BEHAVIOR_HOLDFIREON,\n    AbilityId.BROODLORDQUEUE2_CANCEL: AbilityId.CANCEL_LAST,\n    AbilityId.BROODLORDQUEUE2_CANCELSLOT: AbilityId.CANCEL_SLOT,\n    AbilityId.BUILDINPROGRESSNYDUSCANAL_CANCEL: AbilityId.CANCEL,\n    AbilityId.BUILDNYDUSCANAL_CANCEL: AbilityId.HALT,\n    AbilityId.BUILD_CREEPTUMOR_QUEEN: AbilityId.BUILD_CREEPTUMOR,\n    AbilityId.BUILD_CREEPTUMOR_TUMOR: AbilityId.BUILD_CREEPTUMOR,\n    AbilityId.BUILD_REACTOR_BARRACKS: AbilityId.BUILD_REACTOR,\n    AbilityId.BUILD_REACTOR_FACTORY: AbilityId.BUILD_REACTOR,\n    AbilityId.BUILD_REACTOR_STARPORT: AbilityId.BUILD_REACTOR,\n    AbilityId.BUILD_TECHLAB_BARRACKS: AbilityId.BUILD_TECHLAB,\n    AbilityId.BUILD_TECHLAB_FACTORY: AbilityId.BUILD_TECHLAB,\n    AbilityId.BUILD_TECHLAB_STARPORT: AbilityId.BUILD_TECHLAB,\n    AbilityId.BURROWBANELINGDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWCREEPTUMORDOWN_BURROWDOWN: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_BANELING: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_DRONE: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_HYDRALISK: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_INFESTOR: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_INFESTORTERRAN: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_LURKER: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_QUEEN: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_RAVAGER: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_ROACH: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_SWARMHOST: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_ULTRALISK: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_WIDOWMINE: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDOWN_ZERGLING: AbilityId.BURROWDOWN,\n    AbilityId.BURROWDRONEDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWHYDRALISKDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWINFESTORDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWLURKERMPDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWQUEENDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWRAVAGERDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWROACHDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.BURROWUP_BANELING: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_DRONE: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_HYDRALISK: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_INFESTOR: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_INFESTORTERRAN: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_LURKER: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_QUEEN: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_RAVAGER: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_ROACH: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_SWARMHOST: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_ULTRALISK: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_WIDOWMINE: AbilityId.BURROWUP,\n    AbilityId.BURROWUP_ZERGLING: AbilityId.BURROWUP,\n    AbilityId.BURROWZERGLINGDOWN_CANCEL: AbilityId.CANCEL,\n    AbilityId.CANCELSLOT_ADDON: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_HANGARQUEUE5: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_QUEUE1: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_QUEUE5: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_QUEUECANCELTOSELECTION: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_QUEUEPASSIVE: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCELSLOT_QUEUEPASSIVECANCELTOSELECTION: AbilityId.CANCEL_SLOT,\n    AbilityId.CANCEL_ADEPTPHASESHIFT: AbilityId.CANCEL,\n    AbilityId.CANCEL_ADEPTSHADEPHASESHIFT: AbilityId.CANCEL,\n    AbilityId.CANCEL_BARRACKSADDON: AbilityId.CANCEL,\n    AbilityId.CANCEL_BUILDINPROGRESS: AbilityId.CANCEL,\n    AbilityId.CANCEL_CREEPTUMOR: AbilityId.CANCEL,\n    AbilityId.CANCEL_FACTORYADDON: AbilityId.CANCEL,\n    AbilityId.CANCEL_GRAVITONBEAM: AbilityId.CANCEL,\n    AbilityId.CANCEL_HANGARQUEUE5: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_LOCKON: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHBROODLORD: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHGREATERSPIRE: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHHIVE: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHLAIR: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHLURKER: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHMOTHERSHIP: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHORBITAL: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHOVERLORDTRANSPORT: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHOVERSEER: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHPLANETARYFORTRESS: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHRAVAGER: AbilityId.CANCEL,\n    AbilityId.CANCEL_MORPHTHOREXPLOSIVEMODE: AbilityId.CANCEL,\n    AbilityId.CANCEL_NEURALPARASITE: AbilityId.CANCEL,\n    AbilityId.CANCEL_NUKE: AbilityId.CANCEL,\n    AbilityId.CANCEL_QUEUE1: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_QUEUE5: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_QUEUEADDON: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_QUEUECANCELTOSELECTION: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_QUEUEPASIVE: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_QUEUEPASSIVECANCELTOSELECTION: AbilityId.CANCEL_LAST,\n    AbilityId.CANCEL_SPINECRAWLERROOT: AbilityId.CANCEL,\n    AbilityId.CANCEL_SPORECRAWLERROOT: AbilityId.CANCEL,\n    AbilityId.CANCEL_STARPORTADDON: AbilityId.CANCEL,\n    AbilityId.CANCEL_STASISTRAP: AbilityId.CANCEL,\n    AbilityId.CANCEL_VOIDRAYPRISMATICALIGNMENT: AbilityId.CANCEL,\n    AbilityId.CHANNELSNIPE_CANCEL: AbilityId.CANCEL,\n    AbilityId.COMMANDCENTERTRANSPORT_COMMANDCENTERTRANSPORT: AbilityId.LOAD,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1: AbilityId.RESEARCH_PROTOSSAIRARMOR,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2: AbilityId.RESEARCH_PROTOSSAIRARMOR,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3: AbilityId.RESEARCH_PROTOSSAIRARMOR,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1: AbilityId.RESEARCH_PROTOSSAIRWEAPONS,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2: AbilityId.RESEARCH_PROTOSSAIRWEAPONS,\n    AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3: AbilityId.RESEARCH_PROTOSSAIRWEAPONS,\n    AbilityId.DEFILERMPBURROW_BURROWDOWN: AbilityId.BURROWDOWN,\n    AbilityId.DEFILERMPBURROW_CANCEL: AbilityId.CANCEL,\n    AbilityId.DEFILERMPUNBURROW_BURROWUP: AbilityId.BURROWUP,\n    AbilityId.EFFECT_BLINK_STALKER: AbilityId.EFFECT_BLINK,\n    AbilityId.EFFECT_MASSRECALL_MOTHERSHIPCORE: AbilityId.EFFECT_MASSRECALL,\n    AbilityId.EFFECT_MASSRECALL_NEXUS: AbilityId.EFFECT_MASSRECALL,\n    AbilityId.EFFECT_MASSRECALL_STRATEGICRECALL: AbilityId.EFFECT_MASSRECALL,\n    AbilityId.EFFECT_REPAIR_MULE: AbilityId.EFFECT_REPAIR,\n    AbilityId.EFFECT_REPAIR_REPAIRDRONE: AbilityId.EFFECT_REPAIR,\n    AbilityId.EFFECT_REPAIR_SCV: AbilityId.EFFECT_REPAIR,\n    AbilityId.EFFECT_SHADOWSTRIDE: AbilityId.EFFECT_BLINK,\n    AbilityId.EFFECT_SPRAY_PROTOSS: AbilityId.EFFECT_SPRAY,\n    AbilityId.EFFECT_SPRAY_TERRAN: AbilityId.EFFECT_SPRAY,\n    AbilityId.EFFECT_SPRAY_ZERG: AbilityId.EFFECT_SPRAY,\n    AbilityId.EFFECT_STIM_MARAUDER: AbilityId.EFFECT_STIM,\n    AbilityId.EFFECT_STIM_MARAUDER_REDIRECT: AbilityId.EFFECT_STIM,\n    AbilityId.EFFECT_STIM_MARINE: AbilityId.EFFECT_STIM,\n    AbilityId.EFFECT_STIM_MARINE_REDIRECT: AbilityId.EFFECT_STIM,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1: AbilityId.RESEARCH_TERRANINFANTRYARMOR,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2: AbilityId.RESEARCH_TERRANINFANTRYARMOR,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3: AbilityId.RESEARCH_TERRANINFANTRYARMOR,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS,\n    AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3: AbilityId.RESEARCH_TERRANINFANTRYWEAPONS,\n    AbilityId.FORCEFIELD_CANCEL: AbilityId.CANCEL,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1: AbilityId.RESEARCH_PROTOSSGROUNDARMOR,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2: AbilityId.RESEARCH_PROTOSSGROUNDARMOR,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3: AbilityId.RESEARCH_PROTOSSGROUNDARMOR,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS,\n    AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3: AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS,\n    AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1: AbilityId.RESEARCH_PROTOSSSHIELDS,\n    AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2: AbilityId.RESEARCH_PROTOSSSHIELDS,\n    AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3: AbilityId.RESEARCH_PROTOSSSHIELDS,\n    AbilityId.HALT_BUILDING: AbilityId.HALT,\n    AbilityId.HALT_TERRANBUILD: AbilityId.HALT,\n    AbilityId.HARVEST_GATHER_DRONE: AbilityId.HARVEST_GATHER,\n    AbilityId.HARVEST_GATHER_MULE: AbilityId.HARVEST_GATHER,\n    AbilityId.HARVEST_GATHER_PROBE: AbilityId.HARVEST_GATHER,\n    AbilityId.HARVEST_GATHER_SCV: AbilityId.HARVEST_GATHER,\n    AbilityId.HARVEST_RETURN_DRONE: AbilityId.HARVEST_RETURN,\n    AbilityId.HARVEST_RETURN_MULE: AbilityId.HARVEST_RETURN,\n    AbilityId.HARVEST_RETURN_PROBE: AbilityId.HARVEST_RETURN,\n    AbilityId.HARVEST_RETURN_SCV: AbilityId.HARVEST_RETURN,\n    AbilityId.HOLDPOSITION_BATTLECRUISER: AbilityId.HOLDPOSITION,\n    AbilityId.HOLDPOSITION_HOLD: AbilityId.HOLDPOSITION,\n    AbilityId.LAND_BARRACKS: AbilityId.LAND,\n    AbilityId.LAND_COMMANDCENTER: AbilityId.LAND,\n    AbilityId.LAND_FACTORY: AbilityId.LAND,\n    AbilityId.LAND_ORBITALCOMMAND: AbilityId.LAND,\n    AbilityId.LAND_STARPORT: AbilityId.LAND,\n    AbilityId.LIFT_BARRACKS: AbilityId.LIFT,\n    AbilityId.LIFT_COMMANDCENTER: AbilityId.LIFT,\n    AbilityId.LIFT_FACTORY: AbilityId.LIFT,\n    AbilityId.LIFT_ORBITALCOMMAND: AbilityId.LIFT,\n    AbilityId.LIFT_STARPORT: AbilityId.LIFT,\n    AbilityId.LOADALL_COMMANDCENTER: AbilityId.LOADALL,\n    AbilityId.LOAD_BUNKER: AbilityId.LOAD,\n    AbilityId.LOAD_MEDIVAC: AbilityId.LOAD,\n    AbilityId.LOAD_NYDUSNETWORK: AbilityId.LOAD,\n    AbilityId.LOAD_NYDUSWORM: AbilityId.LOAD,\n    AbilityId.LOAD_OVERLORD: AbilityId.LOAD,\n    AbilityId.LOAD_WARPPRISM: AbilityId.LOAD,\n    AbilityId.MERGEABLE_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHBACKTOGATEWAY_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOBANELING_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTODEVOURERMP_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOGUARDIANMP_CANCEL: AbilityId.CANCEL,\n    AbilityId.MORPHTOSWARMHOSTBURROWEDMP_CANCEL: AbilityId.CANCEL,\n    AbilityId.MOVE_BATTLECRUISER: AbilityId.MOVE,\n    AbilityId.MOVE_MOVE: AbilityId.MOVE,\n    AbilityId.PATROL_BATTLECRUISER: AbilityId.PATROL,\n    AbilityId.PATROL_PATROL: AbilityId.PATROL,\n    AbilityId.PHASINGMODE_CANCEL: AbilityId.CANCEL,\n    AbilityId.PROTOSSBUILD_CANCEL: AbilityId.HALT,\n    AbilityId.QUEENBUILD_CANCEL: AbilityId.HALT,\n    AbilityId.RALLY_BUILDING: AbilityId.RALLY_UNITS,\n    AbilityId.RALLY_COMMANDCENTER: AbilityId.RALLY_WORKERS,\n    AbilityId.RALLY_HATCHERY_UNITS: AbilityId.RALLY_UNITS,\n    AbilityId.RALLY_HATCHERY_WORKERS: AbilityId.RALLY_WORKERS,\n    AbilityId.RALLY_MORPHING_UNIT: AbilityId.RALLY_UNITS,\n    AbilityId.RALLY_NEXUS: AbilityId.RALLY_WORKERS,\n    AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1: AbilityId.RESEARCH_ZERGFLYERARMOR,\n    AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2: AbilityId.RESEARCH_ZERGFLYERARMOR,\n    AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3: AbilityId.RESEARCH_ZERGFLYERARMOR,\n    AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1: AbilityId.RESEARCH_ZERGFLYERATTACK,\n    AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2: AbilityId.RESEARCH_ZERGFLYERATTACK,\n    AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3: AbilityId.RESEARCH_ZERGFLYERATTACK,\n    AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1: AbilityId.RESEARCH_ZERGGROUNDARMOR,\n    AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2: AbilityId.RESEARCH_ZERGGROUNDARMOR,\n    AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3: AbilityId.RESEARCH_ZERGGROUNDARMOR,\n    AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1: AbilityId.RESEARCH_ZERGMELEEWEAPONS,\n    AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2: AbilityId.RESEARCH_ZERGMELEEWEAPONS,\n    AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3: AbilityId.RESEARCH_ZERGMELEEWEAPONS,\n    AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1: AbilityId.RESEARCH_ZERGMISSILEWEAPONS,\n    AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2: AbilityId.RESEARCH_ZERGMISSILEWEAPONS,\n    AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3: AbilityId.RESEARCH_ZERGMISSILEWEAPONS,\n    AbilityId.SCAN_MOVE: AbilityId.ATTACK,\n    AbilityId.SHIELDBATTERYRECHARGEEX5_STOP: AbilityId.CANCEL,\n    AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT: AbilityId.MORPH_ROOT,\n    AbilityId.SPINECRAWLERUPROOT_CANCEL: AbilityId.CANCEL,\n    AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT: AbilityId.MORPH_UPROOT,\n    AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT: AbilityId.MORPH_ROOT,\n    AbilityId.SPORECRAWLERUPROOT_CANCEL: AbilityId.CANCEL,\n    AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT: AbilityId.MORPH_UPROOT,\n    AbilityId.STOP_BATTLECRUISER: AbilityId.STOP,\n    AbilityId.STOP_BUILDING: AbilityId.STOP,\n    AbilityId.STOP_CHEER: AbilityId.STOP,\n    AbilityId.STOP_DANCE: AbilityId.STOP,\n    AbilityId.STOP_HOLDFIRESPECIAL: AbilityId.STOP,\n    AbilityId.STOP_REDIRECT: AbilityId.STOP,\n    AbilityId.STOP_STOP: AbilityId.STOP,\n    AbilityId.TESTZERG_CANCEL: AbilityId.CANCEL,\n    AbilityId.THORAPMODE_CANCEL: AbilityId.CANCEL,\n    AbilityId.TRANSPORTMODE_CANCEL: AbilityId.CANCEL,\n    AbilityId.UNLOADALLAT_MEDIVAC: AbilityId.UNLOADALLAT,\n    AbilityId.UNLOADALLAT_OVERLORD: AbilityId.UNLOADALLAT,\n    AbilityId.UNLOADALLAT_WARPPRISM: AbilityId.UNLOADALLAT,\n    AbilityId.UNLOADALL_BUNKER: AbilityId.UNLOADALL,\n    AbilityId.UNLOADALL_COMMANDCENTER: AbilityId.UNLOADALL,\n    AbilityId.UNLOADALL_NYDASNETWORK: AbilityId.UNLOADALL,\n    AbilityId.UNLOADALL_NYDUSWORM: AbilityId.UNLOADALL,\n    AbilityId.UNLOADALL_WARPPRISM: AbilityId.UNLOADALL,\n    AbilityId.UNLOADUNIT_BUNKER: AbilityId.UNLOADUNIT,\n    AbilityId.UNLOADUNIT_COMMANDCENTER: AbilityId.UNLOADUNIT,\n    AbilityId.UNLOADUNIT_MEDIVAC: AbilityId.UNLOADUNIT,\n    AbilityId.UNLOADUNIT_NYDASNETWORK: AbilityId.UNLOADUNIT,\n    AbilityId.UNLOADUNIT_OVERLORD: AbilityId.UNLOADUNIT,\n    AbilityId.UNLOADUNIT_WARPPRISM: AbilityId.UNLOADUNIT,\n    AbilityId.UPGRADETOWARPGATE_CANCEL: AbilityId.CANCEL,\n    AbilityId.WARPABLE_CANCEL: AbilityId.CANCEL,\n    AbilityId.WIDOWMINEBURROW_CANCEL: AbilityId.CANCEL,\n    AbilityId.ZERGBUILD_CANCEL: AbilityId.HALT,\n}\n"
  },
  {
    "path": "sc2/dicts/unit_abilities.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nUNIT_ABILITIES: dict[UnitTypeId, set[AbilityId]] = {\n    UnitTypeId.ADEPT: {\n        AbilityId.ADEPTPHASESHIFT_ADEPTPHASESHIFT,\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ADEPTPHASESHIFT: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.CANCEL_ADEPTSHADEPHASESHIFT,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ARBITERMP: {\n        AbilityId.ARBITERMPRECALL_ARBITERMPRECALL,\n        AbilityId.ARBITERMPSTASISFIELD_ARBITERMPSTASISFIELD,\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ARCHON: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ARMORY: {\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1,\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2,\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3,\n    },\n    UnitTypeId.AUTOTURRET: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.BANELING: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_BUILDINGATTACKON,\n        AbilityId.BURROWDOWN_BANELING,\n        AbilityId.EXPLODE_EXPLODE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.BANELINGBURROWED: {AbilityId.BURROWUP_BANELING, AbilityId.EXPLODE_EXPLODE},\n    UnitTypeId.BANELINGCOCOON: {AbilityId.RALLY_BUILDING, AbilityId.SMART},\n    UnitTypeId.BANELINGNEST: {AbilityId.RESEARCH_CENTRIFUGALHOOKS},\n    UnitTypeId.BANSHEE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_CLOAKON_BANSHEE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.BARRACKS: {\n        AbilityId.BARRACKSTRAIN_GHOST,\n        AbilityId.BARRACKSTRAIN_MARAUDER,\n        AbilityId.BARRACKSTRAIN_MARINE,\n        AbilityId.BARRACKSTRAIN_REAPER,\n        AbilityId.BUILD_REACTOR_BARRACKS,\n        AbilityId.BUILD_TECHLAB_BARRACKS,\n        AbilityId.LIFT_BARRACKS,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n    },\n    UnitTypeId.BARRACKSFLYING: {\n        AbilityId.BUILD_REACTOR_BARRACKS,\n        AbilityId.BUILD_TECHLAB_BARRACKS,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LAND_BARRACKS,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.BARRACKSTECHLAB: {\n        AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK,\n        AbilityId.RESEARCH_COMBATSHIELD,\n        AbilityId.RESEARCH_CONCUSSIVESHELLS,\n    },\n    UnitTypeId.BATTLECRUISER: {\n        AbilityId.ATTACK_BATTLECRUISER,\n        AbilityId.EFFECT_TACTICALJUMP,\n        AbilityId.HOLDPOSITION_BATTLECRUISER,\n        AbilityId.MOVE_BATTLECRUISER,\n        AbilityId.PATROL_BATTLECRUISER,\n        AbilityId.SMART,\n        AbilityId.STOP_BATTLECRUISER,\n        AbilityId.YAMATO_YAMATOGUN,\n    },\n    UnitTypeId.BROODLING: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.BROODLORD: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.BUNKER: {\n        AbilityId.LOAD_BUNKER,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SALVAGEEFFECT_SALVAGE,\n        AbilityId.SMART,\n    },\n    UnitTypeId.BYPASSARMORDRONE: {AbilityId.ATTACK_ATTACK, AbilityId.MOVE_MOVE, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.CARRIER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BUILD_INTERCEPTORS,\n        AbilityId.CANCEL_HANGARQUEUE5,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELING: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELINGMARINE: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELINGMARINESHIELD: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELINGZEALOT: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELINGZERGLING: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CHANGELINGZERGLINGWINGS: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.COLOSSUS: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.COMMANDCENTER: {\n        AbilityId.COMMANDCENTERTRAIN_SCV,\n        AbilityId.LIFT_COMMANDCENTER,\n        AbilityId.LOADALL_COMMANDCENTER,\n        AbilityId.RALLY_COMMANDCENTER,\n        AbilityId.SMART,\n        AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND,\n        AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS,\n    },\n    UnitTypeId.COMMANDCENTERFLYING: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LAND_COMMANDCENTER,\n        AbilityId.LOADALL_COMMANDCENTER,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CORRUPTOR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.CAUSTICSPRAY_CAUSTICSPRAY,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPHTOBROODLORD_BROODLORD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CORSAIRMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.CORSAIRMPDISRUPTIONWEB_CORSAIRMPDISRUPTIONWEB,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.CREEPTUMORBURROWED: {AbilityId.BUILD_CREEPTUMOR, AbilityId.BUILD_CREEPTUMOR_TUMOR, AbilityId.SMART},\n    UnitTypeId.CYBERNETICSCORE: {\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3,\n        AbilityId.RESEARCH_WARPGATE,\n    },\n    UnitTypeId.CYCLONE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LOCKON_LOCKON,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DARKSHRINE: {AbilityId.RESEARCH_SHADOWSTRIKE},\n    UnitTypeId.DARKTEMPLAR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_SHADOWSTRIDE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_ARCHON,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DEFILERMP: {\n        AbilityId.DEFILERMPBURROW_BURROWDOWN,\n        AbilityId.DEFILERMPCONSUME_DEFILERMPCONSUME,\n        AbilityId.DEFILERMPDARKSWARM_DEFILERMPDARKSWARM,\n        AbilityId.DEFILERMPPLAGUE_DEFILERMPPLAGUE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DEFILERMPBURROWED: {AbilityId.DEFILERMPUNBURROW_BURROWUP},\n    UnitTypeId.DEVOURERMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DISRUPTOR: {\n        AbilityId.EFFECT_PURIFICATIONNOVA,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DISRUPTORPHASED: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.DRONE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BUILD_LURKERDEN,\n        AbilityId.BURROWDOWN_DRONE,\n        AbilityId.EFFECT_SPRAY_ZERG,\n        AbilityId.HARVEST_GATHER_DRONE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.ZERGBUILD_BANELINGNEST,\n        AbilityId.ZERGBUILD_EVOLUTIONCHAMBER,\n        AbilityId.ZERGBUILD_EXTRACTOR,\n        AbilityId.ZERGBUILD_HATCHERY,\n        AbilityId.ZERGBUILD_HYDRALISKDEN,\n        AbilityId.ZERGBUILD_INFESTATIONPIT,\n        AbilityId.ZERGBUILD_NYDUSNETWORK,\n        AbilityId.ZERGBUILD_ROACHWARREN,\n        AbilityId.ZERGBUILD_SPAWNINGPOOL,\n        AbilityId.ZERGBUILD_SPINECRAWLER,\n        AbilityId.ZERGBUILD_SPIRE,\n        AbilityId.ZERGBUILD_SPORECRAWLER,\n        AbilityId.ZERGBUILD_ULTRALISKCAVERN,\n    },\n    UnitTypeId.DRONEBURROWED: {AbilityId.BURROWUP_DRONE},\n    UnitTypeId.EGG: {AbilityId.RALLY_BUILDING, AbilityId.SMART},\n    UnitTypeId.ELSECARO_COLONIST_HUT: {AbilityId.RALLY_BUILDING, AbilityId.SMART},\n    UnitTypeId.ENGINEERINGBAY: {\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3,\n        AbilityId.RESEARCH_HISECAUTOTRACKING,\n        AbilityId.RESEARCH_TERRANSTRUCTUREARMORUPGRADE,\n    },\n    UnitTypeId.EVOLUTIONCHAMBER: {\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1,\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2,\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3,\n    },\n    UnitTypeId.FACTORY: {\n        AbilityId.BUILD_REACTOR_FACTORY,\n        AbilityId.BUILD_TECHLAB_FACTORY,\n        AbilityId.FACTORYTRAIN_HELLION,\n        AbilityId.FACTORYTRAIN_SIEGETANK,\n        AbilityId.FACTORYTRAIN_THOR,\n        AbilityId.FACTORYTRAIN_WIDOWMINE,\n        AbilityId.LIFT_FACTORY,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.TRAIN_CYCLONE,\n        AbilityId.TRAIN_HELLBAT,\n    },\n    UnitTypeId.FACTORYFLYING: {\n        AbilityId.BUILD_REACTOR_FACTORY,\n        AbilityId.BUILD_TECHLAB_FACTORY,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LAND_FACTORY,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.FACTORYTECHLAB: {\n        AbilityId.RESEARCH_CYCLONELOCKONDAMAGE,\n        AbilityId.RESEARCH_DRILLINGCLAWS,\n        AbilityId.RESEARCH_INFERNALPREIGNITER,\n        AbilityId.RESEARCH_SMARTSERVOS,\n    },\n    UnitTypeId.FLEETBEACON: {\n        AbilityId.FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE,\n        AbilityId.FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE,\n        AbilityId.RESEARCH_PHOENIXANIONPULSECRYSTALS,\n    },\n    UnitTypeId.FORGE: {\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3,\n    },\n    UnitTypeId.FUSIONCORE: {\n        AbilityId.FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE,\n        AbilityId.FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE,\n        AbilityId.RESEARCH_BATTLECRUISERWEAPONREFIT,\n    },\n    UnitTypeId.GATEWAY: {\n        AbilityId.GATEWAYTRAIN_DARKTEMPLAR,\n        AbilityId.GATEWAYTRAIN_HIGHTEMPLAR,\n        AbilityId.GATEWAYTRAIN_SENTRY,\n        AbilityId.GATEWAYTRAIN_STALKER,\n        AbilityId.GATEWAYTRAIN_ZEALOT,\n        AbilityId.MORPH_WARPGATE,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.TRAIN_ADEPT,\n    },\n    UnitTypeId.GHOST: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_CLOAKON_GHOST,\n        AbilityId.BEHAVIOR_HOLDFIREON_GHOST,\n        AbilityId.EFFECT_GHOSTSNIPE,\n        AbilityId.EMP_EMP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.GHOSTACADEMY: {AbilityId.BUILD_NUKE, AbilityId.RESEARCH_PERSONALCLOAKING},\n    UnitTypeId.GHOSTNOVA: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_CLOAKON_GHOST,\n        AbilityId.BEHAVIOR_HOLDFIREON_GHOST,\n        AbilityId.EFFECT_GHOSTSNIPE,\n        AbilityId.EMP_EMP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.GREATERSPIRE: {\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3,\n    },\n    UnitTypeId.GUARDIANMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HATCHERY: {\n        AbilityId.RALLY_HATCHERY_UNITS,\n        AbilityId.RALLY_HATCHERY_WORKERS,\n        AbilityId.RESEARCH_BURROW,\n        AbilityId.RESEARCH_PNEUMATIZEDCARAPACE,\n        AbilityId.SMART,\n        AbilityId.TRAINQUEEN_QUEEN,\n        AbilityId.UPGRADETOLAIR_LAIR,\n    },\n    UnitTypeId.HELLION: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_HELLBAT,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HELLIONTANK: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_HELLION,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HERC: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HERCPLACEMENT: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HIGHTEMPLAR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.FEEDBACK_FEEDBACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_ARCHON,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.PSISTORM_PSISTORM,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HIVE: {\n        AbilityId.RALLY_HATCHERY_UNITS,\n        AbilityId.RALLY_HATCHERY_WORKERS,\n        AbilityId.RESEARCH_BURROW,\n        AbilityId.RESEARCH_PNEUMATIZEDCARAPACE,\n        AbilityId.SMART,\n        AbilityId.TRAINQUEEN_QUEEN,\n    },\n    UnitTypeId.HYDRALISK: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_HYDRALISK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.HYDRALISKFRENZY_HYDRALISKFRENZY,\n        AbilityId.MORPH_LURKER,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.HYDRALISKBURROWED: {AbilityId.BURROWUP_HYDRALISK},\n    UnitTypeId.HYDRALISKDEN: {\n        AbilityId.HYDRALISKDENRESEARCH_RESEARCHFRENZY,\n        AbilityId.RESEARCH_GROOVEDSPINES,\n        AbilityId.RESEARCH_MUSCULARAUGMENTS,\n    },\n    UnitTypeId.IMMORTAL: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.INFESTATIONPIT: {AbilityId.RESEARCH_NEURALPARASITE},\n    UnitTypeId.INFESTOR: {\n        AbilityId.AMORPHOUSARMORCLOUD_AMORPHOUSARMORCLOUD,\n        AbilityId.BURROWDOWN_INFESTOR,\n        AbilityId.BURROWDOWN_INFESTORTERRAN,\n        AbilityId.FUNGALGROWTH_FUNGALGROWTH,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.NEURALPARASITE_NEURALPARASITE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.INFESTORBURROWED: {\n        AbilityId.BURROWUP_INFESTOR,\n        AbilityId.BURROWUP_INFESTORTERRAN,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.NEURALPARASITE_NEURALPARASITE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.INFESTORTERRAN: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_INFESTORTERRAN,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.INFESTORTERRANBURROWED: {AbilityId.BURROWUP_INFESTORTERRAN},\n    UnitTypeId.INTERCEPTOR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LAIR: {\n        AbilityId.RALLY_HATCHERY_UNITS,\n        AbilityId.RALLY_HATCHERY_WORKERS,\n        AbilityId.RESEARCH_BURROW,\n        AbilityId.RESEARCH_PNEUMATIZEDCARAPACE,\n        AbilityId.SMART,\n        AbilityId.TRAINQUEEN_QUEEN,\n        AbilityId.UPGRADETOHIVE_HIVE,\n    },\n    UnitTypeId.LARVA: {\n        AbilityId.LARVATRAIN_CORRUPTOR,\n        AbilityId.LARVATRAIN_DRONE,\n        AbilityId.LARVATRAIN_HYDRALISK,\n        AbilityId.LARVATRAIN_INFESTOR,\n        AbilityId.LARVATRAIN_MUTALISK,\n        AbilityId.LARVATRAIN_OVERLORD,\n        AbilityId.LARVATRAIN_ROACH,\n        AbilityId.LARVATRAIN_ULTRALISK,\n        AbilityId.LARVATRAIN_VIPER,\n        AbilityId.LARVATRAIN_ZERGLING,\n        AbilityId.TRAIN_SWARMHOST,\n    },\n    UnitTypeId.LIBERATOR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_LIBERATORAGMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LIBERATORAG: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.MORPH_LIBERATORAAMODE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LOCUSTMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LOCUSTMPFLYING: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_LOCUSTSWOOP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LURKERDENMP: {AbilityId.LURKERDENRESEARCH_RESEARCHLURKERRANGE, AbilityId.RESEARCH_ADAPTIVETALONS},\n    UnitTypeId.LURKERMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_LURKER,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LURKERMPBURROWED: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_HOLDFIREON_LURKER,\n        AbilityId.BURROWUP_LURKER,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.LURKERMPEGG: {AbilityId.RALLY_BUILDING, AbilityId.SMART},\n    UnitTypeId.MARAUDER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_STIM_MARAUDER,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MARINE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_STIM_MARINE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MEDIVAC: {\n        AbilityId.EFFECT_MEDIVACIGNITEAFTERBURNERS,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LOAD_MEDIVAC,\n        AbilityId.MEDIVACHEAL_HEAL,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MISSILETURRET: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.MOTHERSHIP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_MASSRECALL_STRATEGICRECALL,\n        AbilityId.EFFECT_TIMEWARP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOTHERSHIPCLOAK_ORACLECLOAKFIELD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MOTHERSHIPCORE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_MASSRECALL_MOTHERSHIPCORE,\n        AbilityId.EFFECT_PHOTONOVERCHARGE,\n        AbilityId.EFFECT_TIMEWARP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_MOTHERSHIP,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MULE: {\n        AbilityId.EFFECT_REPAIR_MULE,\n        AbilityId.HARVEST_GATHER_MULE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.MUTALISK: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.NEXUS: {\n        AbilityId.EFFECT_CHRONOBOOSTENERGYCOST,\n        AbilityId.EFFECT_MASSRECALL_NEXUS,\n        AbilityId.ENERGYRECHARGE_ENERGYRECHARGE,\n        AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP,\n        AbilityId.NEXUSTRAIN_PROBE,\n        AbilityId.RALLY_NEXUS,\n        AbilityId.SMART,\n    },\n    UnitTypeId.NYDUSCANAL: {AbilityId.LOAD_NYDUSWORM, AbilityId.RALLY_BUILDING, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.NYDUSCANALATTACKER: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.NYDUSCANALCREEPER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.DIGESTERCREEPSPRAY_DIGESTERCREEPSPRAY,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.NYDUSNETWORK: {\n        AbilityId.BUILD_NYDUSWORM,\n        AbilityId.LOAD_NYDUSNETWORK,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OBSERVER: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_SURVEILLANCEMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OBSERVERSIEGEMODE: {AbilityId.MORPH_OBSERVERMODE, AbilityId.STOP_STOP},\n    UnitTypeId.ORACLE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BEHAVIOR_PULSARBEAMON,\n        AbilityId.BUILD_STASISTRAP,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.ORACLEREVELATION_ORACLEREVELATION,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ORBITALCOMMAND: {\n        AbilityId.CALLDOWNMULE_CALLDOWNMULE,\n        AbilityId.COMMANDCENTERTRAIN_SCV,\n        AbilityId.LIFT_ORBITALCOMMAND,\n        AbilityId.RALLY_COMMANDCENTER,\n        AbilityId.SCANNERSWEEP_SCAN,\n        AbilityId.SMART,\n        AbilityId.SUPPLYDROP_SUPPLYDROP,\n    },\n    UnitTypeId.ORBITALCOMMANDFLYING: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LAND_ORBITALCOMMAND,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OVERLORD: {\n        AbilityId.BEHAVIOR_GENERATECREEPON,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_OVERLORDTRANSPORT,\n        AbilityId.MORPH_OVERSEER,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OVERLORDTRANSPORT: {\n        AbilityId.BEHAVIOR_GENERATECREEPON,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LOAD_OVERLORD,\n        AbilityId.MORPH_OVERSEER,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OVERSEER: {\n        AbilityId.CONTAMINATE_CONTAMINATE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_OVERSIGHTMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.SPAWNCHANGELING_SPAWNCHANGELING,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.OVERSEERSIEGEMODE: {\n        AbilityId.CONTAMINATE_CONTAMINATE,\n        AbilityId.MORPH_OVERSEERMODE,\n        AbilityId.SMART,\n        AbilityId.SPAWNCHANGELING_SPAWNCHANGELING,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.PHOENIX: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.GRAVITONBEAM_GRAVITONBEAM,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.PHOTONCANNON: {AbilityId.ATTACK_ATTACK, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.PLANETARYFORTRESS: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.COMMANDCENTERTRAIN_SCV,\n        AbilityId.LOADALL_COMMANDCENTER,\n        AbilityId.RALLY_COMMANDCENTER,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.PROBE: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BUILD_SHIELDBATTERY,\n        AbilityId.EFFECT_SPRAY_PROTOSS,\n        AbilityId.HARVEST_GATHER_PROBE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.PROTOSSBUILD_ASSIMILATOR,\n        AbilityId.PROTOSSBUILD_CYBERNETICSCORE,\n        AbilityId.PROTOSSBUILD_DARKSHRINE,\n        AbilityId.PROTOSSBUILD_FLEETBEACON,\n        AbilityId.PROTOSSBUILD_FORGE,\n        AbilityId.PROTOSSBUILD_GATEWAY,\n        AbilityId.PROTOSSBUILD_NEXUS,\n        AbilityId.PROTOSSBUILD_PHOTONCANNON,\n        AbilityId.PROTOSSBUILD_PYLON,\n        AbilityId.PROTOSSBUILD_ROBOTICSBAY,\n        AbilityId.PROTOSSBUILD_ROBOTICSFACILITY,\n        AbilityId.PROTOSSBUILD_STARGATE,\n        AbilityId.PROTOSSBUILD_TEMPLARARCHIVE,\n        AbilityId.PROTOSSBUILD_TWILIGHTCOUNCIL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.QUEEN: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BUILD_CREEPTUMOR,\n        AbilityId.BUILD_CREEPTUMOR_QUEEN,\n        AbilityId.BURROWDOWN_QUEEN,\n        AbilityId.EFFECT_INJECTLARVA,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.TRANSFUSION_TRANSFUSION,\n    },\n    UnitTypeId.QUEENBURROWED: {AbilityId.BURROWUP_QUEEN},\n    UnitTypeId.QUEENMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.QUEENMPENSNARE_QUEENMPENSNARE,\n        AbilityId.QUEENMPINFESTCOMMANDCENTER_QUEENMPINFESTCOMMANDCENTER,\n        AbilityId.QUEENMPSPAWNBROODLINGS_QUEENMPSPAWNBROODLINGS,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.RAVAGER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_RAVAGER,\n        AbilityId.EFFECT_CORROSIVEBILE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.RAVAGERBURROWED: {AbilityId.BURROWUP_RAVAGER},\n    UnitTypeId.RAVAGERCOCOON: {AbilityId.RALLY_BUILDING, AbilityId.SMART},\n    UnitTypeId.RAVEN: {\n        AbilityId.BUILDAUTOTURRET_AUTOTURRET,\n        AbilityId.EFFECT_ANTIARMORMISSILE,\n        AbilityId.EFFECT_INTERFERENCEMATRIX,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.RAVENREPAIRDRONE: {AbilityId.EFFECT_REPAIR_REPAIRDRONE, AbilityId.SMART, AbilityId.STOP_STOP},\n    UnitTypeId.REAPER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.KD8CHARGE_KD8CHARGE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.REPLICANT: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ROACH: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_ROACH,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPHTORAVAGER_RAVAGER,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ROACHBURROWED: {\n        AbilityId.BURROWUP_ROACH,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ROACHWARREN: {AbilityId.RESEARCH_GLIALREGENERATION, AbilityId.RESEARCH_TUNNELINGCLAWS},\n    UnitTypeId.ROBOTICSBAY: {\n        AbilityId.RESEARCH_EXTENDEDTHERMALLANCE,\n        AbilityId.RESEARCH_GRAVITICBOOSTER,\n        AbilityId.RESEARCH_GRAVITICDRIVE,\n    },\n    UnitTypeId.ROBOTICSFACILITY: {\n        AbilityId.RALLY_BUILDING,\n        AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS,\n        AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL,\n        AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER,\n        AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM,\n        AbilityId.SMART,\n        AbilityId.TRAIN_DISRUPTOR,\n    },\n    UnitTypeId.SCOURGEMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SCOUTMP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SCV: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_REPAIR_SCV,\n        AbilityId.EFFECT_SPRAY_TERRAN,\n        AbilityId.HARVEST_GATHER_SCV,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.TERRANBUILD_ARMORY,\n        AbilityId.TERRANBUILD_BARRACKS,\n        AbilityId.TERRANBUILD_BUNKER,\n        AbilityId.TERRANBUILD_COMMANDCENTER,\n        AbilityId.TERRANBUILD_ENGINEERINGBAY,\n        AbilityId.TERRANBUILD_FACTORY,\n        AbilityId.TERRANBUILD_FUSIONCORE,\n        AbilityId.TERRANBUILD_GHOSTACADEMY,\n        AbilityId.TERRANBUILD_MISSILETURRET,\n        AbilityId.TERRANBUILD_REFINERY,\n        AbilityId.TERRANBUILD_SENSORTOWER,\n        AbilityId.TERRANBUILD_STARPORT,\n        AbilityId.TERRANBUILD_SUPPLYDEPOT,\n    },\n    UnitTypeId.SENSORTOWER: {AbilityId.SALVAGEEFFECT_SALVAGE},\n    UnitTypeId.SENTRY: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.FORCEFIELD_FORCEFIELD,\n        AbilityId.GUARDIANSHIELD_GUARDIANSHIELD,\n        AbilityId.HALLUCINATION_ADEPT,\n        AbilityId.HALLUCINATION_ARCHON,\n        AbilityId.HALLUCINATION_COLOSSUS,\n        AbilityId.HALLUCINATION_DISRUPTOR,\n        AbilityId.HALLUCINATION_HIGHTEMPLAR,\n        AbilityId.HALLUCINATION_IMMORTAL,\n        AbilityId.HALLUCINATION_ORACLE,\n        AbilityId.HALLUCINATION_PHOENIX,\n        AbilityId.HALLUCINATION_PROBE,\n        AbilityId.HALLUCINATION_STALKER,\n        AbilityId.HALLUCINATION_VOIDRAY,\n        AbilityId.HALLUCINATION_WARPPRISM,\n        AbilityId.HALLUCINATION_ZEALOT,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SHIELDBATTERY: {AbilityId.SHIELDBATTERYRECHARGEEX5_SHIELDBATTERYRECHARGE, AbilityId.SMART},\n    UnitTypeId.SIEGETANK: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SIEGEMODE_SIEGEMODE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SIEGETANKSIEGED: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.UNSIEGE_UNSIEGE,\n    },\n    UnitTypeId.SPAWNINGPOOL: {AbilityId.RESEARCH_ZERGLINGADRENALGLANDS, AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST},\n    UnitTypeId.SPINECRAWLER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.SMART,\n        AbilityId.SPINECRAWLERUPROOT_SPINECRAWLERUPROOT,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SPINECRAWLERUPROOTED: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.SPINECRAWLERROOT_SPINECRAWLERROOT,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SPIRE: {\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3,\n        AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE,\n    },\n    UnitTypeId.SPORECRAWLER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.SMART,\n        AbilityId.SPORECRAWLERUPROOT_SPORECRAWLERUPROOT,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.SPORECRAWLERUPROOTED: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.SPORECRAWLERROOT_SPORECRAWLERROOT,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.STALKER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_BLINK_STALKER,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.STARGATE: {\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.STARGATETRAIN_CARRIER,\n        AbilityId.STARGATETRAIN_ORACLE,\n        AbilityId.STARGATETRAIN_PHOENIX,\n        AbilityId.STARGATETRAIN_TEMPEST,\n        AbilityId.STARGATETRAIN_VOIDRAY,\n    },\n    UnitTypeId.STARPORT: {\n        AbilityId.BUILD_REACTOR_STARPORT,\n        AbilityId.BUILD_TECHLAB_STARPORT,\n        AbilityId.LIFT_STARPORT,\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.STARPORTTRAIN_BANSHEE,\n        AbilityId.STARPORTTRAIN_BATTLECRUISER,\n        AbilityId.STARPORTTRAIN_LIBERATOR,\n        AbilityId.STARPORTTRAIN_MEDIVAC,\n        AbilityId.STARPORTTRAIN_RAVEN,\n        AbilityId.STARPORTTRAIN_VIKINGFIGHTER,\n    },\n    UnitTypeId.STARPORTFLYING: {\n        AbilityId.BUILD_REACTOR_STARPORT,\n        AbilityId.BUILD_TECHLAB_STARPORT,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LAND_STARPORT,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.STARPORTTECHLAB: {\n        AbilityId.RESEARCH_BANSHEECLOAKINGFIELD,\n        AbilityId.RESEARCH_BANSHEEHYPERFLIGHTROTORS,\n        AbilityId.STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX,\n    },\n    UnitTypeId.SUPPLYDEPOT: {AbilityId.MORPH_SUPPLYDEPOT_LOWER},\n    UnitTypeId.SUPPLYDEPOTLOWERED: {AbilityId.MORPH_SUPPLYDEPOT_RAISE},\n    UnitTypeId.SWARMHOSTBURROWEDMP: {AbilityId.EFFECT_SPAWNLOCUSTS, AbilityId.SMART},\n    UnitTypeId.SWARMHOSTMP: {\n        AbilityId.BURROWDOWN_SWARMHOST,\n        AbilityId.EFFECT_SPAWNLOCUSTS,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.TECHLAB: {\n        AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK,\n        AbilityId.RESEARCH_BANSHEECLOAKINGFIELD,\n        AbilityId.RESEARCH_COMBATSHIELD,\n        AbilityId.RESEARCH_CONCUSSIVESHELLS,\n        AbilityId.RESEARCH_DRILLINGCLAWS,\n        AbilityId.RESEARCH_INFERNALPREIGNITER,\n        AbilityId.RESEARCH_RAVENCORVIDREACTOR,\n    },\n    UnitTypeId.TEMPEST: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.TEMPLARARCHIVE: {AbilityId.RESEARCH_PSISTORM},\n    UnitTypeId.THOR: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_THORHIGHIMPACTMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.THORAP: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_THOREXPLOSIVEMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.TWILIGHTCOUNCIL: {\n        AbilityId.RESEARCH_ADEPTRESONATINGGLAIVES,\n        AbilityId.RESEARCH_BLINK,\n        AbilityId.RESEARCH_CHARGE,\n    },\n    UnitTypeId.ULTRALISK: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_ULTRALISK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ULTRALISKBURROWED: {AbilityId.BURROWUP_ULTRALISK},\n    UnitTypeId.ULTRALISKCAVERN: {AbilityId.RESEARCH_ANABOLICSYNTHESIS, AbilityId.RESEARCH_CHITINOUSPLATING},\n    UnitTypeId.VIKINGASSAULT: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_VIKINGFIGHTERMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.VIKINGFIGHTER: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPH_VIKINGASSAULTMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.VIPER: {\n        AbilityId.BLINDINGCLOUD_BLINDINGCLOUD,\n        AbilityId.EFFECT_ABDUCT,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PARASITICBOMB_PARASITICBOMB,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.VIPERCONSUMESTRUCTURE_VIPERCONSUME,\n    },\n    UnitTypeId.VOIDMPIMMORTALREVIVECORPSE: {\n        AbilityId.RALLY_BUILDING,\n        AbilityId.SMART,\n        AbilityId.VOIDMPIMMORTALREVIVEREBUILD_IMMORTAL,\n    },\n    UnitTypeId.VOIDRAY: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_VOIDRAYPRISMATICALIGNMENT,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.WARHOUND: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n        AbilityId.TORNADOMISSILE_TORNADOMISSILE,\n    },\n    UnitTypeId.WARPGATE: {\n        AbilityId.MORPH_GATEWAY,\n        AbilityId.SMART,\n        AbilityId.TRAINWARP_ADEPT,\n        AbilityId.WARPGATETRAIN_DARKTEMPLAR,\n        AbilityId.WARPGATETRAIN_HIGHTEMPLAR,\n        AbilityId.WARPGATETRAIN_SENTRY,\n        AbilityId.WARPGATETRAIN_STALKER,\n        AbilityId.WARPGATETRAIN_ZEALOT,\n    },\n    UnitTypeId.WARPPRISM: {\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.LOAD_WARPPRISM,\n        AbilityId.MORPH_WARPPRISMPHASINGMODE,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.WARPPRISMPHASING: {\n        AbilityId.LOAD_WARPPRISM,\n        AbilityId.MORPH_WARPPRISMTRANSPORTMODE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.WIDOWMINE: {\n        AbilityId.BURROWDOWN_WIDOWMINE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SCAN_MOVE,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.WIDOWMINEBURROWED: {\n        AbilityId.BURROWUP_WIDOWMINE,\n        AbilityId.SMART,\n        AbilityId.WIDOWMINEATTACK_WIDOWMINEATTACK,\n    },\n    UnitTypeId.ZEALOT: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.EFFECT_CHARGE,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ZERGLING: {\n        AbilityId.ATTACK_ATTACK,\n        AbilityId.BURROWDOWN_ZERGLING,\n        AbilityId.HOLDPOSITION_HOLD,\n        AbilityId.MORPHTOBANELING_BANELING,\n        AbilityId.MOVE_MOVE,\n        AbilityId.PATROL_PATROL,\n        AbilityId.SMART,\n        AbilityId.STOP_STOP,\n    },\n    UnitTypeId.ZERGLINGBURROWED: {AbilityId.BURROWUP_ZERGLING},\n}\n"
  },
  {
    "path": "sc2/dicts/unit_research_abilities.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\nfrom typing import Union\n\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\n\nRESEARCH_INFO: dict[UnitTypeId, dict[UpgradeId, dict[str, Union[AbilityId, bool, UnitTypeId, UpgradeId]]]] = {\n    UnitTypeId.ARMORY: {\n        UpgradeId.TERRANSHIPWEAPONSLEVEL1: {\"ability\": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1},\n        UpgradeId.TERRANSHIPWEAPONSLEVEL2: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2,\n            \"required_upgrade\": UpgradeId.TERRANSHIPWEAPONSLEVEL1,\n        },\n        UpgradeId.TERRANSHIPWEAPONSLEVEL3: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3,\n            \"required_upgrade\": UpgradeId.TERRANSHIPWEAPONSLEVEL2,\n        },\n        UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1\n        },\n        UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2,\n            \"required_upgrade\": UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1,\n        },\n        UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3,\n            \"required_upgrade\": UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2,\n        },\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: {\"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1},\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2,\n            \"required_upgrade\": UpgradeId.TERRANVEHICLEWEAPONSLEVEL1,\n        },\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: {\n            \"ability\": AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3,\n            \"required_upgrade\": UpgradeId.TERRANVEHICLEWEAPONSLEVEL2,\n        },\n    },\n    UnitTypeId.BANELINGNEST: {\n        UpgradeId.CENTRIFICALHOOKS: {\n            \"ability\": AbilityId.RESEARCH_CENTRIFUGALHOOKS,\n            \"required_building\": UnitTypeId.LAIR,\n        }\n    },\n    UnitTypeId.BARRACKSTECHLAB: {\n        UpgradeId.PUNISHERGRENADES: {\"ability\": AbilityId.RESEARCH_CONCUSSIVESHELLS},\n        UpgradeId.SHIELDWALL: {\"ability\": AbilityId.RESEARCH_COMBATSHIELD},\n        UpgradeId.STIMPACK: {\"ability\": AbilityId.BARRACKSTECHLABRESEARCH_STIMPACK},\n    },\n    UnitTypeId.CYBERNETICSCORE: {\n        UpgradeId.PROTOSSAIRARMORSLEVEL1: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSAIRARMORSLEVEL2: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"required_upgrade\": UpgradeId.PROTOSSAIRARMORSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSAIRARMORSLEVEL3: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"required_upgrade\": UpgradeId.PROTOSSAIRARMORSLEVEL2,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL1: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL2: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"required_upgrade\": UpgradeId.PROTOSSAIRWEAPONSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL3: {\n            \"ability\": AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"required_upgrade\": UpgradeId.PROTOSSAIRWEAPONSLEVEL2,\n            \"requires_power\": True,\n        },\n        UpgradeId.WARPGATERESEARCH: {\"ability\": AbilityId.RESEARCH_WARPGATE, \"requires_power\": True},\n    },\n    UnitTypeId.DARKSHRINE: {\n        UpgradeId.DARKTEMPLARBLINKUPGRADE: {\"ability\": AbilityId.RESEARCH_SHADOWSTRIKE, \"requires_power\": True}\n    },\n    UnitTypeId.ENGINEERINGBAY: {\n        UpgradeId.HISECAUTOTRACKING: {\"ability\": AbilityId.RESEARCH_HISECAUTOTRACKING},\n        UpgradeId.TERRANBUILDINGARMOR: {\"ability\": AbilityId.RESEARCH_TERRANSTRUCTUREARMORUPGRADE},\n        UpgradeId.TERRANINFANTRYARMORSLEVEL1: {\"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1},\n        UpgradeId.TERRANINFANTRYARMORSLEVEL2: {\n            \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2,\n            \"required_building\": UnitTypeId.ARMORY,\n            \"required_upgrade\": UpgradeId.TERRANINFANTRYARMORSLEVEL1,\n        },\n        UpgradeId.TERRANINFANTRYARMORSLEVEL3: {\n            \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3,\n            \"required_building\": UnitTypeId.ARMORY,\n            \"required_upgrade\": UpgradeId.TERRANINFANTRYARMORSLEVEL2,\n        },\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: {\n            \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1\n        },\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: {\n            \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2,\n            \"required_building\": UnitTypeId.ARMORY,\n            \"required_upgrade\": UpgradeId.TERRANINFANTRYWEAPONSLEVEL1,\n        },\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: {\n            \"ability\": AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3,\n            \"required_building\": UnitTypeId.ARMORY,\n            \"required_upgrade\": UpgradeId.TERRANINFANTRYWEAPONSLEVEL2,\n        },\n    },\n    UnitTypeId.EVOLUTIONCHAMBER: {\n        UpgradeId.ZERGGROUNDARMORSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1},\n        UpgradeId.ZERGGROUNDARMORSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGGROUNDARMORSLEVEL1,\n        },\n        UpgradeId.ZERGGROUNDARMORSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGGROUNDARMORSLEVEL2,\n        },\n        UpgradeId.ZERGMELEEWEAPONSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1},\n        UpgradeId.ZERGMELEEWEAPONSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGMELEEWEAPONSLEVEL1,\n        },\n        UpgradeId.ZERGMELEEWEAPONSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGMELEEWEAPONSLEVEL2,\n        },\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1},\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGMISSILEWEAPONSLEVEL1,\n        },\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGMISSILEWEAPONSLEVEL2,\n        },\n    },\n    UnitTypeId.FACTORYTECHLAB: {\n        UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: {\"ability\": AbilityId.RESEARCH_CYCLONELOCKONDAMAGE},\n        UpgradeId.DRILLCLAWS: {\"ability\": AbilityId.RESEARCH_DRILLINGCLAWS, \"required_building\": UnitTypeId.ARMORY},\n        UpgradeId.HIGHCAPACITYBARRELS: {\"ability\": AbilityId.RESEARCH_INFERNALPREIGNITER},\n        UpgradeId.SMARTSERVOS: {\"ability\": AbilityId.RESEARCH_SMARTSERVOS, \"required_building\": UnitTypeId.ARMORY},\n    },\n    UnitTypeId.FLEETBEACON: {\n        UpgradeId.PHOENIXRANGEUPGRADE: {\n            \"ability\": AbilityId.RESEARCH_PHOENIXANIONPULSECRYSTALS,\n            \"requires_power\": True,\n        },\n        UpgradeId.TEMPESTGROUNDATTACKUPGRADE: {\n            \"ability\": AbilityId.FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE,\n            \"requires_power\": True,\n        },\n        UpgradeId.VOIDRAYSPEEDUPGRADE: {\n            \"ability\": AbilityId.FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE,\n            \"requires_power\": True,\n        },\n    },\n    UnitTypeId.FORGE: {\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL1: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL2: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSGROUNDARMORSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL3: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSGROUNDARMORSLEVEL2,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSSHIELDSLEVEL1: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSSHIELDSLEVEL2: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSSHIELDSLEVEL1,\n            \"requires_power\": True,\n        },\n        UpgradeId.PROTOSSSHIELDSLEVEL3: {\n            \"ability\": AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"required_upgrade\": UpgradeId.PROTOSSSHIELDSLEVEL2,\n            \"requires_power\": True,\n        },\n    },\n    UnitTypeId.FUSIONCORE: {\n        UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: {\"ability\": AbilityId.RESEARCH_BATTLECRUISERWEAPONREFIT},\n        UpgradeId.LIBERATORAGRANGEUPGRADE: {\"ability\": AbilityId.FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE},\n        UpgradeId.MEDIVACCADUCEUSREACTOR: {\"ability\": AbilityId.FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE},\n    },\n    UnitTypeId.GHOSTACADEMY: {UpgradeId.PERSONALCLOAKING: {\"ability\": AbilityId.RESEARCH_PERSONALCLOAKING}},\n    UnitTypeId.GREATERSPIRE: {\n        UpgradeId.ZERGFLYERARMORSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1},\n        UpgradeId.ZERGFLYERARMORSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGFLYERARMORSLEVEL1,\n        },\n        UpgradeId.ZERGFLYERARMORSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGFLYERARMORSLEVEL2,\n        },\n        UpgradeId.ZERGFLYERWEAPONSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1},\n        UpgradeId.ZERGFLYERWEAPONSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGFLYERWEAPONSLEVEL1,\n        },\n        UpgradeId.ZERGFLYERWEAPONSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGFLYERWEAPONSLEVEL2,\n        },\n    },\n    UnitTypeId.HATCHERY: {\n        UpgradeId.BURROW: {\"ability\": AbilityId.RESEARCH_BURROW},\n        UpgradeId.OVERLORDSPEED: {\"ability\": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE},\n    },\n    UnitTypeId.HIVE: {\n        UpgradeId.BURROW: {\"ability\": AbilityId.RESEARCH_BURROW},\n        UpgradeId.OVERLORDSPEED: {\"ability\": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE},\n    },\n    UnitTypeId.HYDRALISKDEN: {\n        UpgradeId.EVOLVEGROOVEDSPINES: {\"ability\": AbilityId.RESEARCH_GROOVEDSPINES},\n        UpgradeId.EVOLVEMUSCULARAUGMENTS: {\"ability\": AbilityId.RESEARCH_MUSCULARAUGMENTS},\n        UpgradeId.FRENZY: {\n            \"ability\": AbilityId.HYDRALISKDENRESEARCH_RESEARCHFRENZY,\n            \"required_building\": UnitTypeId.HIVE,\n        },\n    },\n    UnitTypeId.INFESTATIONPIT: {UpgradeId.NEURALPARASITE: {\"ability\": AbilityId.RESEARCH_NEURALPARASITE}},\n    UnitTypeId.LAIR: {\n        UpgradeId.BURROW: {\"ability\": AbilityId.RESEARCH_BURROW},\n        UpgradeId.OVERLORDSPEED: {\"ability\": AbilityId.RESEARCH_PNEUMATIZEDCARAPACE},\n    },\n    UnitTypeId.LURKERDENMP: {\n        UpgradeId.DIGGINGCLAWS: {\"ability\": AbilityId.RESEARCH_ADAPTIVETALONS, \"required_building\": UnitTypeId.HIVE},\n        UpgradeId.LURKERRANGE: {\n            \"ability\": AbilityId.LURKERDENRESEARCH_RESEARCHLURKERRANGE,\n            \"required_building\": UnitTypeId.HIVE,\n        },\n    },\n    UnitTypeId.ROACHWARREN: {\n        UpgradeId.GLIALRECONSTITUTION: {\n            \"ability\": AbilityId.RESEARCH_GLIALREGENERATION,\n            \"required_building\": UnitTypeId.LAIR,\n        },\n        UpgradeId.TUNNELINGCLAWS: {\"ability\": AbilityId.RESEARCH_TUNNELINGCLAWS, \"required_building\": UnitTypeId.LAIR},\n    },\n    UnitTypeId.ROBOTICSBAY: {\n        UpgradeId.EXTENDEDTHERMALLANCE: {\"ability\": AbilityId.RESEARCH_EXTENDEDTHERMALLANCE, \"requires_power\": True},\n        UpgradeId.GRAVITICDRIVE: {\"ability\": AbilityId.RESEARCH_GRAVITICDRIVE, \"requires_power\": True},\n        UpgradeId.OBSERVERGRAVITICBOOSTER: {\"ability\": AbilityId.RESEARCH_GRAVITICBOOSTER, \"requires_power\": True},\n    },\n    UnitTypeId.SPAWNINGPOOL: {\n        UpgradeId.ZERGLINGATTACKSPEED: {\n            \"ability\": AbilityId.RESEARCH_ZERGLINGADRENALGLANDS,\n            \"required_building\": UnitTypeId.HIVE,\n        },\n        UpgradeId.ZERGLINGMOVEMENTSPEED: {\"ability\": AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST},\n    },\n    UnitTypeId.SPIRE: {\n        UpgradeId.ZERGFLYERARMORSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1},\n        UpgradeId.ZERGFLYERARMORSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGFLYERARMORSLEVEL1,\n        },\n        UpgradeId.ZERGFLYERARMORSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGFLYERARMORSLEVEL2,\n        },\n        UpgradeId.ZERGFLYERWEAPONSLEVEL1: {\"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1},\n        UpgradeId.ZERGFLYERWEAPONSLEVEL2: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2,\n            \"required_building\": UnitTypeId.LAIR,\n            \"required_upgrade\": UpgradeId.ZERGFLYERWEAPONSLEVEL1,\n        },\n        UpgradeId.ZERGFLYERWEAPONSLEVEL3: {\n            \"ability\": AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3,\n            \"required_building\": UnitTypeId.HIVE,\n            \"required_upgrade\": UpgradeId.ZERGFLYERWEAPONSLEVEL2,\n        },\n    },\n    UnitTypeId.STARPORTTECHLAB: {\n        UpgradeId.BANSHEECLOAK: {\"ability\": AbilityId.RESEARCH_BANSHEECLOAKINGFIELD},\n        UpgradeId.BANSHEESPEED: {\"ability\": AbilityId.RESEARCH_BANSHEEHYPERFLIGHTROTORS},\n        UpgradeId.INTERFERENCEMATRIX: {\"ability\": AbilityId.STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX},\n    },\n    UnitTypeId.TEMPLARARCHIVE: {\n        UpgradeId.PSISTORMTECH: {\"ability\": AbilityId.RESEARCH_PSISTORM, \"requires_power\": True}\n    },\n    UnitTypeId.TWILIGHTCOUNCIL: {\n        UpgradeId.ADEPTPIERCINGATTACK: {\"ability\": AbilityId.RESEARCH_ADEPTRESONATINGGLAIVES, \"requires_power\": True},\n        UpgradeId.BLINKTECH: {\"ability\": AbilityId.RESEARCH_BLINK, \"requires_power\": True},\n        UpgradeId.CHARGE: {\"ability\": AbilityId.RESEARCH_CHARGE, \"requires_power\": True},\n    },\n    UnitTypeId.ULTRALISKCAVERN: {\n        UpgradeId.ANABOLICSYNTHESIS: {\"ability\": AbilityId.RESEARCH_ANABOLICSYNTHESIS},\n        UpgradeId.CHITINOUSPLATING: {\"ability\": AbilityId.RESEARCH_CHITINOUSPLATING},\n    },\n}\n"
  },
  {
    "path": "sc2/dicts/unit_tech_alias.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.unit_typeid import UnitTypeId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nUNIT_TECH_ALIAS: dict[UnitTypeId, set[UnitTypeId]] = {\n    UnitTypeId.BARRACKSFLYING: {UnitTypeId.BARRACKS},\n    UnitTypeId.BARRACKSREACTOR: {UnitTypeId.REACTOR},\n    UnitTypeId.BARRACKSTECHLAB: {UnitTypeId.TECHLAB},\n    UnitTypeId.COMMANDCENTERFLYING: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.CREEPTUMORBURROWED: {UnitTypeId.CREEPTUMOR},\n    UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.CREEPTUMOR},\n    UnitTypeId.FACTORYFLYING: {UnitTypeId.FACTORY},\n    UnitTypeId.FACTORYREACTOR: {UnitTypeId.REACTOR},\n    UnitTypeId.FACTORYTECHLAB: {UnitTypeId.TECHLAB},\n    UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE},\n    UnitTypeId.HIVE: {UnitTypeId.HATCHERY, UnitTypeId.LAIR},\n    UnitTypeId.LAIR: {UnitTypeId.HATCHERY},\n    UnitTypeId.LIBERATORAG: {UnitTypeId.LIBERATOR},\n    UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.ORBITALCOMMANDFLYING: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD},\n    UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD},\n    UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.OVERLORD},\n    UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.PYLONOVERCHARGED: {UnitTypeId.PYLON},\n    UnitTypeId.QUEENBURROWED: {UnitTypeId.QUEEN},\n    UnitTypeId.SIEGETANKSIEGED: {UnitTypeId.SIEGETANK},\n    UnitTypeId.STARPORTFLYING: {UnitTypeId.STARPORT},\n    UnitTypeId.STARPORTREACTOR: {UnitTypeId.REACTOR},\n    UnitTypeId.STARPORTTECHLAB: {UnitTypeId.TECHLAB},\n    UnitTypeId.SUPPLYDEPOTLOWERED: {UnitTypeId.SUPPLYDEPOT},\n    UnitTypeId.THORAP: {UnitTypeId.THOR},\n    UnitTypeId.VIKINGASSAULT: {UnitTypeId.VIKING},\n    UnitTypeId.VIKINGFIGHTER: {UnitTypeId.VIKING},\n    UnitTypeId.WARPGATE: {UnitTypeId.GATEWAY},\n    UnitTypeId.WARPPRISMPHASING: {UnitTypeId.WARPPRISM},\n    UnitTypeId.WIDOWMINEBURROWED: {UnitTypeId.WIDOWMINE},\n}\n"
  },
  {
    "path": "sc2/dicts/unit_train_build_abilities.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\nfrom typing import Union\n\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\n\nTRAIN_INFO: dict[UnitTypeId, dict[UnitTypeId, dict[str, Union[AbilityId, bool, UnitTypeId]]]] = {\n    UnitTypeId.BARRACKS: {\n        UnitTypeId.GHOST: {\n            \"ability\": AbilityId.BARRACKSTRAIN_GHOST,\n            \"requires_techlab\": True,\n            \"required_building\": UnitTypeId.GHOSTACADEMY,\n        },\n        UnitTypeId.MARAUDER: {\"ability\": AbilityId.BARRACKSTRAIN_MARAUDER, \"requires_techlab\": True},\n        UnitTypeId.MARINE: {\"ability\": AbilityId.BARRACKSTRAIN_MARINE},\n        UnitTypeId.REAPER: {\"ability\": AbilityId.BARRACKSTRAIN_REAPER},\n    },\n    UnitTypeId.COMMANDCENTER: {\n        UnitTypeId.ORBITALCOMMAND: {\n            \"ability\": AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND,\n            \"required_building\": UnitTypeId.BARRACKS,\n        },\n        UnitTypeId.PLANETARYFORTRESS: {\n            \"ability\": AbilityId.UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS,\n            \"required_building\": UnitTypeId.ENGINEERINGBAY,\n        },\n        UnitTypeId.SCV: {\"ability\": AbilityId.COMMANDCENTERTRAIN_SCV},\n    },\n    UnitTypeId.CORRUPTOR: {\n        UnitTypeId.BROODLORD: {\n            \"ability\": AbilityId.MORPHTOBROODLORD_BROODLORD,\n            \"required_building\": UnitTypeId.GREATERSPIRE,\n        }\n    },\n    UnitTypeId.CREEPTUMOR: {\n        UnitTypeId.CREEPTUMOR: {\"ability\": AbilityId.BUILD_CREEPTUMOR_TUMOR, \"requires_placement_position\": True}\n    },\n    UnitTypeId.CREEPTUMORBURROWED: {\n        UnitTypeId.CREEPTUMOR: {\"ability\": AbilityId.BUILD_CREEPTUMOR, \"requires_placement_position\": True}\n    },\n    UnitTypeId.DRONE: {\n        UnitTypeId.BANELINGNEST: {\n            \"ability\": AbilityId.ZERGBUILD_BANELINGNEST,\n            \"required_building\": UnitTypeId.SPAWNINGPOOL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.EVOLUTIONCHAMBER: {\n            \"ability\": AbilityId.ZERGBUILD_EVOLUTIONCHAMBER,\n            \"required_building\": UnitTypeId.HATCHERY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.EXTRACTOR: {\"ability\": AbilityId.ZERGBUILD_EXTRACTOR},\n        UnitTypeId.HATCHERY: {\"ability\": AbilityId.ZERGBUILD_HATCHERY, \"requires_placement_position\": True},\n        UnitTypeId.HYDRALISKDEN: {\n            \"ability\": AbilityId.ZERGBUILD_HYDRALISKDEN,\n            \"required_building\": UnitTypeId.LAIR,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.INFESTATIONPIT: {\n            \"ability\": AbilityId.ZERGBUILD_INFESTATIONPIT,\n            \"required_building\": UnitTypeId.LAIR,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.LURKERDENMP: {\n            \"ability\": AbilityId.BUILD_LURKERDEN,\n            \"required_building\": UnitTypeId.HYDRALISKDEN,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.NYDUSNETWORK: {\n            \"ability\": AbilityId.ZERGBUILD_NYDUSNETWORK,\n            \"required_building\": UnitTypeId.LAIR,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.ROACHWARREN: {\n            \"ability\": AbilityId.ZERGBUILD_ROACHWARREN,\n            \"required_building\": UnitTypeId.SPAWNINGPOOL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SPAWNINGPOOL: {\n            \"ability\": AbilityId.ZERGBUILD_SPAWNINGPOOL,\n            \"required_building\": UnitTypeId.HATCHERY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SPINECRAWLER: {\n            \"ability\": AbilityId.ZERGBUILD_SPINECRAWLER,\n            \"required_building\": UnitTypeId.SPAWNINGPOOL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SPIRE: {\n            \"ability\": AbilityId.ZERGBUILD_SPIRE,\n            \"required_building\": UnitTypeId.LAIR,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SPORECRAWLER: {\n            \"ability\": AbilityId.ZERGBUILD_SPORECRAWLER,\n            \"required_building\": UnitTypeId.SPAWNINGPOOL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.ULTRALISKCAVERN: {\n            \"ability\": AbilityId.ZERGBUILD_ULTRALISKCAVERN,\n            \"required_building\": UnitTypeId.HIVE,\n            \"requires_placement_position\": True,\n        },\n    },\n    UnitTypeId.FACTORY: {\n        UnitTypeId.CYCLONE: {\"ability\": AbilityId.TRAIN_CYCLONE, \"requires_techlab\": True},\n        UnitTypeId.HELLION: {\"ability\": AbilityId.FACTORYTRAIN_HELLION},\n        UnitTypeId.HELLIONTANK: {\"ability\": AbilityId.TRAIN_HELLBAT, \"required_building\": UnitTypeId.ARMORY},\n        UnitTypeId.SIEGETANK: {\"ability\": AbilityId.FACTORYTRAIN_SIEGETANK, \"requires_techlab\": True},\n        UnitTypeId.THOR: {\n            \"ability\": AbilityId.FACTORYTRAIN_THOR,\n            \"requires_techlab\": True,\n            \"required_building\": UnitTypeId.ARMORY,\n        },\n        UnitTypeId.WIDOWMINE: {\"ability\": AbilityId.FACTORYTRAIN_WIDOWMINE},\n    },\n    UnitTypeId.GATEWAY: {\n        UnitTypeId.ADEPT: {\n            \"ability\": AbilityId.TRAIN_ADEPT,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_power\": True,\n        },\n        UnitTypeId.DARKTEMPLAR: {\n            \"ability\": AbilityId.GATEWAYTRAIN_DARKTEMPLAR,\n            \"required_building\": UnitTypeId.DARKSHRINE,\n            \"requires_power\": True,\n        },\n        UnitTypeId.HIGHTEMPLAR: {\n            \"ability\": AbilityId.GATEWAYTRAIN_HIGHTEMPLAR,\n            \"required_building\": UnitTypeId.TEMPLARARCHIVE,\n            \"requires_power\": True,\n        },\n        UnitTypeId.SENTRY: {\n            \"ability\": AbilityId.GATEWAYTRAIN_SENTRY,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_power\": True,\n        },\n        UnitTypeId.STALKER: {\n            \"ability\": AbilityId.GATEWAYTRAIN_STALKER,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_power\": True,\n        },\n        UnitTypeId.ZEALOT: {\"ability\": AbilityId.GATEWAYTRAIN_ZEALOT, \"requires_power\": True},\n    },\n    UnitTypeId.HATCHERY: {\n        UnitTypeId.LAIR: {\"ability\": AbilityId.UPGRADETOLAIR_LAIR, \"required_building\": UnitTypeId.SPAWNINGPOOL},\n        UnitTypeId.QUEEN: {\"ability\": AbilityId.TRAINQUEEN_QUEEN, \"required_building\": UnitTypeId.SPAWNINGPOOL},\n    },\n    UnitTypeId.HIVE: {\n        UnitTypeId.QUEEN: {\"ability\": AbilityId.TRAINQUEEN_QUEEN, \"required_building\": UnitTypeId.SPAWNINGPOOL}\n    },\n    UnitTypeId.HYDRALISK: {\n        UnitTypeId.LURKERMP: {\"ability\": AbilityId.MORPH_LURKER, \"required_building\": UnitTypeId.LURKERDENMP}\n    },\n    UnitTypeId.LAIR: {\n        UnitTypeId.HIVE: {\"ability\": AbilityId.UPGRADETOHIVE_HIVE, \"required_building\": UnitTypeId.INFESTATIONPIT},\n        UnitTypeId.QUEEN: {\"ability\": AbilityId.TRAINQUEEN_QUEEN, \"required_building\": UnitTypeId.SPAWNINGPOOL},\n    },\n    UnitTypeId.LARVA: {\n        UnitTypeId.CORRUPTOR: {\"ability\": AbilityId.LARVATRAIN_CORRUPTOR, \"required_building\": UnitTypeId.SPIRE},\n        UnitTypeId.DRONE: {\"ability\": AbilityId.LARVATRAIN_DRONE},\n        UnitTypeId.HYDRALISK: {\"ability\": AbilityId.LARVATRAIN_HYDRALISK, \"required_building\": UnitTypeId.HYDRALISKDEN},\n        UnitTypeId.INFESTOR: {\"ability\": AbilityId.LARVATRAIN_INFESTOR, \"required_building\": UnitTypeId.INFESTATIONPIT},\n        UnitTypeId.MUTALISK: {\"ability\": AbilityId.LARVATRAIN_MUTALISK, \"required_building\": UnitTypeId.SPIRE},\n        UnitTypeId.OVERLORD: {\"ability\": AbilityId.LARVATRAIN_OVERLORD},\n        UnitTypeId.ROACH: {\"ability\": AbilityId.LARVATRAIN_ROACH, \"required_building\": UnitTypeId.ROACHWARREN},\n        UnitTypeId.SWARMHOSTMP: {\"ability\": AbilityId.TRAIN_SWARMHOST, \"required_building\": UnitTypeId.INFESTATIONPIT},\n        UnitTypeId.ULTRALISK: {\n            \"ability\": AbilityId.LARVATRAIN_ULTRALISK,\n            \"required_building\": UnitTypeId.ULTRALISKCAVERN,\n        },\n        UnitTypeId.VIPER: {\"ability\": AbilityId.LARVATRAIN_VIPER, \"required_building\": UnitTypeId.HIVE},\n        UnitTypeId.ZERGLING: {\"ability\": AbilityId.LARVATRAIN_ZERGLING, \"required_building\": UnitTypeId.SPAWNINGPOOL},\n    },\n    UnitTypeId.NEXUS: {\n        UnitTypeId.MOTHERSHIP: {\n            \"ability\": AbilityId.NEXUSTRAINMOTHERSHIP_MOTHERSHIP,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n        },\n        UnitTypeId.PROBE: {\"ability\": AbilityId.NEXUSTRAIN_PROBE},\n    },\n    UnitTypeId.NYDUSNETWORK: {\n        UnitTypeId.NYDUSCANAL: {\"ability\": AbilityId.BUILD_NYDUSWORM, \"requires_placement_position\": True}\n    },\n    UnitTypeId.ORACLE: {\n        UnitTypeId.ORACLESTASISTRAP: {\"ability\": AbilityId.BUILD_STASISTRAP, \"requires_placement_position\": True}\n    },\n    UnitTypeId.ORBITALCOMMAND: {UnitTypeId.SCV: {\"ability\": AbilityId.COMMANDCENTERTRAIN_SCV}},\n    UnitTypeId.OVERLORD: {\n        UnitTypeId.OVERLORDTRANSPORT: {\n            \"ability\": AbilityId.MORPH_OVERLORDTRANSPORT,\n            \"required_building\": UnitTypeId.LAIR,\n        },\n        UnitTypeId.OVERSEER: {\"ability\": AbilityId.MORPH_OVERSEER, \"required_building\": UnitTypeId.LAIR},\n    },\n    UnitTypeId.OVERLORDTRANSPORT: {\n        UnitTypeId.OVERSEER: {\"ability\": AbilityId.MORPH_OVERSEER, \"required_building\": UnitTypeId.LAIR}\n    },\n    UnitTypeId.OVERSEER: {UnitTypeId.CHANGELING: {\"ability\": AbilityId.SPAWNCHANGELING_SPAWNCHANGELING}},\n    UnitTypeId.OVERSEERSIEGEMODE: {UnitTypeId.CHANGELING: {\"ability\": AbilityId.SPAWNCHANGELING_SPAWNCHANGELING}},\n    UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.SCV: {\"ability\": AbilityId.COMMANDCENTERTRAIN_SCV}},\n    UnitTypeId.PROBE: {\n        UnitTypeId.ASSIMILATOR: {\"ability\": AbilityId.PROTOSSBUILD_ASSIMILATOR},\n        UnitTypeId.CYBERNETICSCORE: {\n            \"ability\": AbilityId.PROTOSSBUILD_CYBERNETICSCORE,\n            \"required_building\": UnitTypeId.GATEWAY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.DARKSHRINE: {\n            \"ability\": AbilityId.PROTOSSBUILD_DARKSHRINE,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.FLEETBEACON: {\n            \"ability\": AbilityId.PROTOSSBUILD_FLEETBEACON,\n            \"required_building\": UnitTypeId.STARGATE,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.FORGE: {\n            \"ability\": AbilityId.PROTOSSBUILD_FORGE,\n            \"required_building\": UnitTypeId.PYLON,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.GATEWAY: {\n            \"ability\": AbilityId.PROTOSSBUILD_GATEWAY,\n            \"required_building\": UnitTypeId.PYLON,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.NEXUS: {\"ability\": AbilityId.PROTOSSBUILD_NEXUS, \"requires_placement_position\": True},\n        UnitTypeId.PHOTONCANNON: {\n            \"ability\": AbilityId.PROTOSSBUILD_PHOTONCANNON,\n            \"required_building\": UnitTypeId.FORGE,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.PYLON: {\"ability\": AbilityId.PROTOSSBUILD_PYLON, \"requires_placement_position\": True},\n        UnitTypeId.ROBOTICSBAY: {\n            \"ability\": AbilityId.PROTOSSBUILD_ROBOTICSBAY,\n            \"required_building\": UnitTypeId.ROBOTICSFACILITY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.ROBOTICSFACILITY: {\n            \"ability\": AbilityId.PROTOSSBUILD_ROBOTICSFACILITY,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SHIELDBATTERY: {\n            \"ability\": AbilityId.BUILD_SHIELDBATTERY,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.STARGATE: {\n            \"ability\": AbilityId.PROTOSSBUILD_STARGATE,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.TEMPLARARCHIVE: {\n            \"ability\": AbilityId.PROTOSSBUILD_TEMPLARARCHIVE,\n            \"required_building\": UnitTypeId.TWILIGHTCOUNCIL,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.TWILIGHTCOUNCIL: {\n            \"ability\": AbilityId.PROTOSSBUILD_TWILIGHTCOUNCIL,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n        },\n    },\n    UnitTypeId.QUEEN: {\n        UnitTypeId.CREEPTUMOR: {\"ability\": AbilityId.BUILD_CREEPTUMOR, \"requires_placement_position\": True},\n        UnitTypeId.CREEPTUMORQUEEN: {\"ability\": AbilityId.BUILD_CREEPTUMOR_QUEEN, \"requires_placement_position\": True},\n    },\n    UnitTypeId.RAVEN: {UnitTypeId.AUTOTURRET: {\"ability\": AbilityId.BUILDAUTOTURRET_AUTOTURRET}},\n    UnitTypeId.ROACH: {\n        UnitTypeId.RAVAGER: {\"ability\": AbilityId.MORPHTORAVAGER_RAVAGER, \"required_building\": UnitTypeId.HATCHERY}\n    },\n    UnitTypeId.ROBOTICSFACILITY: {\n        UnitTypeId.COLOSSUS: {\n            \"ability\": AbilityId.ROBOTICSFACILITYTRAIN_COLOSSUS,\n            \"required_building\": UnitTypeId.ROBOTICSBAY,\n            \"requires_power\": True,\n        },\n        UnitTypeId.DISRUPTOR: {\n            \"ability\": AbilityId.TRAIN_DISRUPTOR,\n            \"required_building\": UnitTypeId.ROBOTICSBAY,\n            \"requires_power\": True,\n        },\n        UnitTypeId.IMMORTAL: {\"ability\": AbilityId.ROBOTICSFACILITYTRAIN_IMMORTAL, \"requires_power\": True},\n        UnitTypeId.OBSERVER: {\"ability\": AbilityId.ROBOTICSFACILITYTRAIN_OBSERVER, \"requires_power\": True},\n        UnitTypeId.WARPPRISM: {\"ability\": AbilityId.ROBOTICSFACILITYTRAIN_WARPPRISM, \"requires_power\": True},\n    },\n    UnitTypeId.SCV: {\n        UnitTypeId.ARMORY: {\n            \"ability\": AbilityId.TERRANBUILD_ARMORY,\n            \"required_building\": UnitTypeId.FACTORY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.BARRACKS: {\n            \"ability\": AbilityId.TERRANBUILD_BARRACKS,\n            \"required_building\": UnitTypeId.SUPPLYDEPOT,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.BUNKER: {\n            \"ability\": AbilityId.TERRANBUILD_BUNKER,\n            \"required_building\": UnitTypeId.BARRACKS,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.COMMANDCENTER: {\"ability\": AbilityId.TERRANBUILD_COMMANDCENTER, \"requires_placement_position\": True},\n        UnitTypeId.ENGINEERINGBAY: {\n            \"ability\": AbilityId.TERRANBUILD_ENGINEERINGBAY,\n            \"required_building\": UnitTypeId.COMMANDCENTER,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.FACTORY: {\n            \"ability\": AbilityId.TERRANBUILD_FACTORY,\n            \"required_building\": UnitTypeId.BARRACKS,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.FUSIONCORE: {\n            \"ability\": AbilityId.TERRANBUILD_FUSIONCORE,\n            \"required_building\": UnitTypeId.STARPORT,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.GHOSTACADEMY: {\n            \"ability\": AbilityId.TERRANBUILD_GHOSTACADEMY,\n            \"required_building\": UnitTypeId.BARRACKS,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.MISSILETURRET: {\n            \"ability\": AbilityId.TERRANBUILD_MISSILETURRET,\n            \"required_building\": UnitTypeId.ENGINEERINGBAY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.REFINERY: {\"ability\": AbilityId.TERRANBUILD_REFINERY},\n        UnitTypeId.SENSORTOWER: {\n            \"ability\": AbilityId.TERRANBUILD_SENSORTOWER,\n            \"required_building\": UnitTypeId.ENGINEERINGBAY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.STARPORT: {\n            \"ability\": AbilityId.TERRANBUILD_STARPORT,\n            \"required_building\": UnitTypeId.FACTORY,\n            \"requires_placement_position\": True,\n        },\n        UnitTypeId.SUPPLYDEPOT: {\"ability\": AbilityId.TERRANBUILD_SUPPLYDEPOT, \"requires_placement_position\": True},\n    },\n    UnitTypeId.SPIRE: {\n        UnitTypeId.GREATERSPIRE: {\n            \"ability\": AbilityId.UPGRADETOGREATERSPIRE_GREATERSPIRE,\n            \"required_building\": UnitTypeId.HIVE,\n        }\n    },\n    UnitTypeId.STARGATE: {\n        UnitTypeId.CARRIER: {\n            \"ability\": AbilityId.STARGATETRAIN_CARRIER,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"requires_power\": True,\n        },\n        UnitTypeId.ORACLE: {\"ability\": AbilityId.STARGATETRAIN_ORACLE, \"requires_power\": True},\n        UnitTypeId.PHOENIX: {\"ability\": AbilityId.STARGATETRAIN_PHOENIX, \"requires_power\": True},\n        UnitTypeId.TEMPEST: {\n            \"ability\": AbilityId.STARGATETRAIN_TEMPEST,\n            \"required_building\": UnitTypeId.FLEETBEACON,\n            \"requires_power\": True,\n        },\n        UnitTypeId.VOIDRAY: {\"ability\": AbilityId.STARGATETRAIN_VOIDRAY, \"requires_power\": True},\n    },\n    UnitTypeId.STARPORT: {\n        UnitTypeId.BANSHEE: {\"ability\": AbilityId.STARPORTTRAIN_BANSHEE, \"requires_techlab\": True},\n        UnitTypeId.BATTLECRUISER: {\n            \"ability\": AbilityId.STARPORTTRAIN_BATTLECRUISER,\n            \"requires_techlab\": True,\n            \"required_building\": UnitTypeId.FUSIONCORE,\n        },\n        UnitTypeId.LIBERATOR: {\"ability\": AbilityId.STARPORTTRAIN_LIBERATOR},\n        UnitTypeId.MEDIVAC: {\"ability\": AbilityId.STARPORTTRAIN_MEDIVAC},\n        UnitTypeId.RAVEN: {\"ability\": AbilityId.STARPORTTRAIN_RAVEN, \"requires_techlab\": True},\n        UnitTypeId.VIKINGFIGHTER: {\"ability\": AbilityId.STARPORTTRAIN_VIKINGFIGHTER},\n    },\n    UnitTypeId.SWARMHOSTBURROWEDMP: {UnitTypeId.LOCUSTMPFLYING: {\"ability\": AbilityId.EFFECT_SPAWNLOCUSTS}},\n    UnitTypeId.SWARMHOSTMP: {UnitTypeId.LOCUSTMPFLYING: {\"ability\": AbilityId.EFFECT_SPAWNLOCUSTS}},\n    UnitTypeId.WARPGATE: {\n        UnitTypeId.ADEPT: {\n            \"ability\": AbilityId.TRAINWARP_ADEPT,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n        UnitTypeId.DARKTEMPLAR: {\n            \"ability\": AbilityId.WARPGATETRAIN_DARKTEMPLAR,\n            \"required_building\": UnitTypeId.DARKSHRINE,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n        UnitTypeId.HIGHTEMPLAR: {\n            \"ability\": AbilityId.WARPGATETRAIN_HIGHTEMPLAR,\n            \"required_building\": UnitTypeId.TEMPLARARCHIVE,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n        UnitTypeId.SENTRY: {\n            \"ability\": AbilityId.WARPGATETRAIN_SENTRY,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n        UnitTypeId.STALKER: {\n            \"ability\": AbilityId.WARPGATETRAIN_STALKER,\n            \"required_building\": UnitTypeId.CYBERNETICSCORE,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n        UnitTypeId.ZEALOT: {\n            \"ability\": AbilityId.WARPGATETRAIN_ZEALOT,\n            \"requires_placement_position\": True,\n            \"requires_power\": True,\n        },\n    },\n    UnitTypeId.ZERGLING: {\n        UnitTypeId.BANELING: {\n            \"ability\": AbilityId.MORPHTOBANELING_BANELING,\n            \"required_building\": UnitTypeId.BANELINGNEST,\n        }\n    },\n}\n"
  },
  {
    "path": "sc2/dicts/unit_trained_from.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.unit_typeid import UnitTypeId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nUNIT_TRAINED_FROM: dict[UnitTypeId, set[UnitTypeId]] = {\n    UnitTypeId.ADEPT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.ARMORY: {UnitTypeId.SCV},\n    UnitTypeId.ASSIMILATOR: {UnitTypeId.PROBE},\n    UnitTypeId.AUTOTURRET: {UnitTypeId.RAVEN},\n    UnitTypeId.BANELING: {UnitTypeId.ZERGLING},\n    UnitTypeId.BANELINGNEST: {UnitTypeId.DRONE},\n    UnitTypeId.BANSHEE: {UnitTypeId.STARPORT},\n    UnitTypeId.BARRACKS: {UnitTypeId.SCV},\n    UnitTypeId.BATTLECRUISER: {UnitTypeId.STARPORT},\n    UnitTypeId.BROODLORD: {UnitTypeId.CORRUPTOR},\n    UnitTypeId.BUNKER: {UnitTypeId.SCV},\n    UnitTypeId.CARRIER: {UnitTypeId.STARGATE},\n    UnitTypeId.CHANGELING: {UnitTypeId.OVERSEER, UnitTypeId.OVERSEERSIEGEMODE},\n    UnitTypeId.COLOSSUS: {UnitTypeId.ROBOTICSFACILITY},\n    UnitTypeId.COMMANDCENTER: {UnitTypeId.SCV},\n    UnitTypeId.CORRUPTOR: {UnitTypeId.LARVA},\n    UnitTypeId.CREEPTUMOR: {UnitTypeId.CREEPTUMORBURROWED, UnitTypeId.QUEEN},\n    UnitTypeId.CREEPTUMORQUEEN: {UnitTypeId.QUEEN},\n    UnitTypeId.CYBERNETICSCORE: {UnitTypeId.PROBE},\n    UnitTypeId.CYCLONE: {UnitTypeId.FACTORY},\n    UnitTypeId.DARKSHRINE: {UnitTypeId.PROBE},\n    UnitTypeId.DARKTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.DISRUPTOR: {UnitTypeId.ROBOTICSFACILITY},\n    UnitTypeId.DRONE: {UnitTypeId.LARVA},\n    UnitTypeId.ENGINEERINGBAY: {UnitTypeId.SCV},\n    UnitTypeId.EVOLUTIONCHAMBER: {UnitTypeId.DRONE},\n    UnitTypeId.EXTRACTOR: {UnitTypeId.DRONE},\n    UnitTypeId.FACTORY: {UnitTypeId.SCV},\n    UnitTypeId.FLEETBEACON: {UnitTypeId.PROBE},\n    UnitTypeId.FORGE: {UnitTypeId.PROBE},\n    UnitTypeId.FUSIONCORE: {UnitTypeId.SCV},\n    UnitTypeId.GATEWAY: {UnitTypeId.PROBE},\n    UnitTypeId.GHOST: {UnitTypeId.BARRACKS},\n    UnitTypeId.GHOSTACADEMY: {UnitTypeId.SCV},\n    UnitTypeId.GREATERSPIRE: {UnitTypeId.SPIRE},\n    UnitTypeId.HATCHERY: {UnitTypeId.DRONE},\n    UnitTypeId.HELLION: {UnitTypeId.FACTORY},\n    UnitTypeId.HELLIONTANK: {UnitTypeId.FACTORY},\n    UnitTypeId.HIGHTEMPLAR: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.HIVE: {UnitTypeId.LAIR},\n    UnitTypeId.HYDRALISK: {UnitTypeId.LARVA},\n    UnitTypeId.HYDRALISKDEN: {UnitTypeId.DRONE},\n    UnitTypeId.IMMORTAL: {UnitTypeId.ROBOTICSFACILITY},\n    UnitTypeId.INFESTATIONPIT: {UnitTypeId.DRONE},\n    UnitTypeId.INFESTOR: {UnitTypeId.LARVA},\n    UnitTypeId.LAIR: {UnitTypeId.HATCHERY},\n    UnitTypeId.LIBERATOR: {UnitTypeId.STARPORT},\n    UnitTypeId.LOCUSTMPFLYING: {UnitTypeId.SWARMHOSTBURROWEDMP, UnitTypeId.SWARMHOSTMP},\n    UnitTypeId.LURKERDENMP: {UnitTypeId.DRONE},\n    UnitTypeId.LURKERMP: {UnitTypeId.HYDRALISK},\n    UnitTypeId.MARAUDER: {UnitTypeId.BARRACKS},\n    UnitTypeId.MARINE: {UnitTypeId.BARRACKS},\n    UnitTypeId.MEDIVAC: {UnitTypeId.STARPORT},\n    UnitTypeId.MISSILETURRET: {UnitTypeId.SCV},\n    UnitTypeId.MOTHERSHIP: {UnitTypeId.NEXUS},\n    UnitTypeId.MUTALISK: {UnitTypeId.LARVA},\n    UnitTypeId.NEXUS: {UnitTypeId.PROBE},\n    UnitTypeId.NYDUSCANAL: {UnitTypeId.NYDUSNETWORK},\n    UnitTypeId.NYDUSNETWORK: {UnitTypeId.DRONE},\n    UnitTypeId.OBSERVER: {UnitTypeId.ROBOTICSFACILITY},\n    UnitTypeId.ORACLE: {UnitTypeId.STARGATE},\n    UnitTypeId.ORACLESTASISTRAP: {UnitTypeId.ORACLE},\n    UnitTypeId.ORBITALCOMMAND: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.OVERLORD: {UnitTypeId.LARVA},\n    UnitTypeId.OVERLORDTRANSPORT: {UnitTypeId.OVERLORD},\n    UnitTypeId.OVERSEER: {UnitTypeId.OVERLORD, UnitTypeId.OVERLORDTRANSPORT},\n    UnitTypeId.PHOENIX: {UnitTypeId.STARGATE},\n    UnitTypeId.PHOTONCANNON: {UnitTypeId.PROBE},\n    UnitTypeId.PLANETARYFORTRESS: {UnitTypeId.COMMANDCENTER},\n    UnitTypeId.PROBE: {UnitTypeId.NEXUS},\n    UnitTypeId.PYLON: {UnitTypeId.PROBE},\n    UnitTypeId.QUEEN: {UnitTypeId.HATCHERY, UnitTypeId.HIVE, UnitTypeId.LAIR},\n    UnitTypeId.RAVAGER: {UnitTypeId.ROACH},\n    UnitTypeId.RAVEN: {UnitTypeId.STARPORT},\n    UnitTypeId.REAPER: {UnitTypeId.BARRACKS},\n    UnitTypeId.REFINERY: {UnitTypeId.SCV},\n    UnitTypeId.ROACH: {UnitTypeId.LARVA},\n    UnitTypeId.ROACHWARREN: {UnitTypeId.DRONE},\n    UnitTypeId.ROBOTICSBAY: {UnitTypeId.PROBE},\n    UnitTypeId.ROBOTICSFACILITY: {UnitTypeId.PROBE},\n    UnitTypeId.SCV: {UnitTypeId.COMMANDCENTER, UnitTypeId.ORBITALCOMMAND, UnitTypeId.PLANETARYFORTRESS},\n    UnitTypeId.SENSORTOWER: {UnitTypeId.SCV},\n    UnitTypeId.SENTRY: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.SHIELDBATTERY: {UnitTypeId.PROBE},\n    UnitTypeId.SIEGETANK: {UnitTypeId.FACTORY},\n    UnitTypeId.SPAWNINGPOOL: {UnitTypeId.DRONE},\n    UnitTypeId.SPINECRAWLER: {UnitTypeId.DRONE},\n    UnitTypeId.SPIRE: {UnitTypeId.DRONE},\n    UnitTypeId.SPORECRAWLER: {UnitTypeId.DRONE},\n    UnitTypeId.STALKER: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.STARGATE: {UnitTypeId.PROBE},\n    UnitTypeId.STARPORT: {UnitTypeId.SCV},\n    UnitTypeId.SUPPLYDEPOT: {UnitTypeId.SCV},\n    UnitTypeId.SWARMHOSTMP: {UnitTypeId.LARVA},\n    UnitTypeId.TEMPEST: {UnitTypeId.STARGATE},\n    UnitTypeId.TEMPLARARCHIVE: {UnitTypeId.PROBE},\n    UnitTypeId.THOR: {UnitTypeId.FACTORY},\n    UnitTypeId.TWILIGHTCOUNCIL: {UnitTypeId.PROBE},\n    UnitTypeId.ULTRALISK: {UnitTypeId.LARVA},\n    UnitTypeId.ULTRALISKCAVERN: {UnitTypeId.DRONE},\n    UnitTypeId.VIKINGFIGHTER: {UnitTypeId.STARPORT},\n    UnitTypeId.VIPER: {UnitTypeId.LARVA},\n    UnitTypeId.VOIDRAY: {UnitTypeId.STARGATE},\n    UnitTypeId.WARPPRISM: {UnitTypeId.ROBOTICSFACILITY},\n    UnitTypeId.WIDOWMINE: {UnitTypeId.FACTORY},\n    UnitTypeId.ZEALOT: {UnitTypeId.GATEWAY, UnitTypeId.WARPGATE},\n    UnitTypeId.ZERGLING: {UnitTypeId.LARVA},\n}\n"
  },
  {
    "path": "sc2/dicts/unit_unit_alias.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.unit_typeid import UnitTypeId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nUNIT_UNIT_ALIAS: dict[UnitTypeId, UnitTypeId] = {\n    UnitTypeId.ADEPTPHASESHIFT: UnitTypeId.ADEPT,\n    UnitTypeId.BANELINGBURROWED: UnitTypeId.BANELING,\n    UnitTypeId.BARRACKSFLYING: UnitTypeId.BARRACKS,\n    UnitTypeId.CHANGELINGMARINE: UnitTypeId.CHANGELING,\n    UnitTypeId.CHANGELINGMARINESHIELD: UnitTypeId.CHANGELING,\n    UnitTypeId.CHANGELINGZEALOT: UnitTypeId.CHANGELING,\n    UnitTypeId.CHANGELINGZERGLING: UnitTypeId.CHANGELING,\n    UnitTypeId.CHANGELINGZERGLINGWINGS: UnitTypeId.CHANGELING,\n    UnitTypeId.COMMANDCENTERFLYING: UnitTypeId.COMMANDCENTER,\n    UnitTypeId.CREEPTUMORBURROWED: UnitTypeId.CREEPTUMOR,\n    UnitTypeId.CREEPTUMORQUEEN: UnitTypeId.CREEPTUMOR,\n    UnitTypeId.DRONEBURROWED: UnitTypeId.DRONE,\n    UnitTypeId.FACTORYFLYING: UnitTypeId.FACTORY,\n    UnitTypeId.GHOSTNOVA: UnitTypeId.GHOST,\n    UnitTypeId.HERCPLACEMENT: UnitTypeId.HERC,\n    UnitTypeId.HYDRALISKBURROWED: UnitTypeId.HYDRALISK,\n    UnitTypeId.INFESTORBURROWED: UnitTypeId.INFESTOR,\n    UnitTypeId.INFESTORTERRANBURROWED: UnitTypeId.INFESTORTERRAN,\n    UnitTypeId.LIBERATORAG: UnitTypeId.LIBERATOR,\n    UnitTypeId.LOCUSTMPFLYING: UnitTypeId.LOCUSTMP,\n    UnitTypeId.LURKERMPBURROWED: UnitTypeId.LURKERMP,\n    UnitTypeId.OBSERVERSIEGEMODE: UnitTypeId.OBSERVER,\n    UnitTypeId.ORBITALCOMMANDFLYING: UnitTypeId.ORBITALCOMMAND,\n    UnitTypeId.OVERSEERSIEGEMODE: UnitTypeId.OVERSEER,\n    UnitTypeId.PYLONOVERCHARGED: UnitTypeId.PYLON,\n    UnitTypeId.QUEENBURROWED: UnitTypeId.QUEEN,\n    UnitTypeId.RAVAGERBURROWED: UnitTypeId.RAVAGER,\n    UnitTypeId.ROACHBURROWED: UnitTypeId.ROACH,\n    UnitTypeId.SIEGETANKSIEGED: UnitTypeId.SIEGETANK,\n    UnitTypeId.SPINECRAWLERUPROOTED: UnitTypeId.SPINECRAWLER,\n    UnitTypeId.SPORECRAWLERUPROOTED: UnitTypeId.SPORECRAWLER,\n    UnitTypeId.STARPORTFLYING: UnitTypeId.STARPORT,\n    UnitTypeId.SUPPLYDEPOTLOWERED: UnitTypeId.SUPPLYDEPOT,\n    UnitTypeId.SWARMHOSTBURROWEDMP: UnitTypeId.SWARMHOSTMP,\n    UnitTypeId.THORAP: UnitTypeId.THOR,\n    UnitTypeId.ULTRALISKBURROWED: UnitTypeId.ULTRALISK,\n    UnitTypeId.VIKINGASSAULT: UnitTypeId.VIKINGFIGHTER,\n    UnitTypeId.WARPPRISMPHASING: UnitTypeId.WARPPRISM,\n    UnitTypeId.WIDOWMINEBURROWED: UnitTypeId.WIDOWMINE,\n    UnitTypeId.ZERGLINGBURROWED: UnitTypeId.ZERGLING,\n}\n"
  },
  {
    "path": "sc2/dicts/upgrade_researched_from.py",
    "content": "# THIS FILE WAS AUTOMATICALLY GENERATED BY \"generate_dicts_from_data_json.py\" DO NOT CHANGE MANUALLY!\n# ANY CHANGE WILL BE OVERWRITTEN\n\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\n\n# from sc2.ids.buff_id import BuffId\n# from sc2.ids.effect_id import EffectId\n\n\nUPGRADE_RESEARCHED_FROM: dict[UpgradeId, UnitTypeId] = {\n    UpgradeId.ADEPTPIERCINGATTACK: UnitTypeId.TWILIGHTCOUNCIL,\n    UpgradeId.ANABOLICSYNTHESIS: UnitTypeId.ULTRALISKCAVERN,\n    UpgradeId.BANSHEECLOAK: UnitTypeId.STARPORTTECHLAB,\n    UpgradeId.BANSHEESPEED: UnitTypeId.STARPORTTECHLAB,\n    UpgradeId.BATTLECRUISERENABLESPECIALIZATIONS: UnitTypeId.FUSIONCORE,\n    UpgradeId.BLINKTECH: UnitTypeId.TWILIGHTCOUNCIL,\n    UpgradeId.BURROW: UnitTypeId.HATCHERY,\n    UpgradeId.CENTRIFICALHOOKS: UnitTypeId.BANELINGNEST,\n    UpgradeId.CHARGE: UnitTypeId.TWILIGHTCOUNCIL,\n    UpgradeId.CHITINOUSPLATING: UnitTypeId.ULTRALISKCAVERN,\n    UpgradeId.CYCLONELOCKONDAMAGEUPGRADE: UnitTypeId.FACTORYTECHLAB,\n    UpgradeId.DARKTEMPLARBLINKUPGRADE: UnitTypeId.DARKSHRINE,\n    UpgradeId.DIGGINGCLAWS: UnitTypeId.LURKERDENMP,\n    UpgradeId.DRILLCLAWS: UnitTypeId.FACTORYTECHLAB,\n    UpgradeId.EVOLVEGROOVEDSPINES: UnitTypeId.HYDRALISKDEN,\n    UpgradeId.EVOLVEMUSCULARAUGMENTS: UnitTypeId.HYDRALISKDEN,\n    UpgradeId.EXTENDEDTHERMALLANCE: UnitTypeId.ROBOTICSBAY,\n    UpgradeId.FRENZY: UnitTypeId.HYDRALISKDEN,\n    UpgradeId.GLIALRECONSTITUTION: UnitTypeId.ROACHWARREN,\n    UpgradeId.GRAVITICDRIVE: UnitTypeId.ROBOTICSBAY,\n    UpgradeId.HIGHCAPACITYBARRELS: UnitTypeId.FACTORYTECHLAB,\n    UpgradeId.HISECAUTOTRACKING: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.INTERFERENCEMATRIX: UnitTypeId.STARPORTTECHLAB,\n    UpgradeId.LIBERATORAGRANGEUPGRADE: UnitTypeId.FUSIONCORE,\n    UpgradeId.LURKERRANGE: UnitTypeId.LURKERDENMP,\n    UpgradeId.MEDIVACCADUCEUSREACTOR: UnitTypeId.FUSIONCORE,\n    UpgradeId.NEURALPARASITE: UnitTypeId.INFESTATIONPIT,\n    UpgradeId.OBSERVERGRAVITICBOOSTER: UnitTypeId.ROBOTICSBAY,\n    UpgradeId.OVERLORDSPEED: UnitTypeId.HATCHERY,\n    UpgradeId.PERSONALCLOAKING: UnitTypeId.GHOSTACADEMY,\n    UpgradeId.PHOENIXRANGEUPGRADE: UnitTypeId.FLEETBEACON,\n    UpgradeId.PROTOSSAIRARMORSLEVEL1: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSAIRARMORSLEVEL2: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSAIRARMORSLEVEL3: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSAIRWEAPONSLEVEL1: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSAIRWEAPONSLEVEL2: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSAIRWEAPONSLEVEL3: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.PROTOSSGROUNDARMORSLEVEL1: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSGROUNDARMORSLEVEL2: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSGROUNDARMORSLEVEL3: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSSHIELDSLEVEL1: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSSHIELDSLEVEL2: UnitTypeId.FORGE,\n    UpgradeId.PROTOSSSHIELDSLEVEL3: UnitTypeId.FORGE,\n    UpgradeId.PSISTORMTECH: UnitTypeId.TEMPLARARCHIVE,\n    UpgradeId.PUNISHERGRENADES: UnitTypeId.BARRACKSTECHLAB,\n    UpgradeId.SHIELDWALL: UnitTypeId.BARRACKSTECHLAB,\n    UpgradeId.SMARTSERVOS: UnitTypeId.FACTORYTECHLAB,\n    UpgradeId.STIMPACK: UnitTypeId.BARRACKSTECHLAB,\n    UpgradeId.TEMPESTGROUNDATTACKUPGRADE: UnitTypeId.FLEETBEACON,\n    UpgradeId.TERRANBUILDINGARMOR: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYARMORSLEVEL1: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYARMORSLEVEL2: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYARMORSLEVEL3: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYWEAPONSLEVEL1: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYWEAPONSLEVEL2: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANINFANTRYWEAPONSLEVEL3: UnitTypeId.ENGINEERINGBAY,\n    UpgradeId.TERRANSHIPWEAPONSLEVEL1: UnitTypeId.ARMORY,\n    UpgradeId.TERRANSHIPWEAPONSLEVEL2: UnitTypeId.ARMORY,\n    UpgradeId.TERRANSHIPWEAPONSLEVEL3: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL1: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL2: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEANDSHIPARMORSLEVEL3: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEWEAPONSLEVEL1: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEWEAPONSLEVEL2: UnitTypeId.ARMORY,\n    UpgradeId.TERRANVEHICLEWEAPONSLEVEL3: UnitTypeId.ARMORY,\n    UpgradeId.TUNNELINGCLAWS: UnitTypeId.ROACHWARREN,\n    UpgradeId.VOIDRAYSPEEDUPGRADE: UnitTypeId.FLEETBEACON,\n    UpgradeId.WARPGATERESEARCH: UnitTypeId.CYBERNETICSCORE,\n    UpgradeId.ZERGFLYERARMORSLEVEL1: UnitTypeId.SPIRE,\n    UpgradeId.ZERGFLYERARMORSLEVEL2: UnitTypeId.SPIRE,\n    UpgradeId.ZERGFLYERARMORSLEVEL3: UnitTypeId.SPIRE,\n    UpgradeId.ZERGFLYERWEAPONSLEVEL1: UnitTypeId.SPIRE,\n    UpgradeId.ZERGFLYERWEAPONSLEVEL2: UnitTypeId.SPIRE,\n    UpgradeId.ZERGFLYERWEAPONSLEVEL3: UnitTypeId.SPIRE,\n    UpgradeId.ZERGGROUNDARMORSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGGROUNDARMORSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGGROUNDARMORSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGLINGATTACKSPEED: UnitTypeId.SPAWNINGPOOL,\n    UpgradeId.ZERGLINGMOVEMENTSPEED: UnitTypeId.SPAWNINGPOOL,\n    UpgradeId.ZERGMELEEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGMELEEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGMELEEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGMISSILEWEAPONSLEVEL1: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGMISSILEWEAPONSLEVEL2: UnitTypeId.EVOLUTIONCHAMBER,\n    UpgradeId.ZERGMISSILEWEAPONSLEVEL3: UnitTypeId.EVOLUTIONCHAMBER,\n}\n"
  },
  {
    "path": "sc2/expiring_dict.py",
    "content": "from __future__ import annotations\n\nfrom collections import OrderedDict\nfrom collections.abc import Hashable, Iterable\nfrom threading import RLock\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:\n    from sc2.bot_ai import BotAI\n\n\nclass ExpiringDict(OrderedDict[Hashable, Any]):\n    \"\"\"\n    An expiring dict that uses the bot.state.game_loop to only return items that are valid for a specific amount of time.\n\n    Example usages::\n\n        async def on_step(iteration: int):\n            # This dict will hold up to 10 items and only return values that have been added up to 20 frames ago\n            my_dict = ExpiringDict(self, max_age_frames=20)\n            if iteration == 0:\n                # Add item\n                my_dict[\"test\"] = \"something\"\n            if iteration == 2:\n                # On default, one iteration is called every 8 frames\n                if \"test\" in my_dict:\n                    print(\"test is in dict\")\n            if iteration == 20:\n                if \"test\" not in my_dict:\n                    print(\"test is not anymore in dict\")\n    \"\"\"\n\n    def __init__(self, bot: BotAI, max_age_frames: int = 1) -> None:\n        assert max_age_frames >= -1\n        assert bot\n\n        OrderedDict.__init__(self)\n        self.bot: BotAI = bot\n        self.max_age: int | float = max_age_frames\n        self.lock: RLock = RLock()\n\n    @property\n    def frame(self) -> int:\n        return self.bot.state.game_loop\n\n    def __contains__(self, key: Hashable) -> bool:\n        \"\"\"Return True if dict has key, else False, e.g. 'key in dict'\"\"\"\n        with self.lock:\n            if OrderedDict.__contains__(self, key):\n                # Each item is a list of [value, frame time]\n                item = OrderedDict.__getitem__(self, key)\n                if self.frame - item[1] < self.max_age:\n                    return True\n                del self[key]\n        return False\n\n    def __getitem__(self, key: Hashable, with_age: bool = False) -> Any:\n        \"\"\"Return the item of the dict using d[key]\"\"\"\n        with self.lock:\n            # Each item is a list of [value, frame time]\n            item = OrderedDict.__getitem__(self, key)\n            if self.frame - item[1] < self.max_age:\n                if with_age:\n                    return item[0], item[1]\n                return item[0]\n            OrderedDict.__delitem__(self, key)\n        raise KeyError(key)\n\n    def __setitem__(self, key: Hashable, value: Any) -> None:\n        \"\"\"Set d[key] = value\"\"\"\n        with self.lock:\n            OrderedDict.__setitem__(self, key, (value, self.frame))\n\n    def __repr__(self) -> str:\n        \"\"\"Printable version of the dict instead of getting memory adress\"\"\"\n        print_list: list[str] = []\n        with self.lock:\n            for key, value in OrderedDict.items(self):\n                if self.frame - value[1] < self.max_age:\n                    print_list.append(f\"{repr(key)}: {repr(value)}\")\n        print_str = \", \".join(print_list)\n        return f\"ExpiringDict({print_str})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def __iter__(self) -> Iterable[Hashable]:\n        \"\"\"Override 'for key in dict:'\"\"\"\n        with self.lock:\n            return self.keys()\n\n    # TODO find a way to improve len\n    def __len__(self) -> int:\n        \"\"\"Override len method as key value pairs aren't instantly being deleted, but only on __get__(item).\n        This function is slow because it has to check if each element is not expired yet.\"\"\"\n        with self.lock:\n            count = 0\n            for _ in self.values():\n                count += 1\n            return count\n\n    def pop(self, key: Hashable, default: Any = None, with_age: bool = False):\n        \"\"\"Return the item and remove it\"\"\"\n        with self.lock:\n            if OrderedDict.__contains__(self, key):\n                item = OrderedDict.__getitem__(self, key)\n                if self.frame - item[1] < self.max_age:\n                    del self[key]\n                    if with_age:\n                        return item[0], item[1]\n                    return item[0]\n                del self[key]\n            if default is None:\n                raise KeyError(key)\n            if with_age:\n                return default, self.frame\n            return default\n\n    def get(self, key: Hashable, default: Any = None, with_age: bool = False):\n        \"\"\"Return the value for key if key is in dict, else default\"\"\"\n        with self.lock:\n            if OrderedDict.__contains__(self, key):\n                item = OrderedDict.__getitem__(self, key)\n                if self.frame - item[1] < self.max_age:\n                    if with_age:\n                        return item[0], item[1]\n                    return item[0]\n            if default is None:\n                raise KeyError(key)\n            if with_age:\n                return default, self.frame\n            return None\n        return None\n\n    def update(self, other_dict: dict[Hashable, Any]) -> None:\n        with self.lock:\n            for key, value in other_dict.items():\n                self[key] = value\n\n    def items(self) -> Iterable[tuple[Hashable, Any]]:\n        \"\"\"Return iterator of zipped list [keys, values]\"\"\"\n        with self.lock:\n            for key, value in OrderedDict.items(self):\n                if self.frame - value[1] < self.max_age:\n                    yield key, value[0]\n\n    def keys(self) -> Iterable[Hashable]:\n        \"\"\"Return iterator of keys\"\"\"\n        with self.lock:\n            for key, value in OrderedDict.items(self):\n                if self.frame - value[1] < self.max_age:\n                    yield key\n\n    def values(self) -> Iterable[Any]:\n        \"\"\"Return iterator of values\"\"\"\n        with self.lock:\n            for value in OrderedDict.values(self):\n                if self.frame - value[1] < self.max_age:\n                    yield value[0]\n"
  },
  {
    "path": "sc2/game_data.py",
    "content": "from __future__ import annotations\n\nfrom bisect import bisect_left\nfrom contextlib import suppress\nfrom dataclasses import dataclass\nfrom functools import lru_cache\n\nfrom s2clientprotocol import data_pb2, sc2api_pb2\nfrom sc2.data import Attribute, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.unit_command import UnitCommand\n\nwith suppress(ImportError):\n    from sc2.dicts.unit_trained_from import UNIT_TRAINED_FROM\n\n# Set of parts of names of abilities that have no cost\n# E.g every ability that has 'Hold' in its name is free\nFREE_ABILITIES = {\"Lower\", \"Raise\", \"Land\", \"Lift\", \"Hold\", \"Harvest\"}\n\n\nclass GameData:\n    def __init__(self, data: sc2api_pb2.ResponseData) -> None:\n        \"\"\"\n        :param data:\n        \"\"\"\n        ids = {a.value for a in AbilityId if a.value != 0}\n        self.abilities: dict[int, AbilityData] = {\n            a.ability_id: AbilityData(self, a) for a in data.abilities if a.ability_id in ids\n        }\n        self.units: dict[int, UnitTypeData] = {u.unit_id: UnitTypeData(self, u) for u in data.units if u.available}\n        self.upgrades: dict[int, UpgradeData] = {u.upgrade_id: UpgradeData(self, u) for u in data.upgrades}\n        # 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\n\n    @lru_cache(maxsize=256)\n    def calculate_ability_cost(self, ability: AbilityData | AbilityId | UnitCommand) -> Cost:\n        if isinstance(ability, AbilityId):\n            ability = self.abilities[ability.value]\n        elif isinstance(ability, UnitCommand):\n            ability = self.abilities[ability.ability.value]\n\n        assert isinstance(ability, AbilityData), f\"Ability is not of type 'AbilityData', but was {type(ability)}\"\n\n        for unit in self.units.values():\n            if unit.creation_ability is None:\n                continue\n\n            if not AbilityData.id_exists(unit.creation_ability.id.value):\n                continue\n\n            if unit.creation_ability.is_free_morph:\n                continue\n\n            if unit.creation_ability == ability:\n                if unit.id == UnitTypeId.ZERGLING:\n                    # HARD CODED: zerglings are generated in pairs\n                    return Cost(unit.cost.minerals * 2, unit.cost.vespene * 2, unit.cost.time)\n                if unit.id == UnitTypeId.BANELING:\n                    # HARD CODED: banelings don't cost 50/25 as described in the API, but 25/25\n                    return Cost(25, 25, unit.cost.time)\n                # Correction for morphing units, e.g. orbital would return 550/0 instead of actual 150/0\n                morph_cost = unit.morph_cost\n                if morph_cost:  # can be None\n                    return morph_cost\n                # Correction for zerg structures without morph: Extractor would return 75 instead of actual 25\n                return unit.cost_zerg_corrected\n\n        for upgrade in self.upgrades.values():\n            if upgrade.research_ability == ability:\n                return upgrade.cost\n\n        return Cost(0, 0)\n\n\nclass AbilityData:\n    ability_ids: list[int] = [ability_id.value for ability_id in AbilityId][1:]  # sorted list\n\n    @classmethod\n    def id_exists(cls, ability_id: int) -> bool:\n        assert isinstance(ability_id, int), f\"Wrong type: {ability_id} is not int\"\n        if ability_id == 0:\n            return False\n        i = bisect_left(cls.ability_ids, ability_id)  # quick binary search\n        return i != len(cls.ability_ids) and cls.ability_ids[i] == ability_id\n\n    def __init__(self, game_data: GameData, proto: data_pb2.AbilityData) -> None:\n        self._game_data = game_data\n        self._proto = proto\n\n        # What happens if we comment this out? Should this not be commented out? What is its purpose?\n        assert self.id != 0\n\n    def __repr__(self) -> str:\n        return f\"AbilityData(name={self._proto.button_name})\"\n\n    @property\n    def id(self) -> AbilityId:\n        \"\"\"Returns the generic remap ID. See sc2/dicts/generic_redirect_abilities.py\"\"\"\n        if self._proto.remaps_to_ability_id:\n            return AbilityId(self._proto.remaps_to_ability_id)\n        return AbilityId(self._proto.ability_id)\n\n    @property\n    def exact_id(self) -> AbilityId:\n        \"\"\"Returns the exact ID of the ability\"\"\"\n        return AbilityId(self._proto.ability_id)\n\n    @property\n    def link_name(self) -> str:\n        \"\"\"For Stimpack this returns 'BarracksTechLabResearch'\"\"\"\n        return self._proto.link_name\n\n    @property\n    def button_name(self) -> str:\n        \"\"\"For Stimpack this returns 'Stimpack'\"\"\"\n        return self._proto.button_name\n\n    @property\n    def friendly_name(self) -> str:\n        \"\"\"For Stimpack this returns 'Research Stimpack'\"\"\"\n        return self._proto.friendly_name\n\n    @property\n    def is_free_morph(self) -> bool:\n        return any(free in self._proto.link_name for free in FREE_ABILITIES)\n\n    @property\n    def cost(self) -> Cost:\n        return self._game_data.calculate_ability_cost(self.id)\n\n\nclass UnitTypeData:\n    def __init__(self, game_data: GameData, proto: data_pb2.UnitTypeData) -> None:\n        \"\"\"\n        :param game_data:\n        :param proto:\n        \"\"\"\n        # The ability_id for lurkers is\n        # LURKERASPECTMPFROMHYDRALISKBURROWED_LURKERMPFROMHYDRALISKBURROWED\n        # instead of the correct MORPH_LURKER.\n        if proto.unit_id == UnitTypeId.LURKERMP.value:\n            proto.ability_id = AbilityId.MORPH_LURKER.value\n\n        self._game_data = game_data\n        self._proto = proto\n\n    def __repr__(self) -> str:\n        return f\"UnitTypeData(name={self.name})\"\n\n    @property\n    def id(self) -> UnitTypeId:\n        return UnitTypeId(self._proto.unit_id)\n\n    @property\n    def name(self) -> str:\n        return self._proto.name\n\n    @property\n    def creation_ability(self) -> AbilityData | None:\n        if self._proto.ability_id == 0:\n            return None\n        if self._proto.ability_id not in self._game_data.abilities:\n            return None\n        return self._game_data.abilities[self._proto.ability_id]\n\n    @property\n    def footprint_radius(self) -> float | None:\n        \"\"\"See unit.py footprint_radius\"\"\"\n        if self.creation_ability is None:\n            return None\n        return self.creation_ability._proto.footprint_radius\n\n    @property\n    def attributes(self) -> list[Attribute]:\n        return [Attribute(i) for i in self._proto.attributes]\n\n    def has_attribute(self, attr: Attribute) -> bool:\n        assert isinstance(attr, Attribute)\n        return attr in self.attributes\n\n    @property\n    def has_minerals(self) -> bool:\n        return self._proto.has_minerals\n\n    @property\n    def has_vespene(self) -> bool:\n        return self._proto.has_vespene\n\n    @property\n    def cargo_size(self) -> int:\n        \"\"\"How much cargo this unit uses up in cargo_space\"\"\"\n        return self._proto.cargo_size\n\n    @property\n    def tech_requirement(self) -> UnitTypeId | None:\n        \"\"\"Tech-building requirement of buildings - may work for units but unreliably\"\"\"\n        if self._proto.tech_requirement == 0:\n            return None\n        if self._proto.tech_requirement not in self._game_data.units:\n            return None\n        return UnitTypeId(self._proto.tech_requirement)\n\n    @property\n    def tech_alias(self) -> list[UnitTypeId] | None:\n        \"\"\"Building tech equality, e.g. OrbitalCommand is the same as CommandCenter\n        Building tech equality, e.g. Hive is the same as Lair and Hatchery\n        For Hive, this returns [UnitTypeId.Hatchery, UnitTypeId.Lair]\n        For SCV, this returns None\"\"\"\n        return_list = [\n            UnitTypeId(tech_alias) for tech_alias in self._proto.tech_alias if tech_alias in self._game_data.units\n        ]\n        return return_list if return_list else None\n\n    @property\n    def unit_alias(self) -> UnitTypeId | None:\n        \"\"\"Building type equality, e.g. FlyingOrbitalCommand is the same as OrbitalCommand\"\"\"\n        if self._proto.unit_alias == 0:\n            return None\n        if self._proto.unit_alias not in self._game_data.units:\n            return None\n        \"\"\" For flying OrbitalCommand, this returns UnitTypeId.OrbitalCommand \"\"\"\n        return UnitTypeId(self._proto.unit_alias)\n\n    @property\n    def race(self) -> Race:\n        return Race(self._proto.race)\n\n    @property\n    def cost(self) -> Cost:\n        return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.build_time)\n\n    @property\n    def cost_zerg_corrected(self) -> Cost:\n        \"\"\"This returns 25 for extractor and 200 for spawning pool instead of 75 and 250 respectively\"\"\"\n        if self.race == Race.Zerg and Attribute.Structure.value in self._proto.attributes:\n            return Cost(self._proto.mineral_cost - 50, self._proto.vespene_cost, self._proto.build_time)\n        return self.cost\n\n    @property\n    def morph_cost(self) -> Cost | None:\n        \"\"\"This returns 150 minerals for OrbitalCommand instead of 550\"\"\"\n        # Morphing units\n        supply_cost = self._proto.food_required\n        if supply_cost > 0 and self.id in UNIT_TRAINED_FROM and len(UNIT_TRAINED_FROM[self.id]) == 1:\n            producer: UnitTypeId\n            for producer in UNIT_TRAINED_FROM[self.id]:\n                producer_unit_data = self._game_data.units[producer.value]\n                if 0 < producer_unit_data._proto.food_required <= supply_cost:\n                    if producer == UnitTypeId.ZERGLING:\n                        producer_cost = Cost(25, 0)\n                    else:\n                        producer_cost = self._game_data.calculate_ability_cost(producer_unit_data.creation_ability)\n                    return Cost(\n                        self._proto.mineral_cost - producer_cost.minerals,\n                        self._proto.vespene_cost - producer_cost.vespene,\n                        self._proto.build_time,\n                    )\n        # Fix for BARRACKSREACTOR which has tech alias [REACTOR] which has (0, 0) cost\n        if self.tech_alias is None or self.tech_alias[0] in {UnitTypeId.TECHLAB, UnitTypeId.REACTOR}:\n            return None\n        # Morphing a HIVE would have HATCHERY and LAIR in the tech alias - now subtract HIVE cost from LAIR cost instead of from HATCHERY cost\n        tech_alias_cost_minerals = max(\n            self._game_data.units[tech_alias.value].cost.minerals for tech_alias in self.tech_alias\n        )\n        tech_alias_cost_vespene = max(\n            self._game_data.units[tech_alias.value].cost.vespene for tech_alias in self.tech_alias\n        )\n        return Cost(\n            self._proto.mineral_cost - tech_alias_cost_minerals,\n            self._proto.vespene_cost - tech_alias_cost_vespene,\n            self._proto.build_time,\n        )\n\n\nclass UpgradeData:\n    def __init__(self, game_data: GameData, proto: data_pb2.UpgradeData) -> None:\n        \"\"\"\n        :param game_data:\n        :param proto:\n        \"\"\"\n        self._game_data = game_data\n        self._proto = proto\n\n    def __repr__(self) -> str:\n        return f\"UpgradeData({self.name} - research ability: {self.research_ability}, {self.cost})\"\n\n    @property\n    def name(self) -> str:\n        return self._proto.name\n\n    @property\n    def research_ability(self) -> AbilityData | None:\n        if self._proto.ability_id == 0:\n            return None\n        if self._proto.ability_id not in self._game_data.abilities:\n            return None\n        return self._game_data.abilities[self._proto.ability_id]\n\n    @property\n    def cost(self) -> Cost:\n        return Cost(self._proto.mineral_cost, self._proto.vespene_cost, self._proto.research_time)\n\n\n@dataclass\nclass Cost:\n    \"\"\"\n    The cost of an action, a structure, a unit or a research upgrade.\n    The time is given in frames (22.4 frames per game second).\n    \"\"\"\n\n    minerals: int\n    vespene: int\n    time: float | None = None\n\n    def __repr__(self) -> str:\n        return f\"Cost({self.minerals}, {self.vespene})\"\n\n    def __eq__(self, other: Cost) -> bool:\n        return self.minerals == other.minerals and self.vespene == other.vespene\n\n    def __ne__(self, other: Cost) -> bool:\n        return self.minerals != other.minerals or self.vespene != other.vespene\n\n    def __bool__(self) -> bool:\n        return self.minerals != 0 or self.vespene != 0\n\n    def __add__(self, other: Cost) -> Cost:\n        if not other:\n            return self\n        if not self:\n            return other\n        time = (self.time or 0) + (other.time or 0)\n        return Cost(self.minerals + other.minerals, self.vespene + other.vespene, time=time)\n\n    def __sub__(self, other: Cost) -> Cost:\n        time = (self.time or 0) + (other.time or 0)\n        return Cost(self.minerals - other.minerals, self.vespene - other.vespene, time=time)\n\n    def __mul__(self, other: int) -> Cost:\n        return Cost(self.minerals * other, self.vespene * other, time=self.time)\n\n    def __rmul__(self, other: int) -> Cost:\n        return Cost(self.minerals * other, self.vespene * other, time=self.time)\n"
  },
  {
    "path": "sc2/game_info.py",
    "content": "from __future__ import annotations\n\nimport heapq\nfrom collections import deque\nfrom collections.abc import Iterable\nfrom dataclasses import dataclass\nfrom functools import cached_property\n\nimport numpy as np\n\nfrom s2clientprotocol import sc2api_pb2\nfrom sc2.pixel_map import PixelMap\nfrom sc2.player import Player\nfrom sc2.position import Point2, Rect, Size\n\n\n@dataclass\nclass Ramp:\n    points: frozenset[Point2]\n    game_info: GameInfo\n\n    @property\n    def x_offset(self) -> float:\n        # Tested by printing actual building locations vs calculated depot positions\n        return 0.5\n\n    @property\n    def y_offset(self) -> float:\n        # Tested by printing actual building locations vs calculated depot positions\n        return 0.5\n\n    @cached_property\n    def _height_map(self) -> PixelMap:\n        return self.game_info.terrain_height\n\n    @cached_property\n    def size(self) -> int:\n        return len(self.points)\n\n    def height_at(self, p: Point2) -> int:\n        return self._height_map[p]\n\n    @cached_property\n    def upper(self) -> frozenset[Point2]:\n        \"\"\"Returns the upper points of a ramp.\"\"\"\n        current_max = -10000\n        result = set()\n        for p in self.points:\n            height = self.height_at(p)\n            if height > current_max:\n                current_max = height\n                result = {p}\n            elif height == current_max:\n                result.add(p)\n        return frozenset(result)\n\n    @cached_property\n    def upper2_for_ramp_wall(self) -> frozenset[Point2]:\n        \"\"\"Returns the 2 upper ramp points of the main base ramp required for the supply depot and barracks placement properties used in this file.\"\"\"\n        # From bottom center, find 2 points that are furthest away (within the same ramp)\n        return frozenset(heapq.nlargest(2, self.upper, key=lambda x: x.distance_to_point2(self.bottom_center)))\n\n    @cached_property\n    def top_center(self) -> Point2:\n        length = len(self.upper)\n        pos = Point2((sum(p.x for p in self.upper) / length, sum(p.y for p in self.upper) / length))\n        return pos\n\n    @cached_property\n    def lower(self) -> frozenset[Point2]:\n        current_min = 10000\n        result = set()\n        for p in self.points:\n            height = self.height_at(p)\n            if height < current_min:\n                current_min = height\n                result = {p}\n            elif height == current_min:\n                result.add(p)\n        return frozenset(result)\n\n    @cached_property\n    def bottom_center(self) -> Point2:\n        length = len(self.lower)\n        pos = Point2((sum(p.x for p in self.lower) / length, sum(p.y for p in self.lower) / length))\n        return pos\n\n    @cached_property\n    def barracks_in_middle(self) -> Point2 | None:\n        \"\"\"Barracks position in the middle of the 2 depots\"\"\"\n        if len(self.upper) not in {2, 5}:\n            return None\n        if len(self.upper2_for_ramp_wall) == 2:\n            points = set(self.upper2_for_ramp_wall)\n            p1 = points.pop().offset((self.x_offset, self.y_offset))\n            p2 = points.pop().offset((self.x_offset, self.y_offset))\n            # Offset from top point to barracks center is (2, 1)\n            intersects = p1.circle_intersection(p2, 5**0.5)\n            any_lower_point = next(iter(self.lower))\n            return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point))\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def depot_in_middle(self) -> Point2 | None:\n        \"\"\"Depot in the middle of the 3 depots\"\"\"\n        if len(self.upper) not in {2, 5}:\n            return None\n        if len(self.upper2_for_ramp_wall) == 2:\n            points = set(self.upper2_for_ramp_wall)\n            p1 = points.pop().offset((self.x_offset, self.y_offset))\n            p2 = points.pop().offset((self.x_offset, self.y_offset))\n            # Offset from top point to depot center is (1.5, 0.5)\n            try:\n                intersects = p1.circle_intersection(p2, 2.5**0.5)\n            except AssertionError:\n                # Returns None when no placement was found, this is the case on the map Honorgrounds LE with an exceptionally large main base ramp\n                return None\n            any_lower_point = next(iter(self.lower))\n            return max(intersects, key=lambda p: p.distance_to_point2(any_lower_point))\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def corner_depots(self) -> set[Point2]:\n        \"\"\"Finds the 2 depot positions on the outside\"\"\"\n        if not self.upper2_for_ramp_wall:\n            return set()\n        if len(self.upper2_for_ramp_wall) == 2:\n            points = set(self.upper2_for_ramp_wall)\n            p1 = points.pop().offset((self.x_offset, self.y_offset))\n            p2 = points.pop().offset((self.x_offset, self.y_offset))\n            center = p1.towards(p2, p1.distance_to_point2(p2) / 2)\n            depot_position = self.depot_in_middle\n            if depot_position is None:\n                return set()\n            # Offset from middle depot to corner depots is (2, 1)\n            intersects = center.circle_intersection(depot_position, 5**0.5)\n            return intersects\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def barracks_can_fit_addon(self) -> bool:\n        \"\"\"Test if a barracks can fit an addon at natural ramp\"\"\"\n        # https://i.imgur.com/4b2cXHZ.png\n        if len(self.upper2_for_ramp_wall) == 2:\n            # pyrefly: ignore\n            return self.barracks_in_middle.x + 1 > max(self.corner_depots, key=lambda depot: depot.x).x\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def barracks_correct_placement(self) -> Point2 | None:\n        \"\"\"Corrected placement so that an addon can fit\"\"\"\n        if self.barracks_in_middle is None:\n            return None\n        if len(self.upper2_for_ramp_wall) == 2:\n            if self.barracks_can_fit_addon:\n                return self.barracks_in_middle\n            return self.barracks_in_middle.offset((-2, 0))\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def protoss_wall_pylon(self) -> Point2 | None:\n        \"\"\"\n        Pylon position that powers the two wall buildings and the warpin position.\n        \"\"\"\n        if len(self.upper) not in {2, 5}:\n            return None\n        if len(self.upper2_for_ramp_wall) != 2:\n            raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n        middle = self.depot_in_middle\n        # direction up the ramp\n        # pyrefly: ignore\n        direction = self.barracks_in_middle.negative_offset(middle)\n\n        return middle + 6 * direction\n\n    @cached_property\n    def protoss_wall_buildings(self) -> frozenset[Point2]:\n        \"\"\"\n        List of two positions for 3x3 buildings that form a wall with a spot for a one unit block.\n        These buildings can be powered by a pylon on the protoss_wall_pylon position.\n        \"\"\"\n        if len(self.upper) not in {2, 5}:\n            return frozenset()\n        if len(self.upper2_for_ramp_wall) == 2:\n            middle = self.depot_in_middle\n            # direction up the ramp\n            # pyrefly: ignore\n            direction = self.barracks_in_middle.negative_offset(middle)\n            # sort depots based on distance to start to get wallin orientation\n            sorted_depots = sorted(\n                self.corner_depots, key=lambda depot: depot.distance_to(self.game_info.player_start_location)\n            )\n            wall1: Point2 = sorted_depots[1].offset(direction)\n            # pyrefly: ignore\n            wall2 = middle + direction + (middle - wall1) / 1.5\n            return frozenset([wall1, wall2])\n\n        raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n\n    @cached_property\n    def protoss_wall_warpin(self) -> Point2 | None:\n        \"\"\"\n        Position for a unit to block the wall created by protoss_wall_buildings.\n        Powered by protoss_wall_pylon.\n        \"\"\"\n        if len(self.upper) not in {2, 5}:\n            return None\n        if len(self.upper2_for_ramp_wall) != 2:\n            raise Exception(\"Not implemented. Trying to access a ramp that has a wrong amount of upper points.\")\n        middle = self.depot_in_middle\n        # direction up the ramp\n        # pyrefly: ignore\n        direction = self.barracks_in_middle.negative_offset(middle)\n        # sort depots based on distance to start to get wallin orientation\n        sorted_depots = sorted(self.corner_depots, key=lambda x: x.distance_to(self.game_info.player_start_location))\n        return sorted_depots[0].negative_offset(direction)\n\n\nclass GameInfo:\n    def __init__(self, proto: sc2api_pb2.ResponseGameInfo) -> None:\n        self._proto = proto\n        self.players: list[Player] = [Player.from_proto(p) for p in self._proto.player_info]\n        self.map_name: str = self._proto.map_name\n        self.local_map_path: str = self._proto.local_map_path\n\n        self.map_size: Size = Size.from_proto(self._proto.start_raw.map_size)\n\n        # self.pathing_grid[point]: if 0, point is not pathable, if 1, point is pathable\n        self.pathing_grid: PixelMap = PixelMap(self._proto.start_raw.pathing_grid, in_bits=True)\n        # self.terrain_height[point]: returns the height in range of 0 to 255 at that point\n        self.terrain_height: PixelMap = PixelMap(self._proto.start_raw.terrain_height)\n        # self.placement_grid[point]: if 0, point is not placeable, if 1, point is pathable\n        self.placement_grid: PixelMap = PixelMap(self._proto.start_raw.placement_grid, in_bits=True)\n        self.playable_area = Rect.from_proto(self._proto.start_raw.playable_area)\n        self.map_center = self.playable_area.center\n        # pyrefly: ignore\n        self.map_ramps: list[Ramp] = None  # Filled later by BotAI._prepare_first_step\n        # pyrefly: ignore\n        self.vision_blockers: frozenset[Point2] = None  # Filled later by BotAI._prepare_first_step\n        self.player_races: dict[int, int] = {\n            p.player_id: p.race_actual or p.race_requested for p in self._proto.player_info\n        }\n        self.start_locations: list[Point2] = [\n            Point2.from_proto(sl).round(decimals=1) for sl in self._proto.start_raw.start_locations\n        ]\n        # pyrefly: ignore\n        self.player_start_location: Point2 = None  # Filled later by BotAI._prepare_first_step\n\n    def _find_ramps_and_vision_blockers(self) -> tuple[list[Ramp], frozenset[Point2]]:\n        \"\"\"Calculate points that are pathable but not placeable.\n        Then divide them into ramp points if not all points around the points are equal height\n        and into vision blockers if they are.\"\"\"\n\n        def equal_height_around(tile):\n            # mask to slice array 1 around tile\n            sliced = self.terrain_height.data_numpy[tile[1] - 1 : tile[1] + 2, tile[0] - 1 : tile[0] + 2]\n            return len(np.unique(sliced)) == 1\n\n        map_area = self.playable_area\n        # all points in the playable area that are pathable but not placable\n        points = [\n            Point2((a, b))\n            for (b, a), value in np.ndenumerate(self.pathing_grid.data_numpy)\n            if value == 1\n            and map_area.x <= a < map_area.x + map_area.width\n            and map_area.y <= b < map_area.y + map_area.height\n            and self.placement_grid[(a, b)] == 0\n        ]\n        # divide points into ramp points and vision blockers\n        ramp_points = [point for point in points if not equal_height_around(point)]\n        vision_blockers = frozenset(point for point in points if equal_height_around(point))\n        ramps = [Ramp(frozenset(group), self) for group in self._find_groups(ramp_points)]\n        return ramps, vision_blockers\n\n    def _find_groups(self, points: Iterable[Point2], minimum_points_per_group: int = 8) -> Iterable[frozenset[Point2]]:\n        \"\"\"\n        From a set of points, this function will try to group points together by\n        painting clusters of points in a rectangular map using flood fill algorithm.\n        Returns groups of points as list, like [{p1, p2, p3}, {p4, p5, p6, p7, p8}]\n        \"\"\"\n        # TODO do we actually need colors here? the ramps will never touch anyways.\n        NOT_COLORED_YET = -1\n        map_width = self.pathing_grid.width\n        map_height = self.pathing_grid.height\n        current_color: int = NOT_COLORED_YET\n        picture: list[list[int]] = [[-2 for _ in range(map_width)] for _ in range(map_height)]\n\n        def paint(pt: Point2) -> None:\n            # pyrefly: ignore\n            picture[pt.y][pt.x] = current_color\n\n        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]\n\n        remaining: set[Point2] = set(points)\n        for point in remaining:\n            paint(point)\n        current_color = 1\n        queue: deque[Point2] = deque()\n        while remaining:\n            current_group: set[Point2] = set()\n            if not queue:\n                start = remaining.pop()\n                paint(start)\n                queue.append(start)\n                current_group.add(start)\n            while queue:\n                base: Point2 = queue.popleft()\n                for offset in nearby:\n                    px, py = base.x + offset[0], base.y + offset[1]\n                    # Do we ever reach out of map bounds?\n                    if not (0 <= px < map_width and 0 <= py < map_height):\n                        continue\n                    # pyrefly: ignore\n                    if picture[py][px] != NOT_COLORED_YET:\n                        continue\n                    point: Point2 = Point2((px, py))\n                    remaining.discard(point)\n                    paint(point)\n                    queue.append(point)\n                    current_group.add(point)\n            if len(current_group) >= minimum_points_per_group:\n                yield frozenset(current_group)\n"
  },
  {
    "path": "sc2/game_state.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom itertools import chain\nfrom s2clientprotocol.raw_pb2 import Effect, Unit\n\nfrom loguru import logger\n\nfrom s2clientprotocol import raw_pb2, sc2api_pb2\nfrom sc2.constants import IS_ENEMY, IS_MINE, FakeEffectID, FakeEffectRadii\nfrom sc2.data import Alliance, DisplayType\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.effect_id import EffectId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.pixel_map import PixelMap\nfrom sc2.position import Point2, Point3\nfrom sc2.power_source import PsionicMatrix\nfrom sc2.score import ScoreDetails\n\ntry:\n    from sc2.dicts.generic_redirect_abilities import GENERIC_REDIRECT_ABILITIES\nexcept ImportError:\n    logger.info('Unable to import \"GENERIC_REDIRECT_ABILITIES\"')\n    GENERIC_REDIRECT_ABILITIES = {}\n\n\nclass Blip:\n    def __init__(self, proto: raw_pb2.Unit) -> None:\n        \"\"\"\n        :param proto:\n        \"\"\"\n        self._proto = proto\n\n    @property\n    def is_blip(self) -> bool:\n        \"\"\"Detected by sensor tower.\"\"\"\n        return self._proto.is_blip\n\n    @property\n    def is_snapshot(self) -> bool:\n        return self._proto.display_type == DisplayType.Snapshot.value\n\n    @property\n    def is_visible(self) -> bool:\n        return self._proto.display_type == DisplayType.Visible.value\n\n    @property\n    def alliance(self) -> int:\n        return self._proto.alliance\n\n    @property\n    def is_mine(self) -> bool:\n        return self._proto.alliance == Alliance.Self.value\n\n    @property\n    def is_enemy(self) -> bool:\n        return self._proto.alliance == Alliance.Enemy.value\n\n    @property\n    def position(self) -> Point2:\n        \"\"\"2d position of the blip.\"\"\"\n        return Point2.from_proto(self._proto.pos)\n\n    @property\n    def position3d(self) -> Point3:\n        \"\"\"3d position of the blip.\"\"\"\n        return Point3.from_proto(self._proto.pos)\n\n\nclass Common:\n    ATTRIBUTES = [\n        \"player_id\",\n        \"minerals\",\n        \"vespene\",\n        \"food_cap\",\n        \"food_used\",\n        \"food_army\",\n        \"food_workers\",\n        \"idle_worker_count\",\n        \"army_count\",\n        \"warp_gate_count\",\n        \"larva_count\",\n    ]\n\n    def __init__(self, proto: sc2api_pb2.PlayerCommon) -> None:\n        self._proto = proto\n\n    def __getattr__(self, attr) -> int:\n        assert attr in self.ATTRIBUTES, f\"'{attr}' is not a valid attribute\"\n        return int(getattr(self._proto, attr))\n\n\nclass EffectData:\n    def __init__(self, proto: raw_pb2.Effect | raw_pb2.Unit, fake: bool = False) -> None:\n        \"\"\"\n        :param proto:\n        :param fake:\n        \"\"\"\n        self._proto: Effect | Unit = proto\n        self.fake = fake\n\n    @property\n    def id(self) -> EffectId | str:\n        if isinstance(self._proto, raw_pb2.Unit):\n            # Returns the string from constants.py, e.g. \"KD8CHARGE\"\n            return FakeEffectID[self._proto.unit_type]\n        return EffectId(self._proto.effect_id)\n\n    @property\n    def positions(self) -> set[Point2]:\n        if isinstance(self._proto, raw_pb2.Unit):\n            return {Point2.from_proto(self._proto.pos)}\n        return {Point2.from_proto(p) for p in self._proto.pos}\n\n    @property\n    def alliance(self) -> Alliance:\n        return Alliance(self._proto.alliance)\n\n    @property\n    def is_mine(self) -> bool:\n        \"\"\"Checks if the effect is caused by me.\"\"\"\n        return self._proto.alliance == IS_MINE\n\n    @property\n    def is_enemy(self) -> bool:\n        \"\"\"Checks if the effect is hostile.\"\"\"\n        return self._proto.alliance == IS_ENEMY\n\n    @property\n    def owner(self) -> int:\n        return self._proto.owner\n\n    @property\n    def radius(self) -> float:\n        if isinstance(self._proto, Unit):\n            return FakeEffectRadii[self._proto.unit_type]\n        return self._proto.radius\n\n    def __repr__(self) -> str:\n        return f\"{self.id} with radius {self.radius} at {self.positions}\"\n\n\n@dataclass\nclass ChatMessage:\n    player_id: int\n    message: str\n\n\n@dataclass\nclass AbilityLookupTemplateClass:\n    ability_id: int\n\n    @property\n    def exact_id(self) -> AbilityId:\n        return AbilityId(self.ability_id)\n\n    @property\n    def generic_id(self) -> AbilityId:\n        \"\"\"\n        See https://github.com/BurnySc2/python-sc2/blob/511c34f6b7ae51bd11e06ba91b6a9624dc04a0c0/sc2/dicts/generic_redirect_abilities.py#L13\n        \"\"\"\n        return GENERIC_REDIRECT_ABILITIES.get(self.exact_id, self.exact_id)\n\n\n@dataclass\nclass ActionRawUnitCommand(AbilityLookupTemplateClass):\n    game_loop: int\n    unit_tags: list[int]\n    queue_command: bool\n    target_world_space_pos: Point2 | None\n    target_unit_tag: int | None = None\n\n\n@dataclass\nclass ActionRawToggleAutocast(AbilityLookupTemplateClass):\n    game_loop: int\n    unit_tags: list[int]\n\n\n@dataclass\nclass ActionRawCameraMove:\n    center_world_space: Point2\n\n\n@dataclass\nclass ActionError(AbilityLookupTemplateClass):\n    unit_tag: int\n    # See here for the codes of 'result': https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/error.proto#L6\n    result: int\n\n\nclass GameState:\n    def __init__(\n        self,\n        response_observation: sc2api_pb2.ResponseObservation,\n        previous_observation: sc2api_pb2.ResponseObservation | None = None,\n    ) -> None:\n        \"\"\"\n        :param response_observation:\n        :param previous_observation:\n        \"\"\"\n        # Only filled in realtime=True in case the bot skips frames\n        self.previous_observation = previous_observation\n        self.response_observation = response_observation\n\n        # https://github.com/Blizzard/s2client-proto/blob/51662231c0965eba47d5183ed0a6336d5ae6b640/s2clientprotocol/sc2api.proto#L575\n        self.observation = response_observation.observation\n        self.observation_raw = self.observation.raw_data\n        self.player_result = response_observation.player_result\n        self.common: Common = Common(self.observation.player_common)\n\n        # Area covered by Pylons and Warpprisms\n        self.psionic_matrix: PsionicMatrix = PsionicMatrix.from_proto(list(self.observation_raw.player.power_sources))\n        # 22.4 per second on faster game speed\n        self.game_loop: int = self.observation.game_loop\n\n        # https://github.com/Blizzard/s2client-proto/blob/33f0ecf615aa06ca845ffe4739ef3133f37265a9/s2clientprotocol/score.proto#L31\n        self.score: ScoreDetails = ScoreDetails(self.observation.score)\n        self.abilities = self.observation.abilities  # abilities of selected units\n        self.upgrades: set[UpgradeId] = {UpgradeId(upgrade) for upgrade in self.observation_raw.player.upgrade_ids}\n\n        # self.visibility[point]: 0=Hidden, 1=Fogged, 2=Visible\n        self.visibility: PixelMap = PixelMap(self.observation_raw.map_state.visibility)\n        # self.creep[point]: 0=No creep, 1=creep\n        self.creep: PixelMap = PixelMap(self.observation_raw.map_state.creep, in_bits=True)\n\n        # Effects like ravager bile shot, lurker attack, everything in effect_id.py\n        self.effects: set[EffectData] = {EffectData(effect) for effect in self.observation_raw.effects}\n        \"\"\" Usage:\n        for effect in self.state.effects:\n            if effect.id == EffectId.RAVAGERCORROSIVEBILECP:\n                positions = effect.positions\n                # dodge the ravager biles\n        \"\"\"\n\n    @cached_property\n    def dead_units(self) -> set[int]:\n        \"\"\"A set of unit tags that died this frame\"\"\"\n        _dead_units = set(self.observation_raw.event.dead_units)\n        if self.previous_observation:\n            return _dead_units | set(self.previous_observation.observation.raw_data.event.dead_units)\n        return _dead_units\n\n    @cached_property\n    def chat(self) -> list[ChatMessage]:\n        \"\"\"List of chat messages sent this frame (by either player).\"\"\"\n        previous_frame_chat = self.previous_observation.chat if self.previous_observation else []\n        return [\n            ChatMessage(message.player_id, message.message)\n            for message in chain(previous_frame_chat, self.response_observation.chat)\n        ]\n\n    @cached_property\n    def alerts(self) -> list[int]:\n        \"\"\"\n        Game alerts, see https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L683-L706\n        \"\"\"\n        if self.previous_observation is not None:\n            return list(chain(self.previous_observation.observation.alerts, self.observation.alerts))\n        return list(self.observation.alerts)\n\n    @cached_property\n    def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | ActionRawCameraMove]:\n        \"\"\"\n        List of successful actions since last frame.\n        See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L630-L637\n\n        Each action is converted into Python dataclasses: ActionRawUnitCommand, ActionRawToggleAutocast, ActionRawCameraMove\n        \"\"\"\n        previous_frame_actions = self.previous_observation.actions if self.previous_observation else []\n        actions: list[ActionRawUnitCommand | ActionRawToggleAutocast | ActionRawCameraMove] = []\n        for action in chain(previous_frame_actions, self.response_observation.actions):\n            action_raw = action.action_raw\n            game_loop = action.game_loop\n            if action_raw.HasField(\"unit_command\"):\n                # Unit commands\n                raw_unit_command = action_raw.unit_command\n                if raw_unit_command.HasField(\"target_world_space_pos\"):\n                    # Actions that have a point as target\n                    actions.append(\n                        ActionRawUnitCommand(\n                            game_loop,\n                            raw_unit_command.ability_id,\n                            list(raw_unit_command.unit_tags),\n                            raw_unit_command.queue_command,\n                            Point2.from_proto(raw_unit_command.target_world_space_pos),\n                        )\n                    )\n                else:\n                    # Actions that have a unit as target\n                    actions.append(\n                        ActionRawUnitCommand(\n                            game_loop,\n                            raw_unit_command.ability_id,\n                            list(raw_unit_command.unit_tags),\n                            raw_unit_command.queue_command,\n                            None,\n                            raw_unit_command.target_unit_tag,\n                        )\n                    )\n            elif action_raw.HasField(\"toggle_autocast\"):\n                # Toggle autocast actions\n                raw_toggle_autocast_action = action_raw.toggle_autocast\n                actions.append(\n                    ActionRawToggleAutocast(\n                        game_loop,\n                        raw_toggle_autocast_action.ability_id,\n                        list(raw_toggle_autocast_action.unit_tags),\n                    )\n                )\n            else:\n                # Camera move actions\n                actions.append(ActionRawCameraMove(Point2.from_proto(action.action_raw.camera_move.center_world_space)))\n        return actions\n\n    @cached_property\n    def actions_unit_commands(self) -> list[ActionRawUnitCommand]:\n        \"\"\"\n        List of successful unit actions since last frame.\n        See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L185-L193\n        \"\"\"\n\n        return list(filter(lambda action: isinstance(action, ActionRawUnitCommand), self.actions))\n\n    @cached_property\n    def actions_toggle_autocast(self) -> list[ActionRawToggleAutocast]:\n        \"\"\"\n        List of successful autocast toggle actions since last frame.\n        See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L199-L202\n        \"\"\"\n\n        return list(filter(lambda action: isinstance(action, ActionRawToggleAutocast), self.actions))\n\n    @cached_property\n    def action_errors(self) -> list[ActionError]:\n        \"\"\"\n        List of erroneous actions since last frame.\n        See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/sc2api.proto#L648-L652\n        \"\"\"\n        previous_frame_errors = self.previous_observation.action_errors if self.previous_observation else []\n        return [\n            ActionError(error.ability_id, error.unit_tag, error.result)\n            for error in chain(self.response_observation.action_errors, previous_frame_errors)\n        ]\n"
  },
  {
    "path": "sc2/generate_ids.py",
    "content": "from __future__ import annotations\n\nimport importlib\nimport json\nimport platform\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nfrom loguru import logger\n\nfrom sc2.game_data import GameData\n\ntry:\n    from sc2.ids.id_version import ID_VERSION_STRING\nexcept ImportError:\n    ID_VERSION_STRING = \"4.11.4.78285\"\n\n\nclass IdGenerator:\n    def __init__(\n        self, game_data: GameData | None = None, game_version: str | None = None, verbose: bool = False\n    ) -> None:\n        self.game_data = game_data\n        self.game_version = game_version\n        self.verbose = verbose\n\n        self.HEADER = f\"\"\"\nfrom __future__ import annotations\n# DO NOT EDIT!\n# This file was automatically generated by \"{Path(__file__).name}\"\n\"\"\"\n\n        self.PF = platform.system()\n\n        self.HOME_DIR = str(Path.home())\n        self.DATA_JSON = {\n            \"Darwin\": self.HOME_DIR + \"/Library/Application Support/Blizzard/StarCraft II/stableid.json\",\n            \"Windows\": self.HOME_DIR + \"/Documents/StarCraft II/stableid.json\",\n            \"Linux\": self.HOME_DIR + \"/Documents/StarCraft II/stableid.json\",\n        }\n\n        self.ENUM_TRANSLATE = {\n            \"Units\": \"UnitTypeId\",\n            \"Abilities\": \"AbilityId\",\n            \"Upgrades\": \"UpgradeId\",\n            \"Buffs\": \"BuffId\",\n            \"Effects\": \"EffectId\",\n        }\n\n        self.FILE_TRANSLATE = {\n            \"Units\": \"unit_typeid\",\n            \"Abilities\": \"ability_id\",\n            \"Upgrades\": \"upgrade_id\",\n            \"Buffs\": \"buff_id\",\n            \"Effects\": \"effect_id\",\n        }\n\n    @staticmethod\n    def make_key(key: str) -> str:\n        if key[0].isdigit():\n            key = \"_\" + key\n        # In patch 5.0, the key has \"@\" character in it which is not possible with python enums\n        return key.upper().replace(\" \", \"_\").replace(\"@\", \"\")\n\n    def parse_data(self, data) -> dict[str, Any]:\n        # for d in data:  # Units, Abilities, Upgrades, Buffs, Effects\n\n        units = self.parse_simple(\"Units\", data)\n        upgrades = self.parse_simple(\"Upgrades\", data)\n        effects = self.parse_simple(\"Effects\", data)\n        buffs = self.parse_simple(\"Buffs\", data)\n\n        abilities = {}\n        for v in data[\"Abilities\"]:\n            key = v[\"buttonname\"]\n            remapid = v.get(\"remapid\")\n\n            if key == \"\" and v[\"index\"] == 0:\n                key = v[\"name\"]\n\n            if (not key) and (remapid is None):\n                assert v[\"buttonname\"] == \"\"\n                continue\n\n            if not key:\n                if v[\"friendlyname\"] != \"\":\n                    key = v[\"friendlyname\"]\n                else:\n                    sys.exit(f\"Not mapped: {v!r}\")\n\n            key = key.upper().replace(\" \", \"_\").replace(\"@\", \"\")\n\n            if \"name\" in v:\n                key = f\"{v['name'].upper().replace(' ', '_')}_{key}\"\n\n            if \"friendlyname\" in v:\n                key = v[\"friendlyname\"].upper().replace(\" \", \"_\")\n\n            if key[0].isdigit():\n                key = \"_\" + key\n\n            if key in abilities and v[\"index\"] == 0:\n                logger.info(f\"{key} has value 0 and id {v['id']}, overwriting {key}: {abilities[key]}\")\n                # Commented out to try to fix: 3670 is not a valid AbilityId\n                abilities[key] = v[\"id\"]\n            elif key in abilities:\n                logger.info(f\"{key} has appeared a second time with id={v['id']}\")\n            else:\n                abilities[key] = v[\"id\"]\n\n        abilities[\"SMART\"] = 1\n\n        enums = {}\n        enums[\"Units\"] = units\n        enums[\"Abilities\"] = abilities\n        enums[\"Upgrades\"] = upgrades\n        enums[\"Buffs\"] = buffs\n        enums[\"Effects\"] = effects\n\n        return enums\n\n    def parse_simple(self, d, data):\n        units = {}\n        for v in data[d]:\n            key = v[\"name\"]\n\n            if not key:\n                continue\n            key_to_insert = self.make_key(key)\n            if key_to_insert in units:\n                index = 2\n                tmp = f\"{key_to_insert}_{index}\"\n                while tmp in units:\n                    index += 1\n                    tmp = f\"{key_to_insert}_{index}\"\n                key_to_insert = tmp\n            units[key_to_insert] = v[\"id\"]\n\n        return units\n\n    def generate_python_code(self, enums) -> None:\n        assert {\"Units\", \"Abilities\", \"Upgrades\", \"Buffs\", \"Effects\"} <= enums.keys()\n\n        sc2dir = Path(__file__).parent\n        idsdir = sc2dir / \"ids\"\n        idsdir.mkdir(exist_ok=True)\n\n        with (idsdir / \"__init__.py\").open(\"w\") as f:\n            initstring = f\"__all__ = {[n.lower() for n in self.FILE_TRANSLATE.values()]!r}\\n\".replace(\"'\", '\"')\n            f.write(\"\\n\".join([self.HEADER, initstring]))\n\n        for name, body in enums.items():\n            class_name = self.ENUM_TRANSLATE[name]\n\n            code = [self.HEADER, \"import enum\", \"\\n\", f\"class {class_name}(enum.Enum):\"]\n\n            for key, value in sorted(body.items(), key=lambda p: p[1]):\n                code.append(f\"    {key} = {value}\")\n\n            # Add repr function to more easily dump enums to dict\n            code += f\"\"\"\n    def __repr__(self) -> str:\n        return f\"{class_name}.{{self.name}}\"\n\"\"\".split(\"\\n\")\n\n            # Add missing ids function to not make the game crash when unknown BuffId was detected\n            if class_name == \"BuffId\":\n                code += f\"\"\"\n    @classmethod\n    def _missing_(cls, value: int) -> {class_name}:\n        return cls.NULL\n\"\"\".split(\"\\n\")\n\n            if class_name == \"AbilityId\":\n                code += f\"\"\"\n    @classmethod\n    def _missing_(cls, value: int) -> {class_name}:\n        return cls.NULL_NULL\n\"\"\".split(\"\\n\")\n\n            code += f\"\"\"\nfor item in {class_name}:\n    globals()[item.name] = item\n\"\"\".split(\"\\n\")\n\n            ids_file_path = (idsdir / self.FILE_TRANSLATE[name]).with_suffix(\".py\")\n            with ids_file_path.open(\"w\") as f:\n                f.write(\"\\n\".join(code))\n\n        if self.game_version is not None:\n            version_path = Path(__file__).parent / \"ids\" / \"id_version.py\"\n            with Path(version_path).open(\"w\") as f:\n                f.write(f'ID_VERSION_STRING = \"{self.game_version}\"\\n')\n\n    def update_ids_from_stableid_json(self) -> None:\n        if self.game_version is None or self.game_version != ID_VERSION_STRING:\n            if self.verbose and self.game_version is not None:\n                logger.info(\n                    f\"Game version is different (Old: {self.game_version}, new: {ID_VERSION_STRING}. Updating ids to match game version\"\n                )\n            stable_id_path = Path(self.DATA_JSON[self.PF])\n            assert stable_id_path.is_file(), f'stable_id.json was not found at path \"{stable_id_path}\"'\n            with stable_id_path.open(encoding=\"utf-8\") as data_file:\n                data = json.loads(data_file.read())\n            self.generate_python_code(self.parse_data(data))\n\n    @staticmethod\n    def reimport_ids() -> None:\n        # Reload the newly written \"id\" files\n        # TODO This only re-imports modules, but if they haven't been imported, it will yield an error\n        importlib.reload(sys.modules[\"sc2.ids.ability_id\"])\n\n        importlib.reload(sys.modules[\"sc2.ids.unit_typeid\"])\n\n        importlib.reload(sys.modules[\"sc2.ids.upgrade_id\"])\n\n        importlib.reload(sys.modules[\"sc2.ids.effect_id\"])\n\n        importlib.reload(sys.modules[\"sc2.ids.buff_id\"])\n\n        # importlib.reload(sys.modules[\"sc2.ids.id_version\"])\n\n        importlib.reload(sys.modules[\"sc2.constants\"])\n\n\nif __name__ == \"__main__\":\n    updater = IdGenerator()\n    updater.update_ids_from_stableid_json()\n"
  },
  {
    "path": "sc2/ids/__init__.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\n\n__all__ = [\"unit_typeid\", \"ability_id\", \"upgrade_id\", \"buff_id\", \"effect_id\"]\n"
  },
  {
    "path": "sc2/ids/ability_id.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\nimport enum\n\n\nclass AbilityId(enum.Enum):\n    NULL_NULL = 0\n    SMART = 1\n    TAUNT_TAUNT = 2\n    STOP_STOP = 4\n    STOP_HOLDFIRESPECIAL = 5\n    STOP_CHEER = 6\n    STOP_DANCE = 7\n    HOLDFIRE_STOPSPECIAL = 10\n    HOLDFIRE_HOLDFIRE = 11\n    MOVE_MOVE = 16\n    PATROL_PATROL = 17\n    HOLDPOSITION_HOLD = 18\n    SCAN_MOVE = 19\n    MOVE_TURN = 20\n    BEACON_CANCEL = 21\n    BEACON_BEACONMOVE = 22\n    ATTACK_ATTACK = 23\n    ATTACK_ATTACKTOWARDS = 24\n    ATTACK_ATTACKBARRAGE = 25\n    EFFECT_SPRAY_TERRAN = 26\n    EFFECT_SPRAY_ZERG = 28\n    EFFECT_SPRAY_PROTOSS = 30\n    EFFECT_SALVAGE = 32\n    CORRUPTION_CORRUPTIONABILITY = 34\n    CORRUPTION_CANCEL = 35\n    BEHAVIOR_HOLDFIREON_GHOST = 36\n    BEHAVIOR_HOLDFIREOFF_GHOST = 38\n    MORPHTOINFESTEDTERRAN_INFESTEDTERRANS = 40\n    EXPLODE_EXPLODE = 42\n    RESEARCH_INTERCEPTORGRAVITONCATAPULT = 44\n    FLEETBEACONRESEARCH_RESEARCHINTERCEPTORLAUNCHSPEEDUPGRADE = 45\n    RESEARCH_PHOENIXANIONPULSECRYSTALS = 46\n    FLEETBEACONRESEARCH_TEMPESTRANGEUPGRADE = 47\n    FLEETBEACONRESEARCH_RESEARCHVOIDRAYSPEEDUPGRADE = 48\n    FLEETBEACONRESEARCH_TEMPESTRESEARCHGROUNDATTACKUPGRADE = 49\n    FUNGALGROWTH_FUNGALGROWTH = 74\n    GUARDIANSHIELD_GUARDIANSHIELD = 76\n    EFFECT_REPAIR_MULE = 78\n    MORPHZERGLINGTOBANELING_BANELING = 80\n    NEXUSTRAINMOTHERSHIP_MOTHERSHIP = 110\n    FEEDBACK_FEEDBACK = 140\n    EFFECT_MASSRECALL_STRATEGICRECALL = 142\n    PLACEPOINTDEFENSEDRONE_POINTDEFENSEDRONE = 144\n    HALLUCINATION_ARCHON = 146\n    HALLUCINATION_COLOSSUS = 148\n    HALLUCINATION_HIGHTEMPLAR = 150\n    HALLUCINATION_IMMORTAL = 152\n    HALLUCINATION_PHOENIX = 154\n    HALLUCINATION_PROBE = 156\n    HALLUCINATION_STALKER = 158\n    HALLUCINATION_VOIDRAY = 160\n    HALLUCINATION_WARPPRISM = 162\n    HALLUCINATION_ZEALOT = 164\n    HARVEST_GATHER_MULE = 166\n    HARVEST_RETURN_MULE = 167\n    SEEKERMISSILE_HUNTERSEEKERMISSILE = 169\n    CALLDOWNMULE_CALLDOWNMULE = 171\n    GRAVITONBEAM_GRAVITONBEAM = 173\n    CANCEL_GRAVITONBEAM = 174\n    BUILDINPROGRESSNYDUSCANAL_CANCEL = 175\n    SIPHON_SIPHON = 177\n    SIPHON_CANCEL = 178\n    LEECH_LEECH = 179\n    SPAWNCHANGELING_SPAWNCHANGELING = 181\n    DISGUISEASZEALOT_ZEALOT = 183\n    DISGUISEASMARINEWITHSHIELD_MARINE = 185\n    DISGUISEASMARINEWITHOUTSHIELD_MARINE = 187\n    DISGUISEASZERGLINGWITHWINGS_ZERGLING = 189\n    DISGUISEASZERGLINGWITHOUTWINGS_ZERGLING = 191\n    PHASESHIFT_PHASESHIFT = 193\n    RALLY_BUILDING = 195\n    RALLY_MORPHING_UNIT = 199\n    RALLY_COMMANDCENTER = 203\n    RALLY_NEXUS = 207\n    RALLY_HATCHERY_UNITS = 211\n    RALLY_HATCHERY_WORKERS = 212\n    ROACHWARRENRESEARCH_ROACHWARRENRESEARCH = 215\n    RESEARCH_GLIALREGENERATION = 216\n    RESEARCH_TUNNELINGCLAWS = 217\n    ROACHWARRENRESEARCH_ROACHSUPPLY = 218\n    SAPSTRUCTURE_SAPSTRUCTURE = 245\n    INFESTEDTERRANS_INFESTEDTERRANS = 247\n    NEURALPARASITE_NEURALPARASITE = 249\n    CANCEL_NEURALPARASITE = 250\n    EFFECT_INJECTLARVA = 251\n    EFFECT_STIM_MARAUDER = 253\n    SUPPLYDROP_SUPPLYDROP = 255\n    _250MMSTRIKECANNONS_250MMSTRIKECANNONS = 257\n    _250MMSTRIKECANNONS_CANCEL = 258\n    TEMPORALRIFT_TEMPORALRIFT = 259\n    EFFECT_CHRONOBOOST = 261\n    RESEARCH_ANABOLICSYNTHESIS = 263\n    RESEARCH_CHITINOUSPLATING = 265\n    WORMHOLETRANSIT_WORMHOLETRANSIT = 293\n    HARVEST_GATHER_SCV = 295\n    HARVEST_RETURN_SCV = 296\n    HARVEST_GATHER_PROBE = 298\n    HARVEST_RETURN_PROBE = 299\n    ATTACKWARPPRISM_ATTACKWARPPRISM = 301\n    ATTACKWARPPRISM_ATTACKTOWARDS = 302\n    ATTACKWARPPRISM_ATTACKBARRAGE = 303\n    CANCEL_QUEUE1 = 304\n    CANCELSLOT_QUEUE1 = 305\n    CANCEL_QUEUE5 = 306\n    CANCELSLOT_QUEUE5 = 307\n    CANCEL_QUEUECANCELTOSELECTION = 308\n    CANCELSLOT_QUEUECANCELTOSELECTION = 309\n    QUE5LONGBLEND_CANCEL = 310\n    QUE5LONGBLEND_CANCELSLOT = 311\n    CANCEL_QUEUEADDON = 312\n    CANCELSLOT_ADDON = 313\n    CANCEL_BUILDINPROGRESS = 314\n    HALT_BUILDING = 315\n    EFFECT_REPAIR_SCV = 316\n    TERRANBUILD_COMMANDCENTER = 318\n    TERRANBUILD_SUPPLYDEPOT = 319\n    TERRANBUILD_REFINERY = 320\n    TERRANBUILD_BARRACKS = 321\n    TERRANBUILD_ENGINEERINGBAY = 322\n    TERRANBUILD_MISSILETURRET = 323\n    TERRANBUILD_BUNKER = 324\n    TERRANBUILD_SENSORTOWER = 326\n    TERRANBUILD_GHOSTACADEMY = 327\n    TERRANBUILD_FACTORY = 328\n    TERRANBUILD_STARPORT = 329\n    TERRANBUILD_ARMORY = 331\n    TERRANBUILD_FUSIONCORE = 333\n    HALT_TERRANBUILD = 348\n    RAVENBUILD_AUTOTURRET = 349\n    RAVENBUILD_CANCEL = 379\n    EFFECT_STIM_MARINE = 380\n    BEHAVIOR_CLOAKON_GHOST = 382\n    BEHAVIOR_CLOAKOFF_GHOST = 383\n    SNIPE_SNIPE = 384\n    MEDIVACHEAL_HEAL = 386\n    SIEGEMODE_SIEGEMODE = 388\n    UNSIEGE_UNSIEGE = 390\n    BEHAVIOR_CLOAKON_BANSHEE = 392\n    BEHAVIOR_CLOAKOFF_BANSHEE = 393\n    LOAD_MEDIVAC = 394\n    UNLOADALLAT_MEDIVAC = 396\n    UNLOADUNIT_MEDIVAC = 397\n    SCANNERSWEEP_SCAN = 399\n    YAMATO_YAMATOGUN = 401\n    MORPH_VIKINGASSAULTMODE = 403\n    MORPH_VIKINGFIGHTERMODE = 405\n    LOAD_BUNKER = 407\n    UNLOADALL_BUNKER = 408\n    UNLOADUNIT_BUNKER = 410\n    COMMANDCENTERTRANSPORT_COMMANDCENTERTRANSPORT = 412\n    UNLOADALL_COMMANDCENTER = 413\n    UNLOADUNIT_COMMANDCENTER = 415\n    LOADALL_COMMANDCENTER = 416\n    LIFT_COMMANDCENTER = 417\n    LAND_COMMANDCENTER = 419\n    BUILD_TECHLAB_BARRACKS = 421\n    BUILD_REACTOR_BARRACKS = 422\n    CANCEL_BARRACKSADDON = 451\n    LIFT_BARRACKS = 452\n    BUILD_TECHLAB_FACTORY = 454\n    BUILD_REACTOR_FACTORY = 455\n    CANCEL_FACTORYADDON = 484\n    LIFT_FACTORY = 485\n    BUILD_TECHLAB_STARPORT = 487\n    BUILD_REACTOR_STARPORT = 488\n    CANCEL_STARPORTADDON = 517\n    LIFT_STARPORT = 518\n    LAND_FACTORY = 520\n    LAND_STARPORT = 522\n    COMMANDCENTERTRAIN_SCV = 524\n    LAND_BARRACKS = 554\n    MORPH_SUPPLYDEPOT_LOWER = 556\n    MORPH_SUPPLYDEPOT_RAISE = 558\n    BARRACKSTRAIN_MARINE = 560\n    BARRACKSTRAIN_REAPER = 561\n    BARRACKSTRAIN_GHOST = 562\n    BARRACKSTRAIN_MARAUDER = 563\n    FACTORYTRAIN_FACTORYTRAIN = 590\n    FACTORYTRAIN_SIEGETANK = 591\n    FACTORYTRAIN_THOR = 594\n    FACTORYTRAIN_HELLION = 595\n    TRAIN_HELLBAT = 596\n    TRAIN_CYCLONE = 597\n    FACTORYTRAIN_WIDOWMINE = 614\n    STARPORTTRAIN_MEDIVAC = 620\n    STARPORTTRAIN_BANSHEE = 621\n    STARPORTTRAIN_RAVEN = 622\n    STARPORTTRAIN_BATTLECRUISER = 623\n    STARPORTTRAIN_VIKINGFIGHTER = 624\n    STARPORTTRAIN_LIBERATOR = 626\n    RESEARCH_HISECAUTOTRACKING = 650\n    RESEARCH_TERRANSTRUCTUREARMORUPGRADE = 651\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1 = 652\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2 = 653\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3 = 654\n    RESEARCH_NEOSTEELFRAME = 655\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1 = 656\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2 = 657\n    ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3 = 658\n    MERCCOMPOUNDRESEARCH_MERCCOMPOUNDRESEARCH = 680\n    MERCCOMPOUNDRESEARCH_REAPERSPEED = 683\n    BUILD_NUKE = 710\n    BARRACKSTECHLABRESEARCH_STIMPACK = 730\n    RESEARCH_COMBATSHIELD = 731\n    RESEARCH_CONCUSSIVESHELLS = 732\n    FACTORYTECHLABRESEARCH_FACTORYTECHLABRESEARCH = 760\n    RESEARCH_INFERNALPREIGNITER = 761\n    FACTORYTECHLABRESEARCH_RESEARCHTRANSFORMATIONSERVOS = 763\n    RESEARCH_DRILLINGCLAWS = 764\n    FACTORYTECHLABRESEARCH_RESEARCHLOCKONRANGEUPGRADE = 765\n    RESEARCH_SMARTSERVOS = 766\n    FACTORYTECHLABRESEARCH_RESEARCHARMORPIERCINGROCKETS = 767\n    RESEARCH_CYCLONERAPIDFIRELAUNCHERS = 768\n    RESEARCH_CYCLONELOCKONDAMAGE = 769\n    FACTORYTECHLABRESEARCH_CYCLONERESEARCHHURRICANETHRUSTERS = 770\n    RESEARCH_BANSHEECLOAKINGFIELD = 790\n    STARPORTTECHLABRESEARCH_RESEARCHMEDIVACENERGYUPGRADE = 792\n    RESEARCH_RAVENCORVIDREACTOR = 793\n    STARPORTTECHLABRESEARCH_RESEARCHSEEKERMISSILE = 796\n    STARPORTTECHLABRESEARCH_RESEARCHDURABLEMATERIALS = 797\n    RESEARCH_BANSHEEHYPERFLIGHTROTORS = 799\n    STARPORTTECHLABRESEARCH_RESEARCHLIBERATORAGMODE = 800\n    STARPORTTECHLABRESEARCH_RESEARCHRAPIDDEPLOYMENT = 802\n    RESEARCH_RAVENRECALIBRATEDEXPLOSIVES = 803\n    RESEARCH_HIGHCAPACITYFUELTANKS = 804\n    RESEARCH_ADVANCEDBALLISTICS = 805\n    STARPORTTECHLABRESEARCH_RAVENRESEARCHENHANCEDMUNITIONS = 806\n    STARPORTTECHLABRESEARCH_RESEARCHRAVENINTERFERENCEMATRIX = 807\n    RESEARCH_PERSONALCLOAKING = 820\n    ARMORYRESEARCH_ARMORYRESEARCH = 850\n    ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1 = 852\n    ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2 = 853\n    ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3 = 854\n    ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1 = 855\n    ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2 = 856\n    ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3 = 857\n    ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL1 = 858\n    ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL2 = 859\n    ARMORYRESEARCH_TERRANSHIPPLATINGLEVEL3 = 860\n    ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1 = 861\n    ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2 = 862\n    ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3 = 863\n    ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL1 = 864\n    ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL2 = 865\n    ARMORYRESEARCH_TERRANVEHICLEANDSHIPPLATINGLEVEL3 = 866\n    PROTOSSBUILD_NEXUS = 880\n    PROTOSSBUILD_PYLON = 881\n    PROTOSSBUILD_ASSIMILATOR = 882\n    PROTOSSBUILD_GATEWAY = 883\n    PROTOSSBUILD_FORGE = 884\n    PROTOSSBUILD_FLEETBEACON = 885\n    PROTOSSBUILD_TWILIGHTCOUNCIL = 886\n    PROTOSSBUILD_PHOTONCANNON = 887\n    PROTOSSBUILD_STARGATE = 889\n    PROTOSSBUILD_TEMPLARARCHIVE = 890\n    PROTOSSBUILD_DARKSHRINE = 891\n    PROTOSSBUILD_ROBOTICSBAY = 892\n    PROTOSSBUILD_ROBOTICSFACILITY = 893\n    PROTOSSBUILD_CYBERNETICSCORE = 894\n    BUILD_SHIELDBATTERY = 895\n    PROTOSSBUILD_CANCEL = 910\n    LOAD_WARPPRISM = 911\n    UNLOADALL_WARPPRISM = 912\n    UNLOADALLAT_WARPPRISM = 913\n    UNLOADUNIT_WARPPRISM = 914\n    GATEWAYTRAIN_ZEALOT = 916\n    GATEWAYTRAIN_STALKER = 917\n    GATEWAYTRAIN_HIGHTEMPLAR = 919\n    GATEWAYTRAIN_DARKTEMPLAR = 920\n    GATEWAYTRAIN_SENTRY = 921\n    TRAIN_ADEPT = 922\n    STARGATETRAIN_PHOENIX = 946\n    STARGATETRAIN_CARRIER = 948\n    STARGATETRAIN_VOIDRAY = 950\n    STARGATETRAIN_ORACLE = 954\n    STARGATETRAIN_TEMPEST = 955\n    ROBOTICSFACILITYTRAIN_WARPPRISM = 976\n    ROBOTICSFACILITYTRAIN_OBSERVER = 977\n    ROBOTICSFACILITYTRAIN_COLOSSUS = 978\n    ROBOTICSFACILITYTRAIN_IMMORTAL = 979\n    TRAIN_DISRUPTOR = 994\n    NEXUSTRAIN_PROBE = 1006\n    PSISTORM_PSISTORM = 1036\n    CANCEL_HANGARQUEUE5 = 1038\n    CANCELSLOT_HANGARQUEUE5 = 1039\n    BROODLORDQUEUE2_CANCEL = 1040\n    BROODLORDQUEUE2_CANCELSLOT = 1041\n    BUILD_INTERCEPTORS = 1042\n    FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1 = 1062\n    FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2 = 1063\n    FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3 = 1064\n    FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1 = 1065\n    FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2 = 1066\n    FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3 = 1067\n    FORGERESEARCH_PROTOSSSHIELDSLEVEL1 = 1068\n    FORGERESEARCH_PROTOSSSHIELDSLEVEL2 = 1069\n    FORGERESEARCH_PROTOSSSHIELDSLEVEL3 = 1070\n    ROBOTICSBAYRESEARCH_ROBOTICSBAYRESEARCH = 1092\n    RESEARCH_GRAVITICBOOSTER = 1093\n    RESEARCH_GRAVITICDRIVE = 1094\n    RESEARCH_EXTENDEDTHERMALLANCE = 1097\n    ROBOTICSBAYRESEARCH_RESEARCHIMMORTALREVIVE = 1099\n    TEMPLARARCHIVESRESEARCH_TEMPLARARCHIVESRESEARCH = 1122\n    RESEARCH_PSISTORM = 1126\n    ZERGBUILD_HATCHERY = 1152\n    ZERGBUILD_CREEPTUMOR = 1153\n    ZERGBUILD_EXTRACTOR = 1154\n    ZERGBUILD_SPAWNINGPOOL = 1155\n    ZERGBUILD_EVOLUTIONCHAMBER = 1156\n    ZERGBUILD_HYDRALISKDEN = 1157\n    ZERGBUILD_SPIRE = 1158\n    ZERGBUILD_ULTRALISKCAVERN = 1159\n    ZERGBUILD_INFESTATIONPIT = 1160\n    ZERGBUILD_NYDUSNETWORK = 1161\n    ZERGBUILD_BANELINGNEST = 1162\n    BUILD_LURKERDEN = 1163\n    ZERGBUILD_ROACHWARREN = 1165\n    ZERGBUILD_SPINECRAWLER = 1166\n    ZERGBUILD_SPORECRAWLER = 1167\n    ZERGBUILD_CANCEL = 1182\n    HARVEST_GATHER_DRONE = 1183\n    HARVEST_RETURN_DRONE = 1184\n    RESEARCH_ZERGMELEEWEAPONSLEVEL1 = 1186\n    RESEARCH_ZERGMELEEWEAPONSLEVEL2 = 1187\n    RESEARCH_ZERGMELEEWEAPONSLEVEL3 = 1188\n    RESEARCH_ZERGGROUNDARMORLEVEL1 = 1189\n    RESEARCH_ZERGGROUNDARMORLEVEL2 = 1190\n    RESEARCH_ZERGGROUNDARMORLEVEL3 = 1191\n    RESEARCH_ZERGMISSILEWEAPONSLEVEL1 = 1192\n    RESEARCH_ZERGMISSILEWEAPONSLEVEL2 = 1193\n    RESEARCH_ZERGMISSILEWEAPONSLEVEL3 = 1194\n    EVOLUTIONCHAMBERRESEARCH_EVOLVEPROPULSIVEPERISTALSIS = 1195\n    UPGRADETOLAIR_LAIR = 1216\n    CANCEL_MORPHLAIR = 1217\n    UPGRADETOHIVE_HIVE = 1218\n    CANCEL_MORPHHIVE = 1219\n    UPGRADETOGREATERSPIRE_GREATERSPIRE = 1220\n    CANCEL_MORPHGREATERSPIRE = 1221\n    LAIRRESEARCH_LAIRRESEARCH = 1222\n    RESEARCH_PNEUMATIZEDCARAPACE = 1223\n    LAIRRESEARCH_EVOLVEVENTRALSACKS = 1224\n    RESEARCH_BURROW = 1225\n    RESEARCH_ZERGLINGADRENALGLANDS = 1252\n    RESEARCH_ZERGLINGMETABOLICBOOST = 1253\n    RESEARCH_GROOVEDSPINES = 1282\n    RESEARCH_MUSCULARAUGMENTS = 1283\n    HYDRALISKDENRESEARCH_RESEARCHFRENZY = 1284\n    HYDRALISKDENRESEARCH_RESEARCHLURKERRANGE = 1286\n    RESEARCH_ZERGFLYERATTACKLEVEL1 = 1312\n    RESEARCH_ZERGFLYERATTACKLEVEL2 = 1313\n    RESEARCH_ZERGFLYERATTACKLEVEL3 = 1314\n    RESEARCH_ZERGFLYERARMORLEVEL1 = 1315\n    RESEARCH_ZERGFLYERARMORLEVEL2 = 1316\n    RESEARCH_ZERGFLYERARMORLEVEL3 = 1317\n    LARVATRAIN_DRONE = 1342\n    LARVATRAIN_ZERGLING = 1343\n    LARVATRAIN_OVERLORD = 1344\n    LARVATRAIN_HYDRALISK = 1345\n    LARVATRAIN_MUTALISK = 1346\n    LARVATRAIN_ULTRALISK = 1348\n    LARVATRAIN_ROACH = 1351\n    LARVATRAIN_INFESTOR = 1352\n    LARVATRAIN_CORRUPTOR = 1353\n    LARVATRAIN_VIPER = 1354\n    TRAIN_SWARMHOST = 1356\n    MORPHTOBROODLORD_BROODLORD = 1372\n    CANCEL_MORPHBROODLORD = 1373\n    BURROWDOWN_BANELING = 1374\n    BURROWBANELINGDOWN_CANCEL = 1375\n    BURROWUP_BANELING = 1376\n    BURROWDOWN_DRONE = 1378\n    BURROWDRONEDOWN_CANCEL = 1379\n    BURROWUP_DRONE = 1380\n    BURROWDOWN_HYDRALISK = 1382\n    BURROWHYDRALISKDOWN_CANCEL = 1383\n    BURROWUP_HYDRALISK = 1384\n    BURROWDOWN_ROACH = 1386\n    BURROWROACHDOWN_CANCEL = 1387\n    BURROWUP_ROACH = 1388\n    BURROWDOWN_ZERGLING = 1390\n    BURROWZERGLINGDOWN_CANCEL = 1391\n    BURROWUP_ZERGLING = 1392\n    BURROWDOWN_INFESTORTERRAN = 1394\n    BURROWUP_INFESTORTERRAN = 1396\n    REDSTONELAVACRITTERBURROW_BURROWDOWN = 1398\n    REDSTONELAVACRITTERINJUREDBURROW_BURROWDOWN = 1400\n    REDSTONELAVACRITTERUNBURROW_BURROWUP = 1402\n    REDSTONELAVACRITTERINJUREDUNBURROW_BURROWUP = 1404\n    LOAD_OVERLORD = 1406\n    UNLOADALLAT_OVERLORD = 1408\n    UNLOADUNIT_OVERLORD = 1409\n    MERGEABLE_CANCEL = 1411\n    WARPABLE_CANCEL = 1412\n    WARPGATETRAIN_ZEALOT = 1413\n    WARPGATETRAIN_STALKER = 1414\n    WARPGATETRAIN_HIGHTEMPLAR = 1416\n    WARPGATETRAIN_DARKTEMPLAR = 1417\n    WARPGATETRAIN_SENTRY = 1418\n    TRAINWARP_ADEPT = 1419\n    BURROWDOWN_QUEEN = 1433\n    BURROWQUEENDOWN_CANCEL = 1434\n    BURROWUP_QUEEN = 1435\n    LOAD_NYDUSNETWORK = 1437\n    UNLOADALL_NYDASNETWORK = 1438\n    UNLOADUNIT_NYDASNETWORK = 1440\n    EFFECT_BLINK_STALKER = 1442\n    BURROWDOWN_INFESTOR = 1444\n    BURROWINFESTORDOWN_CANCEL = 1445\n    BURROWUP_INFESTOR = 1446\n    MORPH_OVERSEER = 1448\n    CANCEL_MORPHOVERSEER = 1449\n    UPGRADETOPLANETARYFORTRESS_PLANETARYFORTRESS = 1450\n    CANCEL_MORPHPLANETARYFORTRESS = 1451\n    INFESTATIONPITRESEARCH_INFESTATIONPITRESEARCH = 1452\n    RESEARCH_NEURALPARASITE = 1455\n    INFESTATIONPITRESEARCH_RESEARCHLOCUSTLIFETIMEINCREASE = 1456\n    INFESTATIONPITRESEARCH_EVOLVEAMORPHOUSARMORCLOUD = 1457\n    RESEARCH_CENTRIFUGALHOOKS = 1482\n    BURROWDOWN_ULTRALISK = 1512\n    BURROWUP_ULTRALISK = 1514\n    UPGRADETOORBITAL_ORBITALCOMMAND = 1516\n    CANCEL_MORPHORBITAL = 1517\n    MORPH_WARPGATE = 1518\n    UPGRADETOWARPGATE_CANCEL = 1519\n    MORPH_GATEWAY = 1520\n    MORPHBACKTOGATEWAY_CANCEL = 1521\n    LIFT_ORBITALCOMMAND = 1522\n    LAND_ORBITALCOMMAND = 1524\n    FORCEFIELD_FORCEFIELD = 1526\n    FORCEFIELD_CANCEL = 1527\n    MORPH_WARPPRISMPHASINGMODE = 1528\n    PHASINGMODE_CANCEL = 1529\n    MORPH_WARPPRISMTRANSPORTMODE = 1530\n    TRANSPORTMODE_CANCEL = 1531\n    RESEARCH_BATTLECRUISERWEAPONREFIT = 1532\n    FUSIONCORERESEARCH_RESEARCHBALLISTICRANGE = 1533\n    FUSIONCORERESEARCH_RESEARCHRAPIDREIGNITIONSYSTEM = 1534\n    FUSIONCORERESEARCH_RESEARCHMEDIVACENERGYUPGRADE = 1535\n    CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1 = 1562\n    CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2 = 1563\n    CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3 = 1564\n    CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL1 = 1565\n    CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL2 = 1566\n    CYBERNETICSCORERESEARCH_PROTOSSAIRARMORLEVEL3 = 1567\n    RESEARCH_WARPGATE = 1568\n    CYBERNETICSCORERESEARCH_RESEARCHHALLUCINATION = 1571\n    RESEARCH_CHARGE = 1592\n    RESEARCH_BLINK = 1593\n    RESEARCH_ADEPTRESONATINGGLAIVES = 1594\n    TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICSURGE = 1595\n    TWILIGHTCOUNCILRESEARCH_RESEARCHAMPLIFIEDSHIELDING = 1596\n    TWILIGHTCOUNCILRESEARCH_RESEARCHPSIONICAMPLIFIERS = 1597\n    TACNUKESTRIKE_NUKECALLDOWN = 1622\n    CANCEL_NUKE = 1623\n    SALVAGEBUNKERREFUND_SALVAGE = 1624\n    SALVAGEBUNKER_SALVAGE = 1626\n    EMP_EMP = 1628\n    VORTEX_VORTEX = 1630\n    TRAINQUEEN_QUEEN = 1632\n    BURROWCREEPTUMORDOWN_BURROWDOWN = 1662\n    TRANSFUSION_TRANSFUSION = 1664\n    TECHLABMORPH_TECHLABMORPH = 1666\n    BARRACKSTECHLABMORPH_TECHLABBARRACKS = 1668\n    FACTORYTECHLABMORPH_TECHLABFACTORY = 1670\n    STARPORTTECHLABMORPH_TECHLABSTARPORT = 1672\n    REACTORMORPH_REACTORMORPH = 1674\n    BARRACKSREACTORMORPH_REACTOR = 1676\n    FACTORYREACTORMORPH_REACTOR = 1678\n    STARPORTREACTORMORPH_REACTOR = 1680\n    ATTACK_REDIRECT = 1682\n    EFFECT_STIM_MARINE_REDIRECT = 1683\n    EFFECT_STIM_MARAUDER_REDIRECT = 1684\n    BURROWEDSTOP_STOPROACHBURROWED = 1685\n    BURROWEDSTOP_HOLDFIRESPECIAL = 1686\n    STOP_REDIRECT = 1691\n    BEHAVIOR_GENERATECREEPON = 1692\n    BEHAVIOR_GENERATECREEPOFF = 1693\n    BUILD_CREEPTUMOR_QUEEN = 1694\n    QUEENBUILD_CANCEL = 1724\n    SPINECRAWLERUPROOT_SPINECRAWLERUPROOT = 1725\n    SPINECRAWLERUPROOT_CANCEL = 1726\n    SPORECRAWLERUPROOT_SPORECRAWLERUPROOT = 1727\n    SPORECRAWLERUPROOT_CANCEL = 1728\n    SPINECRAWLERROOT_SPINECRAWLERROOT = 1729\n    CANCEL_SPINECRAWLERROOT = 1730\n    SPORECRAWLERROOT_SPORECRAWLERROOT = 1731\n    CANCEL_SPORECRAWLERROOT = 1732\n    BUILD_CREEPTUMOR_TUMOR = 1733\n    CANCEL_CREEPTUMOR = 1763\n    BUILDAUTOTURRET_AUTOTURRET = 1764\n    MORPH_ARCHON = 1766\n    ARCHON_WARP_TARGET = 1767\n    BUILD_NYDUSWORM = 1768\n    BUILDNYDUSCANAL_SUMMONNYDUSCANALATTACKER = 1769\n    BUILDNYDUSCANAL_CANCEL = 1798\n    BROODLORDHANGAR_BROODLORDHANGAR = 1799\n    EFFECT_CHARGE = 1819\n    TOWERCAPTURE_TOWERCAPTURE = 1820\n    HERDINTERACT_HERD = 1821\n    FRENZY_FRENZY = 1823\n    CONTAMINATE_CONTAMINATE = 1825\n    SHATTER_SHATTER = 1827\n    INFESTEDTERRANSLAYEGG_INFESTEDTERRANS = 1829\n    CANCEL_QUEUEPASIVE = 1831\n    CANCELSLOT_QUEUEPASSIVE = 1832\n    CANCEL_QUEUEPASSIVECANCELTOSELECTION = 1833\n    CANCELSLOT_QUEUEPASSIVECANCELTOSELECTION = 1834\n    MORPHTOGHOSTALTERNATE_MOVE = 1835\n    MORPHTOGHOSTNOVA_MOVE = 1837\n    DIGESTERCREEPSPRAY_DIGESTERCREEPSPRAY = 1839\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS = 1841\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRIS_CANCEL = 1842\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT = 1843\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPLEFT_CANCEL = 1844\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT = 1845\n    MORPHTOCOLLAPSIBLETERRANTOWERDEBRISRAMPRIGHT_CANCEL = 1846\n    MORPH_MOTHERSHIP = 1847\n    CANCEL_MORPHMOTHERSHIP = 1848\n    MOTHERSHIPSTASIS_MOTHERSHIPSTASIS = 1849\n    CANCEL_MOTHERSHIPSTASIS = 1850\n    MOTHERSHIPCOREWEAPON_MOTHERSHIPSTASIS = 1851\n    NEXUSTRAINMOTHERSHIPCORE_MOTHERSHIPCORE = 1853\n    MOTHERSHIPCORETELEPORT_MOTHERSHIPCORETELEPORT = 1883\n    SALVAGEDRONEREFUND_SALVAGE = 1885\n    SALVAGEDRONE_SALVAGE = 1887\n    SALVAGEZERGLINGREFUND_SALVAGE = 1889\n    SALVAGEZERGLING_SALVAGE = 1891\n    SALVAGEQUEENREFUND_SALVAGE = 1893\n    SALVAGEQUEEN_SALVAGE = 1895\n    SALVAGEROACHREFUND_SALVAGE = 1897\n    SALVAGEROACH_SALVAGE = 1899\n    SALVAGEBANELINGREFUND_SALVAGE = 1901\n    SALVAGEBANELING_SALVAGE = 1903\n    SALVAGEHYDRALISKREFUND_SALVAGE = 1905\n    SALVAGEHYDRALISK_SALVAGE = 1907\n    SALVAGEINFESTORREFUND_SALVAGE = 1909\n    SALVAGEINFESTOR_SALVAGE = 1911\n    SALVAGESWARMHOSTREFUND_SALVAGE = 1913\n    SALVAGESWARMHOST_SALVAGE = 1915\n    SALVAGEULTRALISKREFUND_SALVAGE = 1917\n    SALVAGEULTRALISK_SALVAGE = 1919\n    DIGESTERTRANSPORT_LOADDIGESTER = 1921\n    SPECTRESHIELD_SPECTRESHIELD = 1926\n    XELNAGAHEALINGSHRINE_XELNAGAHEALINGSHRINE = 1928\n    NEXUSINVULNERABILITY_NEXUSINVULNERABILITY = 1930\n    NEXUSPHASESHIFT_NEXUSPHASESHIFT = 1932\n    SPAWNCHANGELINGTARGET_SPAWNCHANGELING = 1934\n    QUEENLAND_QUEENLAND = 1936\n    QUEENFLY_QUEENFLY = 1938\n    ORACLECLOAKFIELD_ORACLECLOAKFIELD = 1940\n    FLYERSHIELD_FLYERSHIELD = 1942\n    LOCUSTTRAIN_SWARMHOST = 1944\n    EFFECT_MASSRECALL_MOTHERSHIPCORE = 1974\n    SINGLERECALL_SINGLERECALL = 1976\n    MORPH_HELLION = 1978\n    RESTORESHIELDS_RESTORESHIELDS = 1980\n    SCRYER_SCRYER = 1982\n    BURROWCHARGETRIAL_BURROWCHARGETRIAL = 1984\n    LEECHRESOURCES_LEECHRESOURCES = 1986\n    LEECHRESOURCES_CANCEL = 1987\n    SNIPEDOT_SNIPEDOT = 1988\n    SWARMHOSTSPAWNLOCUSTS_LOCUSTMP = 1990\n    CLONE_CLONE = 1992\n    BUILDINGSHIELD_BUILDINGSHIELD = 1994\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS = 1996\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRIS_CANCEL = 1997\n    MORPH_HELLBAT = 1998\n    BUILDINGSTASIS_BUILDINGSTASIS = 2000\n    RESOURCEBLOCKER_RESOURCEBLOCKER = 2002\n    RESOURCESTUN_RESOURCESTUN = 2004\n    MAXIUMTHRUST_MAXIMUMTHRUST = 2006\n    SACRIFICE_SACRIFICE = 2008\n    BURROWCHARGEMP_BURROWCHARGEMP = 2010\n    BURROWCHARGEREVD_BURROWCHARGEREVD = 2012\n    BURROWDOWN_SWARMHOST = 2014\n    MORPHTOSWARMHOSTBURROWEDMP_CANCEL = 2015\n    BURROWUP_SWARMHOST = 2016\n    SPAWNINFESTEDTERRAN_LOCUSTMP = 2018\n    ATTACKPROTOSSBUILDING_ATTACKBUILDING = 2048\n    ATTACKPROTOSSBUILDING_ATTACKTOWARDS = 2049\n    ATTACKPROTOSSBUILDING_ATTACKBARRAGE = 2050\n    BURROWEDBANELINGSTOP_STOPROACHBURROWED = 2051\n    BURROWEDBANELINGSTOP_HOLDFIRESPECIAL = 2052\n    STOP_BUILDING = 2057\n    STOPPROTOSSBUILDING_HOLDFIRE = 2058\n    STOPPROTOSSBUILDING_CHEER = 2059\n    STOPPROTOSSBUILDING_DANCE = 2060\n    BLINDINGCLOUD_BLINDINGCLOUD = 2063\n    EYESTALK_EYESTALK = 2065\n    EYESTALK_CANCEL = 2066\n    EFFECT_ABDUCT = 2067\n    VIPERCONSUME_VIPERCONSUME = 2069\n    VIPERCONSUMEMINERALS_VIPERCONSUME = 2071\n    VIPERCONSUMESTRUCTURE_VIPERCONSUME = 2073\n    CANCEL_PROTOSSBUILDINGQUEUE = 2075\n    PROTOSSBUILDINGQUEUE_CANCELSLOT = 2076\n    QUE8_CANCEL = 2077\n    QUE8_CANCELSLOT = 2078\n    TESTZERG_TESTZERG = 2079\n    TESTZERG_CANCEL = 2080\n    BEHAVIOR_BUILDINGATTACKON = 2081\n    BEHAVIOR_BUILDINGATTACKOFF = 2082\n    PICKUPSCRAPSMALL_PICKUPSCRAPSMALL = 2083\n    PICKUPSCRAPMEDIUM_PICKUPSCRAPMEDIUM = 2085\n    PICKUPSCRAPLARGE_PICKUPSCRAPLARGE = 2087\n    PICKUPPALLETGAS_PICKUPPALLETGAS = 2089\n    PICKUPPALLETMINERALS_PICKUPPALLETMINERALS = 2091\n    MASSIVEKNOCKOVER_MASSIVEKNOCKOVER = 2093\n    BURROWDOWN_WIDOWMINE = 2095\n    WIDOWMINEBURROW_CANCEL = 2096\n    BURROWUP_WIDOWMINE = 2097\n    WIDOWMINEATTACK_WIDOWMINEATTACK = 2099\n    TORNADOMISSILE_TORNADOMISSILE = 2101\n    MOTHERSHIPCOREENERGIZE_MOTHERSHIPCOREENERGIZE = 2102\n    MOTHERSHIPCOREENERGIZE_CANCEL = 2103\n    LURKERASPECTMPFROMHYDRALISKBURROWED_LURKERMPFROMHYDRALISKBURROWED = 2104\n    LURKERASPECTMPFROMHYDRALISKBURROWED_CANCEL = 2105\n    LURKERASPECTMP_LURKERMP = 2106\n    LURKERASPECTMP_CANCEL = 2107\n    BURROWDOWN_LURKER = 2108\n    BURROWLURKERMPDOWN_CANCEL = 2109\n    BURROWUP_LURKER = 2110\n    MORPH_LURKERDEN = 2112\n    CANCEL_MORPHLURKERDEN = 2113\n    HALLUCINATION_ORACLE = 2114\n    EFFECT_MEDIVACIGNITEAFTERBURNERS = 2116\n    EXTENDINGBRIDGENEWIDE8OUT_BRIDGEEXTEND = 2118\n    EXTENDINGBRIDGENEWIDE8_BRIDGERETRACT = 2120\n    EXTENDINGBRIDGENWWIDE8OUT_BRIDGEEXTEND = 2122\n    EXTENDINGBRIDGENWWIDE8_BRIDGERETRACT = 2124\n    EXTENDINGBRIDGENEWIDE10OUT_BRIDGEEXTEND = 2126\n    EXTENDINGBRIDGENEWIDE10_BRIDGERETRACT = 2128\n    EXTENDINGBRIDGENWWIDE10OUT_BRIDGEEXTEND = 2130\n    EXTENDINGBRIDGENWWIDE10_BRIDGERETRACT = 2132\n    EXTENDINGBRIDGENEWIDE12OUT_BRIDGEEXTEND = 2134\n    EXTENDINGBRIDGENEWIDE12_BRIDGERETRACT = 2136\n    EXTENDINGBRIDGENWWIDE12OUT_BRIDGEEXTEND = 2138\n    EXTENDINGBRIDGENWWIDE12_BRIDGERETRACT = 2140\n    INVULNERABILITYSHIELD_INVULNERABILITYSHIELD = 2142\n    CRITTERFLEE_CRITTERFLEE = 2144\n    ORACLEREVELATION_ORACLEREVELATION = 2146\n    ORACLEREVELATIONMODE_ORACLEREVELATIONMODE = 2148\n    ORACLEREVELATIONMODE_CANCEL = 2149\n    ORACLENORMALMODE_ORACLENORMALMODE = 2150\n    ORACLENORMALMODE_CANCEL = 2151\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT = 2152\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT_CANCEL = 2153\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT = 2154\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFT_CANCEL = 2155\n    VOIDSIPHON_VOIDSIPHON = 2156\n    ULTRALISKWEAPONCOOLDOWN_ULTRALISKWEAPONCOOLDOWN = 2158\n    MOTHERSHIPCOREPURIFYNEXUSCANCEL_CANCEL = 2160\n    EFFECT_PHOTONOVERCHARGE = 2162\n    XELNAGA_CAVERNS_DOORE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2164\n    XELNAGA_CAVERNS_DOOREOPENED_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2166\n    XELNAGA_CAVERNS_DOORN_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2168\n    XELNAGA_CAVERNS_DOORNE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2170\n    XELNAGA_CAVERNS_DOORNEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2172\n    XELNAGA_CAVERNS_DOORNOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2174\n    XELNAGA_CAVERNS_DOORNW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2176\n    XELNAGA_CAVERNS_DOORNWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2178\n    XELNAGA_CAVERNS_DOORS_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2180\n    XELNAGA_CAVERNS_DOORSE_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2182\n    XELNAGA_CAVERNS_DOORSEOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2184\n    XELNAGA_CAVERNS_DOORSOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2186\n    XELNAGA_CAVERNS_DOORSW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2188\n    XELNAGA_CAVERNS_DOORSWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2190\n    XELNAGA_CAVERNS_DOORW_XELNAGA_CAVERNS_DOORDEFAULTCLOSE = 2192\n    XELNAGA_CAVERNS_DOORWOPENED_XELNAGA_CAVERNS_DOORDEFAULTOPEN = 2194\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE8OUT_BRIDGEEXTEND = 2196\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE8_BRIDGERETRACT = 2198\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW8OUT_BRIDGEEXTEND = 2200\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW8_BRIDGERETRACT = 2202\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE10OUT_BRIDGEEXTEND = 2204\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE10_BRIDGERETRACT = 2206\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW10OUT_BRIDGEEXTEND = 2208\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW10_BRIDGERETRACT = 2210\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE12OUT_BRIDGEEXTEND = 2212\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE12_BRIDGERETRACT = 2214\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW12OUT_BRIDGEEXTEND = 2216\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW12_BRIDGERETRACT = 2218\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH8OUT_BRIDGEEXTEND = 2220\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH8_BRIDGERETRACT = 2222\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV8OUT_BRIDGEEXTEND = 2224\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV8_BRIDGERETRACT = 2226\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH10OUT_BRIDGEEXTEND = 2228\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH10_BRIDGERETRACT = 2230\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV10OUT_BRIDGEEXTEND = 2232\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV10_BRIDGERETRACT = 2234\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH12OUT_BRIDGEEXTEND = 2236\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH12_BRIDGERETRACT = 2238\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV12OUT_BRIDGEEXTEND = 2240\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV12_BRIDGERETRACT = 2242\n    EFFECT_TIMEWARP = 2244\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8OUT_BRIDGEEXTEND = 2246\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8_BRIDGERETRACT = 2248\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8OUT_BRIDGEEXTEND = 2250\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8_BRIDGERETRACT = 2252\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT10OUT_BRIDGEEXTEND = 2254\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT10_BRIDGERETRACT = 2256\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT10OUT_BRIDGEEXTEND = 2258\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT10_BRIDGERETRACT = 2260\n    TARSONIS_DOORN_TARSONIS_DOORN = 2262\n    TARSONIS_DOORNLOWERED_TARSONIS_DOORNLOWERED = 2264\n    TARSONIS_DOORNE_TARSONIS_DOORNE = 2266\n    TARSONIS_DOORNELOWERED_TARSONIS_DOORNELOWERED = 2268\n    TARSONIS_DOORE_TARSONIS_DOORE = 2270\n    TARSONIS_DOORELOWERED_TARSONIS_DOORELOWERED = 2272\n    TARSONIS_DOORNW_TARSONIS_DOORNW = 2274\n    TARSONIS_DOORNWLOWERED_TARSONIS_DOORNWLOWERED = 2276\n    COMPOUNDMANSION_DOORN_COMPOUNDMANSION_DOORN = 2278\n    COMPOUNDMANSION_DOORNLOWERED_COMPOUNDMANSION_DOORNLOWERED = 2280\n    COMPOUNDMANSION_DOORNE_COMPOUNDMANSION_DOORNE = 2282\n    COMPOUNDMANSION_DOORNELOWERED_COMPOUNDMANSION_DOORNELOWERED = 2284\n    COMPOUNDMANSION_DOORE_COMPOUNDMANSION_DOORE = 2286\n    COMPOUNDMANSION_DOORELOWERED_COMPOUNDMANSION_DOORELOWERED = 2288\n    COMPOUNDMANSION_DOORNW_COMPOUNDMANSION_DOORNW = 2290\n    COMPOUNDMANSION_DOORNWLOWERED_COMPOUNDMANSION_DOORNWLOWERED = 2292\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL1 = 2294\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL2 = 2295\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPWEAPONSLEVEL3 = 2296\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL1 = 2297\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL2 = 2298\n    ARMORYRESEARCHSWARM_TERRANVEHICLEANDSHIPPLATINGLEVEL3 = 2299\n    CAUSTICSPRAY_CAUSTICSPRAY = 2324\n    ORACLECLOAKINGFIELDTARGETED_ORACLECLOAKINGFIELDTARGETED = 2326\n    EFFECT_IMMORTALBARRIER = 2328\n    MORPHTORAVAGER_RAVAGER = 2330\n    CANCEL_MORPHRAVAGER = 2331\n    MORPH_LURKER = 2332\n    CANCEL_MORPHLURKER = 2333\n    ORACLEPHASESHIFT_ORACLEPHASESHIFT = 2334\n    RELEASEINTERCEPTORS_RELEASEINTERCEPTORS = 2336\n    EFFECT_CORROSIVEBILE = 2338\n    BURROWDOWN_RAVAGER = 2340\n    BURROWRAVAGERDOWN_CANCEL = 2341\n    BURROWUP_RAVAGER = 2342\n    PURIFICATIONNOVA_PURIFICATIONNOVA = 2344\n    EFFECT_PURIFICATIONNOVA = 2346\n    IMPALE_IMPALE = 2348\n    LOCKON_LOCKON = 2350\n    LOCKONAIR_LOCKONAIR = 2352\n    CANCEL_LOCKON = 2354\n    CORRUPTIONBOMB_CORRUPTIONBOMB = 2356\n    CORRUPTIONBOMB_CANCEL = 2357\n    EFFECT_TACTICALJUMP = 2358\n    OVERCHARGE_OVERCHARGE = 2360\n    MORPH_THORHIGHIMPACTMODE = 2362\n    THORAPMODE_CANCEL = 2363\n    MORPH_THOREXPLOSIVEMODE = 2364\n    CANCEL_MORPHTHOREXPLOSIVEMODE = 2365\n    LIGHTOFAIUR_LIGHTOFAIUR = 2366\n    EFFECT_MASSRECALL_MOTHERSHIP = 2368\n    LOAD_NYDUSWORM = 2370\n    UNLOADALL_NYDUSWORM = 2371\n    BEHAVIOR_PULSARBEAMON = 2375\n    BEHAVIOR_PULSARBEAMOFF = 2376\n    PULSARBEAM_RIPFIELD = 2377\n    PULSARCANNON_PULSARCANNON = 2379\n    VOIDSWARMHOSTSPAWNLOCUST_VOIDSWARMHOSTSPAWNLOCUST = 2381\n    LOCUSTMPFLYINGMORPHTOGROUND_LOCUSTMPFLYINGSWOOP = 2383\n    LOCUSTMPMORPHTOAIR_LOCUSTMPFLYINGSWOOP = 2385\n    EFFECT_LOCUSTSWOOP = 2387\n    HALLUCINATION_DISRUPTOR = 2389\n    HALLUCINATION_ADEPT = 2391\n    EFFECT_VOIDRAYPRISMATICALIGNMENT = 2393\n    SEEKERDUMMYCHANNEL_SEEKERDUMMYCHANNEL = 2395\n    AIURLIGHTBRIDGENE8OUT_BRIDGEEXTEND = 2397\n    AIURLIGHTBRIDGENE8_BRIDGERETRACT = 2399\n    AIURLIGHTBRIDGENE10OUT_BRIDGEEXTEND = 2401\n    AIURLIGHTBRIDGENE10_BRIDGERETRACT = 2403\n    AIURLIGHTBRIDGENE12OUT_BRIDGEEXTEND = 2405\n    AIURLIGHTBRIDGENE12_BRIDGERETRACT = 2407\n    AIURLIGHTBRIDGENW8OUT_BRIDGEEXTEND = 2409\n    AIURLIGHTBRIDGENW8_BRIDGERETRACT = 2411\n    AIURLIGHTBRIDGENW10OUT_BRIDGEEXTEND = 2413\n    AIURLIGHTBRIDGENW10_BRIDGERETRACT = 2415\n    AIURLIGHTBRIDGENW12OUT_BRIDGEEXTEND = 2417\n    AIURLIGHTBRIDGENW12_BRIDGERETRACT = 2419\n    AIURTEMPLEBRIDGENE8OUT_BRIDGEEXTEND = 2421\n    AIURTEMPLEBRIDGENE8_BRIDGERETRACT = 2423\n    AIURTEMPLEBRIDGENE10OUT_BRIDGEEXTEND = 2425\n    AIURTEMPLEBRIDGENE10_BRIDGERETRACT = 2427\n    AIURTEMPLEBRIDGENE12OUT_BRIDGEEXTEND = 2429\n    AIURTEMPLEBRIDGENE12_BRIDGERETRACT = 2431\n    AIURTEMPLEBRIDGENW8OUT_BRIDGEEXTEND = 2433\n    AIURTEMPLEBRIDGENW8_BRIDGERETRACT = 2435\n    AIURTEMPLEBRIDGENW10OUT_BRIDGEEXTEND = 2437\n    AIURTEMPLEBRIDGENW10_BRIDGERETRACT = 2439\n    AIURTEMPLEBRIDGENW12OUT_BRIDGEEXTEND = 2441\n    AIURTEMPLEBRIDGENW12_BRIDGERETRACT = 2443\n    SHAKURASLIGHTBRIDGENE8OUT_BRIDGEEXTEND = 2445\n    SHAKURASLIGHTBRIDGENE8_BRIDGERETRACT = 2447\n    SHAKURASLIGHTBRIDGENE10OUT_BRIDGEEXTEND = 2449\n    SHAKURASLIGHTBRIDGENE10_BRIDGERETRACT = 2451\n    SHAKURASLIGHTBRIDGENE12OUT_BRIDGEEXTEND = 2453\n    SHAKURASLIGHTBRIDGENE12_BRIDGERETRACT = 2455\n    SHAKURASLIGHTBRIDGENW8OUT_BRIDGEEXTEND = 2457\n    SHAKURASLIGHTBRIDGENW8_BRIDGERETRACT = 2459\n    SHAKURASLIGHTBRIDGENW10OUT_BRIDGEEXTEND = 2461\n    SHAKURASLIGHTBRIDGENW10_BRIDGERETRACT = 2463\n    SHAKURASLIGHTBRIDGENW12OUT_BRIDGEEXTEND = 2465\n    SHAKURASLIGHTBRIDGENW12_BRIDGERETRACT = 2467\n    VOIDMPIMMORTALREVIVEREBUILD_IMMORTAL = 2469\n    VOIDMPIMMORTALREVIVEDEATH_IMMORTAL = 2471\n    ARBITERMPSTASISFIELD_ARBITERMPSTASISFIELD = 2473\n    ARBITERMPRECALL_ARBITERMPRECALL = 2475\n    CORSAIRMPDISRUPTIONWEB_CORSAIRMPDISRUPTIONWEB = 2477\n    MORPHTOGUARDIANMP_MORPHTOGUARDIANMP = 2479\n    MORPHTOGUARDIANMP_CANCEL = 2480\n    MORPHTODEVOURERMP_MORPHTODEVOURERMP = 2481\n    MORPHTODEVOURERMP_CANCEL = 2482\n    DEFILERMPCONSUME_DEFILERMPCONSUME = 2483\n    DEFILERMPDARKSWARM_DEFILERMPDARKSWARM = 2485\n    DEFILERMPPLAGUE_DEFILERMPPLAGUE = 2487\n    DEFILERMPBURROW_BURROWDOWN = 2489\n    DEFILERMPBURROW_CANCEL = 2490\n    DEFILERMPUNBURROW_BURROWUP = 2491\n    QUEENMPENSNARE_QUEENMPENSNARE = 2493\n    QUEENMPSPAWNBROODLINGS_QUEENMPSPAWNBROODLINGS = 2495\n    QUEENMPINFESTCOMMANDCENTER_QUEENMPINFESTCOMMANDCENTER = 2497\n    LIGHTNINGBOMB_LIGHTNINGBOMB = 2499\n    GRAPPLE_GRAPPLE = 2501\n    ORACLESTASISTRAP_ORACLEBUILDSTASISTRAP = 2503\n    BUILD_STASISTRAP = 2505\n    CANCEL_STASISTRAP = 2535\n    ORACLESTASISTRAPACTIVATE_ACTIVATESTASISWARD = 2536\n    SELFREPAIR_SELFREPAIR = 2538\n    SELFREPAIR_CANCEL = 2539\n    AGGRESSIVEMUTATION_AGGRESSIVEMUTATION = 2540\n    PARASITICBOMB_PARASITICBOMB = 2542\n    ADEPTPHASESHIFT_ADEPTPHASESHIFT = 2544\n    PURIFICATIONNOVAMORPH_PURIFICATIONNOVA = 2546\n    PURIFICATIONNOVAMORPHBACK_PURIFICATIONNOVA = 2548\n    BEHAVIOR_HOLDFIREON_LURKER = 2550\n    BEHAVIOR_HOLDFIREOFF_LURKER = 2552\n    LIBERATORMORPHTOAG_LIBERATORAGMODE = 2554\n    LIBERATORMORPHTOAA_LIBERATORAAMODE = 2556\n    MORPH_LIBERATORAGMODE = 2558\n    MORPH_LIBERATORAAMODE = 2560\n    TIMESTOP_TIMESTOP = 2562\n    TIMESTOP_CANCEL = 2563\n    AIURLIGHTBRIDGEABANDONEDNE8OUT_BRIDGEEXTEND = 2564\n    AIURLIGHTBRIDGEABANDONEDNE8_BRIDGERETRACT = 2566\n    AIURLIGHTBRIDGEABANDONEDNE10OUT_BRIDGEEXTEND = 2568\n    AIURLIGHTBRIDGEABANDONEDNE10_BRIDGERETRACT = 2570\n    AIURLIGHTBRIDGEABANDONEDNE12OUT_BRIDGEEXTEND = 2572\n    AIURLIGHTBRIDGEABANDONEDNE12_BRIDGERETRACT = 2574\n    AIURLIGHTBRIDGEABANDONEDNW8OUT_BRIDGEEXTEND = 2576\n    AIURLIGHTBRIDGEABANDONEDNW8_BRIDGERETRACT = 2578\n    AIURLIGHTBRIDGEABANDONEDNW10OUT_BRIDGEEXTEND = 2580\n    AIURLIGHTBRIDGEABANDONEDNW10_BRIDGERETRACT = 2582\n    AIURLIGHTBRIDGEABANDONEDNW12OUT_BRIDGEEXTEND = 2584\n    AIURLIGHTBRIDGEABANDONEDNW12_BRIDGERETRACT = 2586\n    KD8CHARGE_KD8CHARGE = 2588\n    PENETRATINGSHOT_PENETRATINGSHOT = 2590\n    CLOAKINGDRONE_CLOAKINGDRONE = 2592\n    CANCEL_ADEPTPHASESHIFT = 2594\n    CANCEL_ADEPTSHADEPHASESHIFT = 2596\n    SLAYNELEMENTALGRAB_SLAYNELEMENTALGRAB = 2598\n    MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS = 2600\n    MORPHTOCOLLAPSIBLEPURIFIERTOWERDEBRIS_CANCEL = 2601\n    PORTCITY_BRIDGE_UNITNE8OUT_BRIDGEEXTEND = 2602\n    PORTCITY_BRIDGE_UNITNE8_BRIDGERETRACT = 2604\n    PORTCITY_BRIDGE_UNITSE8OUT_BRIDGEEXTEND = 2606\n    PORTCITY_BRIDGE_UNITSE8_BRIDGERETRACT = 2608\n    PORTCITY_BRIDGE_UNITNW8OUT_BRIDGEEXTEND = 2610\n    PORTCITY_BRIDGE_UNITNW8_BRIDGERETRACT = 2612\n    PORTCITY_BRIDGE_UNITSW8OUT_BRIDGEEXTEND = 2614\n    PORTCITY_BRIDGE_UNITSW8_BRIDGERETRACT = 2616\n    PORTCITY_BRIDGE_UNITNE10OUT_BRIDGEEXTEND = 2618\n    PORTCITY_BRIDGE_UNITNE10_BRIDGERETRACT = 2620\n    PORTCITY_BRIDGE_UNITSE10OUT_BRIDGEEXTEND = 2622\n    PORTCITY_BRIDGE_UNITSE10_BRIDGERETRACT = 2624\n    PORTCITY_BRIDGE_UNITNW10OUT_BRIDGEEXTEND = 2626\n    PORTCITY_BRIDGE_UNITNW10_BRIDGERETRACT = 2628\n    PORTCITY_BRIDGE_UNITSW10OUT_BRIDGEEXTEND = 2630\n    PORTCITY_BRIDGE_UNITSW10_BRIDGERETRACT = 2632\n    PORTCITY_BRIDGE_UNITNE12OUT_BRIDGEEXTEND = 2634\n    PORTCITY_BRIDGE_UNITNE12_BRIDGERETRACT = 2636\n    PORTCITY_BRIDGE_UNITSE12OUT_BRIDGEEXTEND = 2638\n    PORTCITY_BRIDGE_UNITSE12_BRIDGERETRACT = 2640\n    PORTCITY_BRIDGE_UNITNW12OUT_BRIDGEEXTEND = 2642\n    PORTCITY_BRIDGE_UNITNW12_BRIDGERETRACT = 2644\n    PORTCITY_BRIDGE_UNITSW12OUT_BRIDGEEXTEND = 2646\n    PORTCITY_BRIDGE_UNITSW12_BRIDGERETRACT = 2648\n    PORTCITY_BRIDGE_UNITN8OUT_BRIDGEEXTEND = 2650\n    PORTCITY_BRIDGE_UNITN8_BRIDGERETRACT = 2652\n    PORTCITY_BRIDGE_UNITS8OUT_BRIDGEEXTEND = 2654\n    PORTCITY_BRIDGE_UNITS8_BRIDGERETRACT = 2656\n    PORTCITY_BRIDGE_UNITE8OUT_BRIDGEEXTEND = 2658\n    PORTCITY_BRIDGE_UNITE8_BRIDGERETRACT = 2660\n    PORTCITY_BRIDGE_UNITW8OUT_BRIDGEEXTEND = 2662\n    PORTCITY_BRIDGE_UNITW8_BRIDGERETRACT = 2664\n    PORTCITY_BRIDGE_UNITN10OUT_BRIDGEEXTEND = 2666\n    PORTCITY_BRIDGE_UNITN10_BRIDGERETRACT = 2668\n    PORTCITY_BRIDGE_UNITS10OUT_BRIDGEEXTEND = 2670\n    PORTCITY_BRIDGE_UNITS10_BRIDGERETRACT = 2672\n    PORTCITY_BRIDGE_UNITE10OUT_BRIDGEEXTEND = 2674\n    PORTCITY_BRIDGE_UNITE10_BRIDGERETRACT = 2676\n    PORTCITY_BRIDGE_UNITW10OUT_BRIDGEEXTEND = 2678\n    PORTCITY_BRIDGE_UNITW10_BRIDGERETRACT = 2680\n    PORTCITY_BRIDGE_UNITN12OUT_BRIDGEEXTEND = 2682\n    PORTCITY_BRIDGE_UNITN12_BRIDGERETRACT = 2684\n    PORTCITY_BRIDGE_UNITS12OUT_BRIDGEEXTEND = 2686\n    PORTCITY_BRIDGE_UNITS12_BRIDGERETRACT = 2688\n    PORTCITY_BRIDGE_UNITE12OUT_BRIDGEEXTEND = 2690\n    PORTCITY_BRIDGE_UNITE12_BRIDGERETRACT = 2692\n    PORTCITY_BRIDGE_UNITW12OUT_BRIDGEEXTEND = 2694\n    PORTCITY_BRIDGE_UNITW12_BRIDGERETRACT = 2696\n    TEMPESTDISRUPTIONBLAST_TEMPESTDISRUPTIONBLAST = 2698\n    CANCEL_TEMPESTDISRUPTIONBLAST = 2699\n    EFFECT_SHADOWSTRIDE = 2700\n    LAUNCHINTERCEPTORS_LAUNCHINTERCEPTORS = 2702\n    EFFECT_SPAWNLOCUSTS = 2704\n    LOCUSTMPFLYINGSWOOPATTACK_LOCUSTMPFLYINGSWOOP = 2706\n    MORPH_OVERLORDTRANSPORT = 2708\n    CANCEL_MORPHOVERLORDTRANSPORT = 2709\n    BYPASSARMOR_BYPASSARMOR = 2710\n    BYPASSARMORDRONECU_BYPASSARMORDRONECU = 2712\n    EFFECT_GHOSTSNIPE = 2714\n    CHANNELSNIPE_CANCEL = 2715\n    PURIFYMORPHPYLON_MOTHERSHIPCOREWEAPON = 2716\n    PURIFYMORPHPYLONBACK_MOTHERSHIPCOREWEAPON = 2718\n    RESEARCH_SHADOWSTRIKE = 2720\n    HEAL_MEDICHEAL = 2750\n    LURKERASPECT_LURKER = 2752\n    LURKERASPECT_CANCEL = 2753\n    BURROWLURKERDOWN_BURROWDOWN = 2754\n    BURROWLURKERDOWN_CANCEL = 2755\n    BURROWLURKERUP_BURROWUP = 2756\n    D8CHARGE_D8CHARGE = 2758\n    DEFENSIVEMATRIX_DEFENSIVEMATRIX = 2760\n    MISSILEPODS_MISSILEPODS = 2762\n    LOKIMISSILEPODS_MISSILEPODS = 2764\n    HUTTRANSPORT_HUTLOAD = 2766\n    HUTTRANSPORT_HUTUNLOADALL = 2767\n    MORPHTOTECHREACTOR_MORPHTOTECHREACTOR = 2771\n    LEVIATHANSPAWNBROODLORD_SPAWNBROODLORD = 2773\n    SS_CARRIERBOSSATTACKLAUNCH_SS_SHOOTING = 2775\n    SS_CARRIERSPAWNINTERCEPTOR_SS_CARRIERSPAWNINTERCEPTOR = 2777\n    SS_CARRIERBOSSATTACKTARGET_SS_SHOOTING = 2779\n    SS_FIGHTERBOMB_SS_FIGHTERBOMB = 2781\n    SS_LIGHTNINGPROJECTORTOGGLE_SS_LIGHTNINGPROJECTORTOGGLE = 2783\n    SS_PHOENIXSHOOTING_SS_SHOOTING = 2785\n    SS_POWERUPMORPHTOBOMB_SS_POWERUPMORPHTOBOMB = 2787\n    SS_BATTLECRUISERMISSILEATTACK_SS_SHOOTING = 2789\n    SS_LEVIATHANSPAWNBOMBS_SS_LEVIATHANSPAWNBOMBS = 2791\n    SS_BATTLECRUISERHUNTERSEEKERATTACK_SS_SHOOTING = 2793\n    SS_POWERUPMORPHTOHEALTH_SS_POWERUPMORPHTOHEALTH = 2795\n    SS_LEVIATHANTENTACLEATTACKL1NODELAY_SS_LEVIATHANTENTACLEATTACKL1NODELAY = 2797\n    SS_LEVIATHANTENTACLEATTACKL2NODELAY_SS_LEVIATHANTENTACLEATTACKL2NODELAY = 2799\n    SS_LEVIATHANTENTACLEATTACKR1NODELAY_SS_LEVIATHANTENTACLEATTACKR1NODELAY = 2801\n    SS_LEVIATHANTENTACLEATTACKR2NODELAY_SS_LEVIATHANTENTACLEATTACKR2NODELAY = 2803\n    SS_SCIENCEVESSELTELEPORT_ZERATULBLINK = 2805\n    SS_TERRATRONBEAMATTACK_SS_TERRATRONBEAMATTACK = 2807\n    SS_TERRATRONSAWATTACK_SS_TERRATRONSAWATTACK = 2809\n    SS_WRAITHATTACK_SS_SHOOTING = 2811\n    SS_SWARMGUARDIANATTACK_SS_SHOOTING = 2813\n    SS_POWERUPMORPHTOSIDEMISSILES_SS_POWERUPMORPHTOSIDEMISSILES = 2815\n    SS_POWERUPMORPHTOSTRONGERMISSILES_SS_POWERUPMORPHTOSTRONGERMISSILES = 2817\n    SS_SCOUTATTACK_SS_SHOOTING = 2819\n    SS_INTERCEPTORATTACK_SS_SHOOTING = 2821\n    SS_CORRUPTORATTACK_SS_SHOOTING = 2823\n    SS_LEVIATHANTENTACLEATTACKL2_SS_LEVIATHANTENTACLEATTACKL2 = 2825\n    SS_LEVIATHANTENTACLEATTACKR1_SS_LEVIATHANTENTACLEATTACKR1 = 2827\n    SS_LEVIATHANTENTACLEATTACKL1_SS_LEVIATHANTENTACLEATTACKL1 = 2829\n    SS_LEVIATHANTENTACLEATTACKR2_SS_LEVIATHANTENTACLEATTACKR2 = 2831\n    SS_SCIENCEVESSELATTACK_SS_SHOOTING = 2833\n    HEALREDIRECT_HEALREDIRECT = 2835\n    LURKERASPECTFROMHYDRALISKBURROWED_LURKERFROMHYDRALISKBURROWED = 2836\n    LURKERASPECTFROMHYDRALISKBURROWED_CANCEL = 2837\n    UPGRADETOLURKERDEN_LURKERDEN = 2838\n    UPGRADETOLURKERDEN_CANCEL = 2839\n    ADVANCEDCONSTRUCTION_CANCEL = 2840\n    BUILDINPROGRESSNONCANCELLABLE_CANCEL = 2842\n    INFESTEDVENTSPAWNCORRUPTOR_SPAWNCORRUPTOR = 2844\n    INFESTEDVENTSPAWNBROODLORD_SPAWNBROODLORD = 2846\n    IRRADIATE_IRRADIATE = 2848\n    IRRADIATE_CANCEL = 2849\n    INFESTEDVENTSPAWNMUTALISK_LEVIATHANSPAWNMUTALISK = 2850\n    MAKEVULTURESPIDERMINES_SPIDERMINEREPLENISH = 2852\n    MEDIVACDOUBLEBEAMHEAL_HEAL = 2872\n    MINDCONTROL_MINDCONTROL = 2874\n    OBLITERATE_OBLITERATE = 2876\n    VOODOOSHIELD_VOODOOSHIELD = 2878\n    RELEASEMINION_RELEASEMINION = 2880\n    ULTRASONICPULSE_ULTRASONICPULSE = 2882\n    ARCHIVESEAL_ARCHIVESEAL = 2884\n    ARTANISVORTEX_VORTEX = 2886\n    ARTANISWORMHOLETRANSIT_WORMHOLETRANSIT = 2888\n    BUNKERATTACK_BUNKERATTACK = 2890\n    BUNKERATTACK_ATTACKTOWARDS = 2891\n    BUNKERATTACK_ATTACKBARRAGE = 2892\n    BUNKERSTOP_STOPBUNKER = 2893\n    BUNKERSTOP_HOLDFIRESPECIAL = 2894\n    CANCELTERRAZINEHARVEST_CANCEL = 2899\n    LEVIATHANSPAWNMUTALISK_LEVIATHANSPAWNMUTALISK = 2901\n    PARKCOLONISTVEHICLE_PARKCOLONISTVEHICLE = 2903\n    STARTCOLONISTVEHICLE_STARTCOLONISTVEHICLE = 2905\n    CONSUMPTION_CONSUMPTION = 2907\n    CONSUMEDNA_CONSUMEDNA = 2909\n    EGGPOP_EGGPOP = 2911\n    EXPERIMENTALPLASMAGUN_EXPERIMENTALPLASMAGUN = 2913\n    GATHERSPECIALOBJECT_GATHERSPECIALOBJECT = 2915\n    KERRIGANSEARCH_KERRIGANSEARCH = 2917\n    LOKIUNDOCK_LIFT = 2919\n    MINDBLAST_MINDBLAST = 2921\n    MORPHTOINFESTEDCIVILIAN_MORPHTOINFESTEDCIVILIAN = 2923\n    QUEENSHOCKWAVE_QUEENSHOCKWAVE = 2925\n    TAURENOUTHOUSELIFTOFF_TAURENOUTHOUSEFLY = 2927\n    TAURENOUTHOUSETRANSPORT_LOADTAURENOUTHOUSE = 2929\n    TAURENOUTHOUSETRANSPORT_UNLOADTAURENOUTHOUSE = 2930\n    TYCHUS03OMEGASTORM_OMEGASTORM = 2934\n    RAYNORSNIPE_RAYNORSNIPE = 2936\n    BONESHEAL_BONESHEAL = 2938\n    BONESTOSSGRENADE_TOSSGRENADETYCHUS = 2940\n    HERCULESTRANSPORT_MEDIVACLOAD = 2942\n    HERCULESTRANSPORT_MEDIVACUNLOADALL = 2944\n    SPECOPSDROPSHIPTRANSPORT_MEDIVACLOAD = 2947\n    SPECOPSDROPSHIPTRANSPORT_MEDIVACUNLOADALL = 2949\n    DUSKWINGBANSHEECLOAKINGFIELD_CLOAKONBANSHEE = 2952\n    DUSKWINGBANSHEECLOAKINGFIELD_CLOAKOFF = 2953\n    HYPERIONYAMATOSPECIAL_HYPERIONYAMATOGUN = 2954\n    INFESTABLEHUTTRANSPORT_HUTLOAD = 2956\n    INFESTABLEHUTTRANSPORT_HUTUNLOADALL = 2957\n    DUTCHPLACETURRET_DUTCHPLACETURRET = 2961\n    BURROWINFESTEDCIVILIANDOWN_BURROWDOWN = 2963\n    BURROWINFESTEDCIVILIANUP_BURROWUP = 2965\n    SELENDISHANGAR_INTERCEPTOR = 2967\n    FORCEFIELDBEAM_FORCEFIELDBEAM = 2987\n    SIEGEBREAKERSIEGE_SIEGEMODE = 2989\n    SIEGEBREAKERUNSIEGE_UNSIEGE = 2991\n    SOULCHANNEL_SOULCHANNEL = 2993\n    SOULCHANNEL_CANCEL = 2994\n    PERDITIONTURRETBURROW_PERDITIONTURRETBURROW = 2995\n    PERDITIONTURRETUNBURROW_PERDITIONTURRETUNBURROW = 2997\n    SENTRYGUNBURROW_BURROWTURRET = 2999\n    SENTRYGUNUNBURROW_UNBURROWTURRET = 3001\n    SPIDERMINEUNBURROWRANGEDUMMY_SPIDERMINEUNBURROWRANGEDUMMY = 3003\n    GRAVITONPRISON_GRAVITONPRISON = 3005\n    IMPLOSION_IMPLOSION = 3007\n    OMEGASTORM_OMEGASTORM = 3009\n    PSIONICSHOCKWAVE_PSIONICSHOCKWAVE = 3011\n    HYBRIDFAOESTUN_HYBRIDFAOESTUN = 3013\n    SUMMONMERCENARIES_HIREKELMORIANMINERS = 3015\n    SUMMONMERCENARIES_HIREDEVILDOGS = 3016\n    SUMMONMERCENARIES_HIRESPARTANCOMPANY = 3017\n    SUMMONMERCENARIES_HIREHAMMERSECURITIES = 3018\n    SUMMONMERCENARIES_HIRESIEGEBREAKERS = 3019\n    SUMMONMERCENARIES_HIREHELSANGELS = 3020\n    SUMMONMERCENARIES_HIREDUSKWING = 3021\n    SUMMONMERCENARIES_HIREDUKESREVENGE = 3022\n    SUMMONMERCENARIESPH_HIREKELMORIANMINERSPH = 3045\n    ENERGYNOVA_ENERGYNOVA = 3075\n    THEMOROSDEVICE_THEMOROSDEVICE = 3077\n    TOSSGRENADE_TOSSGRENADE = 3079\n    VOIDSEEKERTRANSPORT_MEDIVACLOAD = 3081\n    VOIDSEEKERTRANSPORT_MEDIVACUNLOADALL = 3083\n    TERRANBUILDDROP_SUPPLYDEPOTDROP = 3086\n    TERRANBUILDDROP_CANCEL = 3116\n    ODINNUCLEARSTRIKE_ODINNUKECALLDOWN = 3117\n    ODINNUCLEARSTRIKE_CANCEL = 3118\n    ODINWRECKAGE_ODIN = 3119\n    RESEARCHLABTRANSPORT_HUTLOAD = 3121\n    RESEARCHLABTRANSPORT_HUTUNLOADALL = 3122\n    COLONYSHIPTRANSPORT_MEDIVACLOAD = 3126\n    COLONYSHIPTRANSPORT_MEDIVACUNLOADALL = 3128\n    COLONYINFESTATION_COLONYINFESTATION = 3131\n    DOMINATION_DOMINATION = 3133\n    DOMINATION_CANCEL = 3134\n    KARASSPLASMASURGE_KARASSPLASMASURGE = 3135\n    KARASSPSISTORM_PSISTORM = 3137\n    HYBRIDBLINK_ZERATULBLINK = 3139\n    HYBRIDCPLASMABLAST_HYBRIDCPLASMABLAST = 3141\n    HEROARMNUKE_NUKEARM = 3143\n    HERONUCLEARSTRIKE_NUKECALLDOWN = 3163\n    HERONUCLEARSTRIKE_CANCEL = 3164\n    ODINBARRAGE_ODINBARRAGE = 3165\n    ODINBARRAGE_CANCEL = 3166\n    PURIFIERTOGGLEPOWER_PURIFIERPOWERDOWN = 3167\n    PURIFIERTOGGLEPOWER_PURIFIERPOWERUP = 3168\n    PHASEMINEBLAST_PHASEMINEBLAST = 3169\n    VOIDSEEKERPHASEMINEBLAST_PHASEMINEBLAST = 3171\n    TRANSPORTTRUCKTRANSPORT_TRANSPORTTRUCKLOAD = 3173\n    TRANSPORTTRUCKTRANSPORT_TRANSPORTTRUCKUNLOADALL = 3174\n    VAL03QUEENOFBLADESBURROW_BURROWDOWN = 3178\n    VAL03QUEENOFBLADESDEEPTUNNEL_DEEPTUNNEL = 3180\n    VAL03QUEENOFBLADESUNBURROW_BURROWUP = 3182\n    VULTURESPIDERMINEBURROW_VULTURESPIDERMINEBURROW = 3184\n    VULTURESPIDERMINEUNBURROW_VULTURESPIDERMINEUNBURROW = 3186\n    LOKIYAMATO_LOKIYAMATOGUN = 3188\n    DUKESREVENGEYAMATO_YAMATOGUN = 3190\n    ZERATULBLINK_ZERATULBLINK = 3192\n    ROGUEGHOSTCLOAK_CLOAKONSPECTRE = 3194\n    ROGUEGHOSTCLOAK_CLOAKOFF = 3195\n    VULTURESPIDERMINES_SPIDERMINE = 3196\n    VULTUREQUEUE3_CANCEL = 3198\n    VULTUREQUEUE3_CANCELSLOT = 3199\n    SUPERWARPGATETRAIN_ZEALOT = 3200\n    SUPERWARPGATETRAIN_STALKER = 3201\n    SUPERWARPGATETRAIN_IMMORTAL = 3202\n    SUPERWARPGATETRAIN_HIGHTEMPLAR = 3203\n    SUPERWARPGATETRAIN_DARKTEMPLAR = 3204\n    SUPERWARPGATETRAIN_SENTRY = 3205\n    SUPERWARPGATETRAIN_CARRIER = 3206\n    SUPERWARPGATETRAIN_PHOENIX = 3207\n    SUPERWARPGATETRAIN_VOIDRAY = 3208\n    SUPERWARPGATETRAIN_ARCHON = 3209\n    SUPERWARPGATETRAIN_WARPINZERATUL = 3210\n    SUPERWARPGATETRAIN_WARPINURUN = 3211\n    SUPERWARPGATETRAIN_WARPINMOHANDAR = 3212\n    SUPERWARPGATETRAIN_WARPINSELENDIS = 3213\n    SUPERWARPGATETRAIN_WARPINSCOUT = 3214\n    SUPERWARPGATETRAIN_COLOSSUS = 3215\n    SUPERWARPGATETRAIN_WARPPRISM = 3216\n    BURROWOMEGALISKDOWN_BURROWDOWN = 3220\n    BURROWOMEGALISKUP_BURROWUP = 3222\n    BURROWINFESTEDABOMINATIONDOWN_BURROWDOWN = 3224\n    BURROWINFESTEDABOMINATIONUP_BURROWUP = 3226\n    BURROWHUNTERKILLERDOWN_BURROWDOWN = 3228\n    BURROWHUNTERKILLERDOWN_CANCEL = 3229\n    BURROWHUNTERKILLERUP_BURROWUP = 3230\n    NOVASNIPE_NOVASNIPE = 3232\n    VORTEXPURIFIER_VORTEX = 3234\n    TALDARIMVORTEX_VORTEX = 3236\n    PURIFIERPLANETCRACKER_PLANETCRACKER = 3238\n    BURROWINFESTEDTERRANCAMPAIGNDOWN_BURROWDOWN = 3240\n    BURROWINFESTEDTERRANCAMPAIGNUP_BURROWUP = 3242\n    INFESTEDMONSTERTRAIN_INFESTEDCIVILIAN = 3244\n    INFESTEDMONSTERTRAIN_INFESTEDTERRANCAMPAIGN = 3245\n    INFESTEDMONSTERTRAIN_INFESTEDABOMINATION = 3246\n    BIODOMETRANSPORT_BIODOMELOAD = 3274\n    BIODOMETRANSPORT_BIODOMEUNLOADALL = 3275\n    CHECKSTATION_CHECKSTATION = 3279\n    CHECKSTATIONDIAGONALBLUR_CHECKSTATIONDIAGONALBLUR = 3281\n    CHECKSTATIONDIAGONALULBR_CHECKSTATIONDIAGONALULBR = 3283\n    CHECKSTATIONVERTICAL_CHECKSTATIONVERTICAL = 3285\n    CHECKSTATIONOPENED_CHECKSTATIONOPENED = 3287\n    CHECKSTATIONDIAGONALBLUROPENED_CHECKSTATIONDIAGONALBLUROPENED = 3289\n    CHECKSTATIONDIAGONALULBROPENED_CHECKSTATIONDIAGONALULBROPENED = 3291\n    CHECKSTATIONVERTICALOPENED_CHECKSTATIONVERTICALOPENED = 3293\n    ATTACKALLOWSINVULNERABLE_ATTACKALLOWSINVULNERABLE = 3295\n    ATTACKALLOWSINVULNERABLE_ATTACKTOWARDS = 3296\n    ATTACKALLOWSINVULNERABLE_ATTACKBARRAGE = 3297\n    ZERATULSTUN_ZERATULSTUN = 3298\n    WRAITHCLOAK_WRAITHCLOAK = 3300\n    WRAITHCLOAK_CLOAKOFF = 3301\n    TECHREACTORMORPH_TECHREACTORMORPH = 3302\n    BARRACKSTECHREACTORMORPH_TECHLABBARRACKS = 3304\n    FACTORYTECHREACTORMORPH_TECHLABFACTORY = 3306\n    STARPORTTECHREACTORMORPH_TECHLABSTARPORT = 3308\n    SS_FIGHTERSHOOTING_SS_SHOOTING = 3310\n    RAYNORC4_PLANTC4CHARGE = 3312\n    DUKESREVENGEDEFENSIVEMATRIX_DEFENSIVEMATRIX = 3314\n    DUKESREVENGEMISSILEPODS_MISSILEPODS = 3316\n    THORWRECKAGE_THOR = 3318\n    _330MMBARRAGECANNONS_330MMBARRAGECANNONS = 3320\n    _330MMBARRAGECANNONS_CANCEL = 3321\n    THORREBORN_THOR = 3322\n    THORREBORN_CANCEL = 3323\n    SPECTRENUKE_SPECTRENUKECALLDOWN = 3324\n    SPECTRENUKE_CANCEL = 3325\n    SPECTRENUKESILOARMMAGAZINE_SPECTRENUKESILOARMMAGAZINE = 3326\n    SPECTRENUKESILOARMMAGAZINE_SPECTRENUKEARM = 3327\n    COLONISTSHIPLIFTOFF_LIFT = 3346\n    COLONISTSHIPLAND_LAND = 3348\n    BIODOMECOMMANDLIFTOFF_LIFT = 3350\n    BIODOMECOMMANDLAND_LAND = 3352\n    HERCULESLIFTOFF_LIFT = 3354\n    HERCULESLAND_HERCULESLAND = 3356\n    LIGHTBRIDGEOFF_LIGHTBRIDGEOFF = 3358\n    LIGHTBRIDGEON_LIGHTBRIDGEON = 3360\n    LIBRARYDOWN_LIBRARYDOWN = 3362\n    LIBRARYUP_LIBRARYUP = 3364\n    TEMPLEDOORDOWN_TEMPLEDOORDOWN = 3366\n    TEMPLEDOORUP_TEMPLEDOORUP = 3368\n    TEMPLEDOORDOWNURDL_TEMPLEDOORDOWNURDL = 3370\n    TEMPLEDOORUPURDL_TEMPLEDOORUPURDL = 3372\n    PSYTROUSOXIDE_PSYTROUSOXIDEON = 3374\n    PSYTROUSOXIDE_PSYTROUSOXIDEOFF = 3375\n    VOIDSEEKERDOCK_VOIDSEEKERDOCK = 3376\n    BIOPLASMIDDISCHARGE_BIOPLASMIDDISCHARGE = 3378\n    WRECKINGCREWASSAULTMODE_ASSAULTMODE = 3380\n    WRECKINGCREWFIGHTERMODE_FIGHTERMODE = 3382\n    BIOSTASIS_BIOSTASIS = 3384\n    COLONISTTRANSPORTTRANSPORT_COLONISTTRANSPORTLOAD = 3386\n    COLONISTTRANSPORTTRANSPORT_COLONISTTRANSPORTUNLOADALL = 3387\n    DROPTOSUPPLYDEPOT_RAISE = 3391\n    REFINERYTOAUTOMATEDREFINERY_RAISE = 3393\n    HELIOSCRASHMORPH_CRASHMORPH = 3395\n    NANOREPAIR_HEAL = 3397\n    PICKUP_PICKUP = 3399\n    PICKUPARCADE_PICKUP = 3401\n    PICKUPGAS100_PICKUPGAS100 = 3403\n    PICKUPMINERALS100_PICKUPMINERALS100 = 3405\n    PICKUPHEALTH25_PICKUPHEALTH25 = 3407\n    PICKUPHEALTH50_PICKUPHEALTH50 = 3409\n    PICKUPHEALTH100_PICKUPHEALTH100 = 3411\n    PICKUPHEALTHFULL_PICKUPHEALTHFULL = 3413\n    PICKUPENERGY25_PICKUPENERGY25 = 3415\n    PICKUPENERGY50_PICKUPENERGY50 = 3417\n    PICKUPENERGY100_PICKUPENERGY100 = 3419\n    PICKUPENERGYFULL_PICKUPENERGYFULL = 3421\n    TAURENSTIMPACK_STIM = 3423\n    TESTINVENTORY_TESTINVENTORY = 3425\n    TESTPAWN_TESTPAWN = 3434\n    TESTREVIVE_SCV = 3454\n    TESTSELL_TESTSELL = 3484\n    TESTINTERACT_DESIGNATE = 3514\n    CLIFFDOOROPEN0_SPACEPLATFORMDOOROPEN = 3515\n    CLIFFDOORCLOSE0_SPACEPLATFORMDOORCLOSE = 3517\n    CLIFFDOOROPEN1_SPACEPLATFORMDOOROPEN = 3519\n    CLIFFDOORCLOSE1_SPACEPLATFORMDOORCLOSE = 3521\n    DESTRUCTIBLEGATEDIAGONALBLURLOWERED_GATEOPEN = 3523\n    DESTRUCTIBLEGATEDIAGONALULBRLOWERED_GATEOPEN = 3525\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3527\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3529\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3531\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3533\n    DESTRUCTIBLEGATEDIAGONALBLUR_GATECLOSE = 3535\n    DESTRUCTIBLEGATEDIAGONALULBR_GATECLOSE = 3537\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3539\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTAL_GATECLOSE = 3541\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLF_GATECLOSE = 3543\n    DESTRUCTIBLEGATESTRAIGHTVERTICAL_GATECLOSE = 3545\n    TESTLEARN_TESTLEARN = 3547\n    TESTLEVELEDSPELL_YAMATOGUN = 3567\n    METALGATEDIAGONALBLURLOWERED_GATEOPEN = 3569\n    METALGATEDIAGONALULBRLOWERED_GATEOPEN = 3571\n    METALGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3573\n    METALGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3575\n    METALGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3577\n    METALGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3579\n    METALGATEDIAGONALBLUR_GATECLOSE = 3581\n    METALGATEDIAGONALULBR_GATECLOSE = 3583\n    METALGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3585\n    METALGATESTRAIGHTHORIZONTAL_GATECLOSE = 3587\n    METALGATESTRAIGHTVERTICALLF_GATECLOSE = 3589\n    METALGATESTRAIGHTVERTICAL_GATECLOSE = 3591\n    SECURITYGATEDIAGONALBLURLOWERED_GATEOPEN = 3593\n    SECURITYGATEDIAGONALULBRLOWERED_GATEOPEN = 3595\n    SECURITYGATESTRAIGHTHORIZONTALBFLOWERED_GATEOPEN = 3597\n    SECURITYGATESTRAIGHTHORIZONTALLOWERED_GATEOPEN = 3599\n    SECURITYGATESTRAIGHTVERTICALLFLOWERED_GATEOPEN = 3601\n    SECURITYGATESTRAIGHTVERTICALLOWERED_GATEOPEN = 3603\n    SECURITYGATEDIAGONALBLUR_GATECLOSE = 3605\n    SECURITYGATEDIAGONALULBR_GATECLOSE = 3607\n    SECURITYGATESTRAIGHTHORIZONTALBF_GATECLOSE = 3609\n    SECURITYGATESTRAIGHTHORIZONTAL_GATECLOSE = 3611\n    SECURITYGATESTRAIGHTVERTICALLF_GATECLOSE = 3613\n    SECURITYGATESTRAIGHTVERTICAL_GATECLOSE = 3615\n    CHANGESHRINETERRAN_CHANGESHRINETERRAN = 3617\n    CHANGESHRINEPROTOSS_CHANGESHRINEPROTOSS = 3619\n    SPECTREHOLDFIRE_SPECTREHOLDFIRE = 3621\n    SPECTREWEAPONSFREE_WEAPONSFREE = 3623\n    GWALEARN_TESTLEARN = 3625\n    REAPERPLACEMENTMORPH_REAPERPLACEMENTMORPH = 3645\n    LIGHTBRIDGEOFFTOPRIGHT_LIGHTBRIDGEOFF = 3647\n    LIGHTBRIDGEONTOPRIGHT_LIGHTBRIDGEON = 3649\n    TESTHEROGRAB_GRABZERGLING = 3651\n    TESTHEROTHROW_THROWZERGLING = 3653\n    TESTHERODEBUGMISSILEABILITY_TESTHERODEBUGMISSILEABILITY = 3655\n    TESTHERODEBUGTRACKINGABILITY_TESTHERODEBUGTRACKINGABILITY = 3657\n    TESTHERODEBUGTRACKINGABILITY_CANCEL = 3658\n    CANCEL = 3659\n    HALT = 3660\n    BURROWDOWN = 3661\n    BURROWUP = 3662\n    LOADALL = 3663\n    UNLOADALL = 3664\n    STOP = 3665\n    HARVEST_GATHER = 3666\n    HARVEST_RETURN = 3667\n    LOAD = 3668\n    UNLOADALLAT = 3669\n    CANCEL_LAST = 3671\n    CANCEL_SLOT = 3672\n    RALLY_UNITS = 3673\n    ATTACK = 3674\n    EFFECT_STIM = 3675\n    BEHAVIOR_CLOAKON = 3676\n    BEHAVIOR_CLOAKOFF = 3677\n    LAND = 3678\n    LIFT = 3679\n    MORPH_ROOT = 3680\n    MORPH_UPROOT = 3681\n    BUILD_TECHLAB = 3682\n    BUILD_REACTOR = 3683\n    EFFECT_SPRAY = 3684\n    EFFECT_REPAIR = 3685\n    EFFECT_MASSRECALL = 3686\n    EFFECT_BLINK = 3687\n    BEHAVIOR_HOLDFIREON = 3688\n    BEHAVIOR_HOLDFIREOFF = 3689\n    RALLY_WORKERS = 3690\n    BUILD_CREEPTUMOR = 3691\n    RESEARCH_PROTOSSAIRARMOR = 3692\n    RESEARCH_PROTOSSAIRWEAPONS = 3693\n    RESEARCH_PROTOSSGROUNDARMOR = 3694\n    RESEARCH_PROTOSSGROUNDWEAPONS = 3695\n    RESEARCH_PROTOSSSHIELDS = 3696\n    RESEARCH_TERRANINFANTRYARMOR = 3697\n    RESEARCH_TERRANINFANTRYWEAPONS = 3698\n    RESEARCH_TERRANSHIPWEAPONS = 3699\n    RESEARCH_TERRANVEHICLEANDSHIPPLATING = 3700\n    RESEARCH_TERRANVEHICLEWEAPONS = 3701\n    RESEARCH_ZERGFLYERARMOR = 3702\n    RESEARCH_ZERGFLYERATTACK = 3703\n    RESEARCH_ZERGGROUNDARMOR = 3704\n    RESEARCH_ZERGMELEEWEAPONS = 3705\n    RESEARCH_ZERGMISSILEWEAPONS = 3706\n    CANCEL_VOIDRAYPRISMATICALIGNMENT = 3707\n    RESEARCH_ADAPTIVETALONS = 3709\n    LURKERDENRESEARCH_RESEARCHLURKERRANGE = 3710\n    MORPH_OBSERVERMODE = 3739\n    MORPH_SURVEILLANCEMODE = 3741\n    MORPH_OVERSIGHTMODE = 3743\n    MORPH_OVERSEERMODE = 3745\n    EFFECT_INTERFERENCEMATRIX = 3747\n    EFFECT_REPAIRDRONE = 3749\n    EFFECT_REPAIR_REPAIRDRONE = 3751\n    EFFECT_ANTIARMORMISSILE = 3753\n    EFFECT_CHRONOBOOSTENERGYCOST = 3755\n    EFFECT_MASSRECALL_NEXUS = 3757\n    NEXUSSHIELDRECHARGE_NEXUSSHIELDRECHARGE = 3759\n    NEXUSSHIELDRECHARGEONPYLON_NEXUSSHIELDRECHARGEONPYLON = 3761\n    INFESTORENSNARE_INFESTORENSNARE = 3763\n    EFFECT_RESTORE = 3765\n    NEXUSSHIELDOVERCHARGE_NEXUSSHIELDOVERCHARGE = 3767\n    NEXUSSHIELDOVERCHARGEOFF_NEXUSSHIELDOVERCHARGEOFF = 3769\n    ATTACK_BATTLECRUISER = 3771\n    BATTLECRUISERATTACK_ATTACKTOWARDS = 3772\n    BATTLECRUISERATTACK_ATTACKBARRAGE = 3773\n    BATTLECRUISERATTACKEVALUATOR_MOTHERSHIPCOREATTACK = 3774\n    MOVE_BATTLECRUISER = 3776\n    PATROL_BATTLECRUISER = 3777\n    HOLDPOSITION_BATTLECRUISER = 3778\n    BATTLECRUISERMOVE_ACQUIREMOVE = 3779\n    BATTLECRUISERMOVE_TURN = 3780\n    BATTLECRUISERSTOPEVALUATOR_STOP = 3781\n    STOP_BATTLECRUISER = 3783\n    BATTLECRUISERSTOP_HOLDFIRE = 3784\n    BATTLECRUISERSTOP_CHEER = 3785\n    BATTLECRUISERSTOP_DANCE = 3786\n    VIPERPARASITICBOMBRELAY_PARASITICBOMB = 3789\n    PARASITICBOMBRELAYDODGE_PARASITICBOMB = 3791\n    HOLDPOSITION = 3793\n    MOVE = 3794\n    PATROL = 3795\n    UNLOADUNIT = 3796\n    LOADOUTSPRAY_LOADOUTSPRAY1 = 3797\n    LOADOUTSPRAY_LOADOUTSPRAY2 = 3798\n    LOADOUTSPRAY_LOADOUTSPRAY3 = 3799\n    LOADOUTSPRAY_LOADOUTSPRAY4 = 3800\n    LOADOUTSPRAY_LOADOUTSPRAY5 = 3801\n    LOADOUTSPRAY_LOADOUTSPRAY6 = 3802\n    LOADOUTSPRAY_LOADOUTSPRAY7 = 3803\n    LOADOUTSPRAY_LOADOUTSPRAY8 = 3804\n    LOADOUTSPRAY_LOADOUTSPRAY9 = 3805\n    LOADOUTSPRAY_LOADOUTSPRAY10 = 3806\n    LOADOUTSPRAY_LOADOUTSPRAY11 = 3807\n    LOADOUTSPRAY_LOADOUTSPRAY12 = 3808\n    LOADOUTSPRAY_LOADOUTSPRAY13 = 3809\n    LOADOUTSPRAY_LOADOUTSPRAY14 = 3810\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN = 3966\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN_CANCEL = 3967\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN = 3969\n    MORPHTOCOLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN_CANCEL = 3970\n    BATTERYOVERCHARGE_BATTERYOVERCHARGE = 4107\n    HYDRALISKFRENZY_HYDRALISKFRENZY = 4109\n    AMORPHOUSARMORCLOUD_AMORPHOUSARMORCLOUD = 4111\n    SHIELDBATTERYRECHARGEEX5_SHIELDBATTERYRECHARGE = 4113\n    SHIELDBATTERYRECHARGEEX5_STOP = 4114\n    MORPHTOBANELING_BANELING = 4121\n    MORPHTOBANELING_CANCEL = 4122\n    MOTHERSHIPCLOAK_ORACLECLOAKFIELD = 4124\n    ENERGYRECHARGE_ENERGYRECHARGE = 4126\n    SALVAGEEFFECT_SALVAGE = 4128\n    SALVAGESENSORTOWERREFUND_SALVAGE = 4130\n    WORKERSTOPIDLEABILITYVESPENE_GATHER = 4132\n\n    def __repr__(self) -> str:\n        return f\"AbilityId.{self.name}\"\n\n    @classmethod\n    def _missing_(cls, value: int) -> AbilityId:\n        return cls.NULL_NULL\n\n\nfor item in AbilityId:\n    globals()[item.name] = item\n"
  },
  {
    "path": "sc2/ids/buff_id.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\nimport enum\n\n\nclass BuffId(enum.Enum):\n    NULL = 0\n    RADAR25 = 1\n    TAUNTB = 2\n    DISABLEABILS = 3\n    TRANSIENTMORPH = 4\n    GRAVITONBEAM = 5\n    GHOSTCLOAK = 6\n    BANSHEECLOAK = 7\n    POWERUSERWARPABLE = 8\n    VORTEXBEHAVIORENEMY = 9\n    CORRUPTION = 10\n    QUEENSPAWNLARVATIMER = 11\n    GHOSTHOLDFIRE = 12\n    GHOSTHOLDFIREB = 13\n    LEECH = 14\n    LEECHDISABLEABILITIES = 15\n    EMPDECLOAK = 16\n    FUNGALGROWTH = 17\n    GUARDIANSHIELD = 18\n    SEEKERMISSILETIMEOUT = 19\n    TIMEWARPPRODUCTION = 20\n    ETHEREAL = 21\n    NEURALPARASITE = 22\n    NEURALPARASITEWAIT = 23\n    STIMPACKMARAUDER = 24\n    SUPPLYDROP = 25\n    _250MMSTRIKECANNONS = 26\n    STIMPACK = 27\n    PSISTORM = 28\n    CLOAKFIELDEFFECT = 29\n    CHARGING = 30\n    AIDANGERBUFF = 31\n    VORTEXBEHAVIOR = 32\n    SLOW = 33\n    TEMPORALRIFTUNIT = 34\n    SHEEPBUSY = 35\n    CONTAMINATED = 36\n    TIMESCALECONVERSIONBEHAVIOR = 37\n    BLINDINGCLOUDSTRUCTURE = 38\n    COLLAPSIBLEROCKTOWERCONJOINEDSEARCH = 39\n    COLLAPSIBLEROCKTOWERRAMPDIAGONALCONJOINEDSEARCH = 40\n    COLLAPSIBLETERRANTOWERCONJOINEDSEARCH = 41\n    COLLAPSIBLETERRANTOWERRAMPDIAGONALCONJOINEDSEARCH = 42\n    DIGESTERCREEPSPRAYVISION = 43\n    INVULNERABILITYSHIELD = 44\n    MINEDRONECOUNTDOWN = 45\n    MOTHERSHIPSTASIS = 46\n    MOTHERSHIPSTASISCASTER = 47\n    MOTHERSHIPCOREENERGIZEVISUAL = 48\n    ORACLEREVELATION = 49\n    GHOSTSNIPEDOT = 50\n    NEXUSPHASESHIFT = 51\n    NEXUSINVULNERABILITY = 52\n    ROUGHTERRAINSEARCH = 53\n    ROUGHTERRAINSLOW = 54\n    ORACLECLOAKFIELD = 55\n    ORACLECLOAKFIELDEFFECT = 56\n    SCRYERFRIENDLY = 57\n    SPECTRESHIELD = 58\n    VIPERCONSUMESTRUCTURE = 59\n    RESTORESHIELDS = 60\n    MERCENARYCYCLONEMISSILES = 61\n    MERCENARYSENSORDISH = 62\n    MERCENARYSHIELD = 63\n    SCRYER = 64\n    STUNROUNDINITIALBEHAVIOR = 65\n    BUILDINGSHIELD = 66\n    LASERSIGHT = 67\n    PROTECTIVEBARRIER = 68\n    CORRUPTORGROUNDATTACKDEBUFF = 69\n    BATTLECRUISERANTIAIRDISABLE = 70\n    BUILDINGSTASIS = 71\n    STASIS = 72\n    RESOURCESTUN = 73\n    MAXIMUMTHRUST = 74\n    CHARGEUP = 75\n    CLOAKUNIT = 76\n    NULLFIELD = 77\n    RESCUE = 78\n    BENIGN = 79\n    LASERTARGETING = 80\n    ENGAGE = 81\n    CAPRESOURCE = 82\n    BLINDINGCLOUD = 83\n    DOOMDAMAGEDELAY = 84\n    EYESTALK = 85\n    BURROWCHARGE = 86\n    HIDDEN = 87\n    MINEDRONEDOT = 88\n    MEDIVACSPEEDBOOST = 89\n    EXTENDBRIDGEEXTENDINGBRIDGENEWIDE8OUT = 90\n    EXTENDBRIDGEEXTENDINGBRIDGENWWIDE8OUT = 91\n    EXTENDBRIDGEEXTENDINGBRIDGENEWIDE10OUT = 92\n    EXTENDBRIDGEEXTENDINGBRIDGENWWIDE10OUT = 93\n    EXTENDBRIDGEEXTENDINGBRIDGENEWIDE12OUT = 94\n    EXTENDBRIDGEEXTENDINGBRIDGENWWIDE12OUT = 95\n    PHASESHIELD = 96\n    PURIFY = 97\n    VOIDSIPHON = 98\n    ORACLEWEAPON = 99\n    ANTIAIRWEAPONSWITCHCOOLDOWN = 100\n    ARBITERMPSTASISFIELD = 101\n    IMMORTALOVERLOAD = 102\n    CLOAKINGFIELDTARGETED = 103\n    LIGHTNINGBOMB = 104\n    ORACLEPHASESHIFT = 105\n    RELEASEINTERCEPTORSCOOLDOWN = 106\n    RELEASEINTERCEPTORSTIMEDLIFEWARNING = 107\n    RELEASEINTERCEPTORSWANDERDELAY = 108\n    RELEASEINTERCEPTORSBEACON = 109\n    ARBITERMPCLOAKFIELDEFFECT = 110\n    PURIFICATIONNOVA = 111\n    CORRUPTIONBOMBDAMAGE = 112\n    CORSAIRMPDISRUPTIONWEB = 113\n    DISRUPTORPUSH = 114\n    LIGHTOFAIUR = 115\n    LOCKON = 116\n    OVERCHARGE = 117\n    OVERCHARGEDAMAGE = 118\n    OVERCHARGESPEEDBOOST = 119\n    SEEKERMISSILE = 120\n    TEMPORALFIELD = 121\n    VOIDRAYSWARMDAMAGEBOOST = 122\n    VOIDMPIMMORTALREVIVESUPRESSED = 123\n    DEVOURERMPACIDSPORES = 124\n    DEFILERMPCONSUME = 125\n    DEFILERMPDARKSWARM = 126\n    DEFILERMPPLAGUE = 127\n    QUEENMPENSNARE = 128\n    ORACLESTASISTRAPTARGET = 129\n    SELFREPAIR = 130\n    AGGRESSIVEMUTATION = 131\n    PARASITICBOMB = 132\n    PARASITICBOMBUNITKU = 133\n    PARASITICBOMBSECONDARYUNITSEARCH = 134\n    ADEPTDEATHCHECK = 135\n    LURKERHOLDFIRE = 136\n    LURKERHOLDFIREB = 137\n    TIMESTOPSTUN = 138\n    SLAYNELEMENTALGRABSTUN = 139\n    PURIFICATIONNOVAPOST = 140\n    DISABLEINTERCEPTORS = 141\n    BYPASSARMORDEBUFFONE = 142\n    BYPASSARMORDEBUFFTWO = 143\n    BYPASSARMORDEBUFFTHREE = 144\n    CHANNELSNIPECOMBAT = 145\n    TEMPESTDISRUPTIONBLASTSTUNBEHAVIOR = 146\n    GRAVITONPRISON = 147\n    INFESTORDISEASE = 148\n    SS_LIGHTNINGPROJECTOR = 149\n    PURIFIERPLANETCRACKERCHARGE = 150\n    SPECTRECLOAKING = 151\n    WRAITHCLOAK = 152\n    PSYTROUSOXIDE = 153\n    BANSHEECLOAKCROSSSPECTRUMDAMPENERS = 154\n    SS_BATTLECRUISERHUNTERSEEKERTIMEOUT = 155\n    SS_STRONGERENEMYBUFF = 156\n    SS_TERRATRONARMMISSILETARGETCHECK = 157\n    SS_MISSILETIMEOUT = 158\n    SS_LEVIATHANBOMBCOLLISIONCHECK = 159\n    SS_LEVIATHANBOMBEXPLODETIMER = 160\n    SS_LEVIATHANBOMBMISSILETARGETCHECK = 161\n    SS_TERRATRONCOLLISIONCHECK = 162\n    SS_CARRIERBOSSCOLLISIONCHECK = 163\n    SS_CORRUPTORMISSILETARGETCHECK = 164\n    SS_INVULNERABLE = 165\n    SS_LEVIATHANTENTACLEMISSILETARGETCHECK = 166\n    SS_LEVIATHANTENTACLEMISSILETARGETCHECKINVERTED = 167\n    SS_LEVIATHANTENTACLETARGETDEATHDELAY = 168\n    SS_LEVIATHANTENTACLEMISSILESCANSWAPDELAY = 169\n    SS_POWERUPDIAGONAL2 = 170\n    SS_BATTLECRUISERCOLLISIONCHECK = 171\n    SS_TERRATRONMISSILESPINNERMISSILELAUNCHER = 172\n    SS_TERRATRONMISSILESPINNERCOLLISIONCHECK = 173\n    SS_TERRATRONMISSILELAUNCHER = 174\n    SS_BATTLECRUISERMISSILELAUNCHER = 175\n    SS_TERRATRONSTUN = 176\n    SS_VIKINGRESPAWN = 177\n    SS_WRAITHCOLLISIONCHECK = 178\n    SS_SCOURGEMISSILETARGETCHECK = 179\n    SS_SCOURGEDEATH = 180\n    SS_SWARMGUARDIANCOLLISIONCHECK = 181\n    SS_FIGHTERBOMBMISSILEDEATH = 182\n    SS_FIGHTERDRONEDAMAGERESPONSE = 183\n    SS_INTERCEPTORCOLLISIONCHECK = 184\n    SS_CARRIERCOLLISIONCHECK = 185\n    SS_MISSILETARGETCHECKVIKINGDRONE = 186\n    SS_MISSILETARGETCHECKVIKINGSTRONG1 = 187\n    SS_MISSILETARGETCHECKVIKINGSTRONG2 = 188\n    SS_POWERUPHEALTH1 = 189\n    SS_POWERUPHEALTH2 = 190\n    SS_POWERUPSTRONG = 191\n    SS_POWERUPMORPHTOBOMB = 192\n    SS_POWERUPMORPHTOHEALTH = 193\n    SS_POWERUPMORPHTOSIDEMISSILES = 194\n    SS_POWERUPMORPHTOSTRONGERMISSILES = 195\n    SS_CORRUPTORCOLLISIONCHECK = 196\n    SS_SCOUTCOLLISIONCHECK = 197\n    SS_PHOENIXCOLLISIONCHECK = 198\n    SS_SCOURGECOLLISIONCHECK = 199\n    SS_LEVIATHANCOLLISIONCHECK = 200\n    SS_SCIENCEVESSELCOLLISIONCHECK = 201\n    SS_TERRATRONSAWCOLLISIONCHECK = 202\n    SS_LIGHTNINGPROJECTORCOLLISIONCHECK = 203\n    SHIFTDELAY = 204\n    BIOSTASIS = 205\n    PERSONALCLOAKINGFREE = 206\n    EMPDRAIN = 207\n    MINDBLASTSTUN = 208\n    _330MMBARRAGECANNONS = 209\n    VOODOOSHIELD = 210\n    SPECTRECLOAKINGFREE = 211\n    ULTRASONICPULSESTUN = 212\n    IRRADIATE = 213\n    NYDUSWORMLAVAINSTANTDEATH = 214\n    PREDATORCLOAKING = 215\n    PSIDISRUPTION = 216\n    MINDCONTROL = 217\n    QUEENKNOCKDOWN = 218\n    SCIENCEVESSELCLOAKFIELD = 219\n    SPORECANNONMISSILE = 220\n    ARTANISTEMPORALRIFTUNIT = 221\n    ARTANISCLOAKINGFIELDEFFECT = 222\n    ARTANISVORTEXBEHAVIOR = 223\n    INCAPACITATED = 224\n    KARASSPSISTORM = 225\n    DUTCHMARAUDERSLOW = 226\n    JUMPSTOMPSTUN = 227\n    JUMPSTOMPFSTUN = 228\n    RAYNORMISSILETIMEDLIFE = 229\n    PSIONICSHOCKWAVEHEIGHTANDSTUN = 230\n    SHADOWCLONE = 231\n    AUTOMATEDREPAIR = 232\n    SLIMED = 233\n    RAYNORTIMEBOMBMISSILE = 234\n    RAYNORTIMEBOMBUNIT = 235\n    TYCHUSCOMMANDOSTIMPACK = 236\n    VIRALPLASMA = 237\n    NAPALM = 238\n    BURSTCAPACITORSDAMAGEBUFF = 239\n    COLONYINFESTATION = 240\n    DOMINATION = 241\n    EMPBURST = 242\n    HYBRIDCZERGYROOTS = 243\n    HYBRIDFZERGYROOTS = 244\n    LOCKDOWNB = 245\n    SPECTRELOCKDOWNB = 246\n    VOODOOLOCKDOWN = 247\n    ZERATULSTUN = 248\n    BUILDINGSCARAB = 249\n    VORTEXBEHAVIORERADICATOR = 250\n    GHOSTBLAST = 251\n    HEROICBUFF03 = 252\n    CANNONRADAR = 253\n    SS_MISSILETARGETCHECKVIKING = 254\n    SS_MISSILETARGETCHECK = 255\n    SS_MAXSPEED = 256\n    SS_MAXACCELERATION = 257\n    SS_POWERUPDIAGONAL1 = 258\n    WATER = 259\n    DEFENSIVEMATRIX = 260\n    TESTATTRIBUTE = 261\n    TESTVETERANCY = 262\n    SHREDDERSWARMDAMAGEAPPLY = 263\n    CORRUPTORINFESTING = 264\n    MERCGROUNDDROPDELAY = 265\n    MERCGROUNDDROP = 266\n    MERCAIRDROPDELAY = 267\n    SPECTREHOLDFIRE = 268\n    SPECTREHOLDFIREB = 269\n    ITEMGRAVITYBOMBS = 270\n    CARRYMINERALFIELDMINERALS = 271\n    CARRYHIGHYIELDMINERALFIELDMINERALS = 272\n    CARRYHARVESTABLEVESPENEGEYSERGAS = 273\n    CARRYHARVESTABLEVESPENEGEYSERGASPROTOSS = 274\n    CARRYHARVESTABLEVESPENEGEYSERGASZERG = 275\n    PERMANENTLYCLOAKED = 276\n    RAVENSCRAMBLERMISSILE = 277\n    RAVENSHREDDERMISSILETIMEOUT = 278\n    RAVENSHREDDERMISSILETINT = 279\n    RAVENSHREDDERMISSILEARMORREDUCTION = 280\n    CHRONOBOOSTENERGYCOST = 281\n    NEXUSSHIELDRECHARGEONPYLONBEHAVIOR = 282\n    NEXUSSHIELDRECHARGEONPYLONBEHAVIORSECONDARYONTARGET = 283\n    INFESTORENSNARE = 284\n    INFESTORENSNAREMAKEPRECURSORREHEIGHTSOURCE = 285\n    NEXUSSHIELDOVERCHARGE = 286\n    PARASITICBOMBDELAYTIMEDLIFE = 287\n    TRANSFUSION = 288\n    ACCELERATIONZONETEMPORALFIELD = 289\n    ACCELERATIONZONEFLYINGTEMPORALFIELD = 290\n    INHIBITORZONEFLYINGTEMPORALFIELD = 291\n    LOADOUTSPRAYTRACKER = 292\n    INHIBITORZONETEMPORALFIELD = 293\n    CLOAKFIELD = 294\n    RESONATINGGLAIVESPHASESHIFT = 295\n    NEURALPARASITECHILDREN = 296\n    AMORPHOUSARMORCLOUD = 297\n    RAVENSHREDDERMISSILEARMORREDUCTIONUISUBTRUCT = 298\n    TAKENDAMAGE = 299\n    RAVENSCRAMBLERMISSILECARRIER = 300\n    BATTERYOVERCHARGE = 301\n    HYDRALISKFRENZY = 302\n\n    def __repr__(self) -> str:\n        return f\"BuffId.{self.name}\"\n\n    @classmethod\n    def _missing_(cls, value: int) -> BuffId:\n        return cls.NULL\n\n\nfor item in BuffId:\n    globals()[item.name] = item\n"
  },
  {
    "path": "sc2/ids/effect_id.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\nimport enum\n\n\nclass EffectId(enum.Enum):\n    NULL = 0\n    PSISTORMPERSISTENT = 1\n    GUARDIANSHIELDPERSISTENT = 2\n    TEMPORALFIELDGROWINGBUBBLECREATEPERSISTENT = 3\n    TEMPORALFIELDAFTERBUBBLECREATEPERSISTENT = 4\n    THERMALLANCESFORWARD = 5\n    SCANNERSWEEP = 6\n    NUKEPERSISTENT = 7\n    LIBERATORTARGETMORPHDELAYPERSISTENT = 8\n    LIBERATORTARGETMORPHPERSISTENT = 9\n    BLINDINGCLOUDCP = 10\n    RAVAGERCORROSIVEBILECP = 11\n    LURKERMP = 12\n\n    def __repr__(self) -> str:\n        return f\"EffectId.{self.name}\"\n\n\nfor item in EffectId:\n    globals()[item.name] = item\n"
  },
  {
    "path": "sc2/ids/id_version.py",
    "content": "ID_VERSION_STRING = \"4.11.4.78285\"\n"
  },
  {
    "path": "sc2/ids/unit_typeid.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\nimport enum\n\n\nclass UnitTypeId(enum.Enum):\n    NOTAUNIT = 0\n    SYSTEM_SNAPSHOT_DUMMY = 1\n    BALL = 2\n    STEREOSCOPICOPTIONSUNIT = 3\n    COLOSSUS = 4\n    TECHLAB = 5\n    REACTOR = 6\n    INFESTORTERRAN = 7\n    BANELINGCOCOON = 8\n    BANELING = 9\n    MOTHERSHIP = 10\n    POINTDEFENSEDRONE = 11\n    CHANGELING = 12\n    CHANGELINGZEALOT = 13\n    CHANGELINGMARINESHIELD = 14\n    CHANGELINGMARINE = 15\n    CHANGELINGZERGLINGWINGS = 16\n    CHANGELINGZERGLING = 17\n    COMMANDCENTER = 18\n    SUPPLYDEPOT = 19\n    REFINERY = 20\n    BARRACKS = 21\n    ENGINEERINGBAY = 22\n    MISSILETURRET = 23\n    BUNKER = 24\n    SENSORTOWER = 25\n    GHOSTACADEMY = 26\n    FACTORY = 27\n    STARPORT = 28\n    ARMORY = 29\n    FUSIONCORE = 30\n    AUTOTURRET = 31\n    SIEGETANKSIEGED = 32\n    SIEGETANK = 33\n    VIKINGASSAULT = 34\n    VIKINGFIGHTER = 35\n    COMMANDCENTERFLYING = 36\n    BARRACKSTECHLAB = 37\n    BARRACKSREACTOR = 38\n    FACTORYTECHLAB = 39\n    FACTORYREACTOR = 40\n    STARPORTTECHLAB = 41\n    STARPORTREACTOR = 42\n    FACTORYFLYING = 43\n    STARPORTFLYING = 44\n    SCV = 45\n    BARRACKSFLYING = 46\n    SUPPLYDEPOTLOWERED = 47\n    MARINE = 48\n    REAPER = 49\n    GHOST = 50\n    MARAUDER = 51\n    THOR = 52\n    HELLION = 53\n    MEDIVAC = 54\n    BANSHEE = 55\n    RAVEN = 56\n    BATTLECRUISER = 57\n    NUKE = 58\n    NEXUS = 59\n    PYLON = 60\n    ASSIMILATOR = 61\n    GATEWAY = 62\n    FORGE = 63\n    FLEETBEACON = 64\n    TWILIGHTCOUNCIL = 65\n    PHOTONCANNON = 66\n    STARGATE = 67\n    TEMPLARARCHIVE = 68\n    DARKSHRINE = 69\n    ROBOTICSBAY = 70\n    ROBOTICSFACILITY = 71\n    CYBERNETICSCORE = 72\n    ZEALOT = 73\n    STALKER = 74\n    HIGHTEMPLAR = 75\n    DARKTEMPLAR = 76\n    SENTRY = 77\n    PHOENIX = 78\n    CARRIER = 79\n    VOIDRAY = 80\n    WARPPRISM = 81\n    OBSERVER = 82\n    IMMORTAL = 83\n    PROBE = 84\n    INTERCEPTOR = 85\n    HATCHERY = 86\n    CREEPTUMOR = 87\n    EXTRACTOR = 88\n    SPAWNINGPOOL = 89\n    EVOLUTIONCHAMBER = 90\n    HYDRALISKDEN = 91\n    SPIRE = 92\n    ULTRALISKCAVERN = 93\n    INFESTATIONPIT = 94\n    NYDUSNETWORK = 95\n    BANELINGNEST = 96\n    ROACHWARREN = 97\n    SPINECRAWLER = 98\n    SPORECRAWLER = 99\n    LAIR = 100\n    HIVE = 101\n    GREATERSPIRE = 102\n    EGG = 103\n    DRONE = 104\n    ZERGLING = 105\n    OVERLORD = 106\n    HYDRALISK = 107\n    MUTALISK = 108\n    ULTRALISK = 109\n    ROACH = 110\n    INFESTOR = 111\n    CORRUPTOR = 112\n    BROODLORDCOCOON = 113\n    BROODLORD = 114\n    BANELINGBURROWED = 115\n    DRONEBURROWED = 116\n    HYDRALISKBURROWED = 117\n    ROACHBURROWED = 118\n    ZERGLINGBURROWED = 119\n    INFESTORTERRANBURROWED = 120\n    REDSTONELAVACRITTERBURROWED = 121\n    REDSTONELAVACRITTERINJUREDBURROWED = 122\n    REDSTONELAVACRITTER = 123\n    REDSTONELAVACRITTERINJURED = 124\n    QUEENBURROWED = 125\n    QUEEN = 126\n    INFESTORBURROWED = 127\n    OVERLORDCOCOON = 128\n    OVERSEER = 129\n    PLANETARYFORTRESS = 130\n    ULTRALISKBURROWED = 131\n    ORBITALCOMMAND = 132\n    WARPGATE = 133\n    ORBITALCOMMANDFLYING = 134\n    FORCEFIELD = 135\n    WARPPRISMPHASING = 136\n    CREEPTUMORBURROWED = 137\n    CREEPTUMORQUEEN = 138\n    SPINECRAWLERUPROOTED = 139\n    SPORECRAWLERUPROOTED = 140\n    ARCHON = 141\n    NYDUSCANAL = 142\n    BROODLINGESCORT = 143\n    GHOSTALTERNATE = 144\n    GHOSTNOVA = 145\n    RICHMINERALFIELD = 146\n    RICHMINERALFIELD750 = 147\n    URSADON = 148\n    XELNAGATOWER = 149\n    INFESTEDTERRANSEGG = 150\n    LARVA = 151\n    REAPERPLACEHOLDER = 152\n    MARINEACGLUESCREENDUMMY = 153\n    FIREBATACGLUESCREENDUMMY = 154\n    MEDICACGLUESCREENDUMMY = 155\n    MARAUDERACGLUESCREENDUMMY = 156\n    VULTUREACGLUESCREENDUMMY = 157\n    SIEGETANKACGLUESCREENDUMMY = 158\n    VIKINGACGLUESCREENDUMMY = 159\n    BANSHEEACGLUESCREENDUMMY = 160\n    BATTLECRUISERACGLUESCREENDUMMY = 161\n    ORBITALCOMMANDACGLUESCREENDUMMY = 162\n    BUNKERACGLUESCREENDUMMY = 163\n    BUNKERUPGRADEDACGLUESCREENDUMMY = 164\n    MISSILETURRETACGLUESCREENDUMMY = 165\n    HELLBATACGLUESCREENDUMMY = 166\n    GOLIATHACGLUESCREENDUMMY = 167\n    CYCLONEACGLUESCREENDUMMY = 168\n    WRAITHACGLUESCREENDUMMY = 169\n    SCIENCEVESSELACGLUESCREENDUMMY = 170\n    HERCULESACGLUESCREENDUMMY = 171\n    THORACGLUESCREENDUMMY = 172\n    PERDITIONTURRETACGLUESCREENDUMMY = 173\n    FLAMINGBETTYACGLUESCREENDUMMY = 174\n    DEVASTATIONTURRETACGLUESCREENDUMMY = 175\n    BLASTERBILLYACGLUESCREENDUMMY = 176\n    SPINNINGDIZZYACGLUESCREENDUMMY = 177\n    ZERGLINGKERRIGANACGLUESCREENDUMMY = 178\n    RAPTORACGLUESCREENDUMMY = 179\n    QUEENCOOPACGLUESCREENDUMMY = 180\n    HYDRALISKACGLUESCREENDUMMY = 181\n    HYDRALISKLURKERACGLUESCREENDUMMY = 182\n    MUTALISKBROODLORDACGLUESCREENDUMMY = 183\n    BROODLORDACGLUESCREENDUMMY = 184\n    ULTRALISKACGLUESCREENDUMMY = 185\n    TORRASQUEACGLUESCREENDUMMY = 186\n    OVERSEERACGLUESCREENDUMMY = 187\n    LURKERACGLUESCREENDUMMY = 188\n    SPINECRAWLERACGLUESCREENDUMMY = 189\n    SPORECRAWLERACGLUESCREENDUMMY = 190\n    NYDUSNETWORKACGLUESCREENDUMMY = 191\n    OMEGANETWORKACGLUESCREENDUMMY = 192\n    ZERGLINGZAGARAACGLUESCREENDUMMY = 193\n    SWARMLINGACGLUESCREENDUMMY = 194\n    BANELINGACGLUESCREENDUMMY = 195\n    SPLITTERLINGACGLUESCREENDUMMY = 196\n    ABERRATIONACGLUESCREENDUMMY = 197\n    SCOURGEACGLUESCREENDUMMY = 198\n    CORRUPTORACGLUESCREENDUMMY = 199\n    BILELAUNCHERACGLUESCREENDUMMY = 200\n    SWARMQUEENACGLUESCREENDUMMY = 201\n    ROACHACGLUESCREENDUMMY = 202\n    ROACHVILEACGLUESCREENDUMMY = 203\n    RAVAGERACGLUESCREENDUMMY = 204\n    SWARMHOSTACGLUESCREENDUMMY = 205\n    MUTALISKACGLUESCREENDUMMY = 206\n    GUARDIANACGLUESCREENDUMMY = 207\n    DEVOURERACGLUESCREENDUMMY = 208\n    VIPERACGLUESCREENDUMMY = 209\n    BRUTALISKACGLUESCREENDUMMY = 210\n    LEVIATHANACGLUESCREENDUMMY = 211\n    ZEALOTACGLUESCREENDUMMY = 212\n    ZEALOTAIURACGLUESCREENDUMMY = 213\n    DRAGOONACGLUESCREENDUMMY = 214\n    HIGHTEMPLARACGLUESCREENDUMMY = 215\n    ARCHONACGLUESCREENDUMMY = 216\n    IMMORTALACGLUESCREENDUMMY = 217\n    OBSERVERACGLUESCREENDUMMY = 218\n    PHOENIXAIURACGLUESCREENDUMMY = 219\n    REAVERACGLUESCREENDUMMY = 220\n    TEMPESTACGLUESCREENDUMMY = 221\n    PHOTONCANNONACGLUESCREENDUMMY = 222\n    ZEALOTVORAZUNACGLUESCREENDUMMY = 223\n    ZEALOTSHAKURASACGLUESCREENDUMMY = 224\n    STALKERSHAKURASACGLUESCREENDUMMY = 225\n    DARKTEMPLARSHAKURASACGLUESCREENDUMMY = 226\n    CORSAIRACGLUESCREENDUMMY = 227\n    VOIDRAYACGLUESCREENDUMMY = 228\n    VOIDRAYSHAKURASACGLUESCREENDUMMY = 229\n    ORACLEACGLUESCREENDUMMY = 230\n    DARKARCHONACGLUESCREENDUMMY = 231\n    DARKPYLONACGLUESCREENDUMMY = 232\n    ZEALOTPURIFIERACGLUESCREENDUMMY = 233\n    SENTRYPURIFIERACGLUESCREENDUMMY = 234\n    IMMORTALKARAXACGLUESCREENDUMMY = 235\n    COLOSSUSACGLUESCREENDUMMY = 236\n    COLOSSUSPURIFIERACGLUESCREENDUMMY = 237\n    PHOENIXPURIFIERACGLUESCREENDUMMY = 238\n    CARRIERACGLUESCREENDUMMY = 239\n    CARRIERAIURACGLUESCREENDUMMY = 240\n    KHAYDARINMONOLITHACGLUESCREENDUMMY = 241\n    SHIELDBATTERYACGLUESCREENDUMMY = 242\n    ELITEMARINEACGLUESCREENDUMMY = 243\n    MARAUDERCOMMANDOACGLUESCREENDUMMY = 244\n    SPECOPSGHOSTACGLUESCREENDUMMY = 245\n    HELLBATRANGERACGLUESCREENDUMMY = 246\n    STRIKEGOLIATHACGLUESCREENDUMMY = 247\n    HEAVYSIEGETANKACGLUESCREENDUMMY = 248\n    RAIDLIBERATORACGLUESCREENDUMMY = 249\n    RAVENTYPEIIACGLUESCREENDUMMY = 250\n    COVERTBANSHEEACGLUESCREENDUMMY = 251\n    RAILGUNTURRETACGLUESCREENDUMMY = 252\n    BLACKOPSMISSILETURRETACGLUESCREENDUMMY = 253\n    SUPPLICANTACGLUESCREENDUMMY = 254\n    STALKERTALDARIMACGLUESCREENDUMMY = 255\n    SENTRYTALDARIMACGLUESCREENDUMMY = 256\n    HIGHTEMPLARTALDARIMACGLUESCREENDUMMY = 257\n    IMMORTALTALDARIMACGLUESCREENDUMMY = 258\n    COLOSSUSTALDARIMACGLUESCREENDUMMY = 259\n    WARPPRISMTALDARIMACGLUESCREENDUMMY = 260\n    PHOTONCANNONTALDARIMACGLUESCREENDUMMY = 261\n    NEEDLESPINESWEAPON = 262\n    CORRUPTIONWEAPON = 263\n    INFESTEDTERRANSWEAPON = 264\n    NEURALPARASITEWEAPON = 265\n    POINTDEFENSEDRONERELEASEWEAPON = 266\n    HUNTERSEEKERWEAPON = 267\n    MULE = 268\n    THORAAWEAPON = 269\n    PUNISHERGRENADESLMWEAPON = 270\n    VIKINGFIGHTERWEAPON = 271\n    ATALASERBATTERYLMWEAPON = 272\n    ATSLASERBATTERYLMWEAPON = 273\n    LONGBOLTMISSILEWEAPON = 274\n    D8CHARGEWEAPON = 275\n    YAMATOWEAPON = 276\n    IONCANNONSWEAPON = 277\n    ACIDSALIVAWEAPON = 278\n    SPINECRAWLERWEAPON = 279\n    SPORECRAWLERWEAPON = 280\n    GLAIVEWURMWEAPON = 281\n    GLAIVEWURMM2WEAPON = 282\n    GLAIVEWURMM3WEAPON = 283\n    STALKERWEAPON = 284\n    EMP2WEAPON = 285\n    BACKLASHROCKETSLMWEAPON = 286\n    PHOTONCANNONWEAPON = 287\n    PARASITESPOREWEAPON = 288\n    BROODLING = 289\n    BROODLORDBWEAPON = 290\n    AUTOTURRETRELEASEWEAPON = 291\n    LARVARELEASEMISSILE = 292\n    ACIDSPINESWEAPON = 293\n    FRENZYWEAPON = 294\n    CONTAMINATEWEAPON = 295\n    BEACONRALLY = 296\n    BEACONARMY = 297\n    BEACONATTACK = 298\n    BEACONDEFEND = 299\n    BEACONHARASS = 300\n    BEACONIDLE = 301\n    BEACONAUTO = 302\n    BEACONDETECT = 303\n    BEACONSCOUT = 304\n    BEACONCLAIM = 305\n    BEACONEXPAND = 306\n    BEACONCUSTOM1 = 307\n    BEACONCUSTOM2 = 308\n    BEACONCUSTOM3 = 309\n    BEACONCUSTOM4 = 310\n    ADEPT = 311\n    ROCKS2X2NONCONJOINED = 312\n    FUNGALGROWTHMISSILE = 313\n    NEURALPARASITETENTACLEMISSILE = 314\n    BEACON_PROTOSS = 315\n    BEACON_PROTOSSSMALL = 316\n    BEACON_TERRAN = 317\n    BEACON_TERRANSMALL = 318\n    BEACON_ZERG = 319\n    BEACON_ZERGSMALL = 320\n    LYOTE = 321\n    CARRIONBIRD = 322\n    KARAKMALE = 323\n    KARAKFEMALE = 324\n    URSADAKFEMALEEXOTIC = 325\n    URSADAKMALE = 326\n    URSADAKFEMALE = 327\n    URSADAKCALF = 328\n    URSADAKMALEEXOTIC = 329\n    UTILITYBOT = 330\n    COMMENTATORBOT1 = 331\n    COMMENTATORBOT2 = 332\n    COMMENTATORBOT3 = 333\n    COMMENTATORBOT4 = 334\n    SCANTIPEDE = 335\n    DOG = 336\n    SHEEP = 337\n    COW = 338\n    INFESTEDTERRANSEGGPLACEMENT = 339\n    INFESTORTERRANSWEAPON = 340\n    MINERALFIELD = 341\n    VESPENEGEYSER = 342\n    SPACEPLATFORMGEYSER = 343\n    RICHVESPENEGEYSER = 344\n    DESTRUCTIBLESEARCHLIGHT = 345\n    DESTRUCTIBLEBULLHORNLIGHTS = 346\n    DESTRUCTIBLESTREETLIGHT = 347\n    DESTRUCTIBLESPACEPLATFORMSIGN = 348\n    DESTRUCTIBLESTOREFRONTCITYPROPS = 349\n    DESTRUCTIBLEBILLBOARDTALL = 350\n    DESTRUCTIBLEBILLBOARDSCROLLINGTEXT = 351\n    DESTRUCTIBLESPACEPLATFORMBARRIER = 352\n    DESTRUCTIBLESIGNSDIRECTIONAL = 353\n    DESTRUCTIBLESIGNSCONSTRUCTION = 354\n    DESTRUCTIBLESIGNSFUNNY = 355\n    DESTRUCTIBLESIGNSICONS = 356\n    DESTRUCTIBLESIGNSWARNING = 357\n    DESTRUCTIBLEGARAGE = 358\n    DESTRUCTIBLEGARAGELARGE = 359\n    DESTRUCTIBLETRAFFICSIGNAL = 360\n    TRAFFICSIGNAL = 361\n    BRAXISALPHADESTRUCTIBLE1X1 = 362\n    BRAXISALPHADESTRUCTIBLE2X2 = 363\n    DESTRUCTIBLEDEBRIS4X4 = 364\n    DESTRUCTIBLEDEBRIS6X6 = 365\n    DESTRUCTIBLEROCK2X4VERTICAL = 366\n    DESTRUCTIBLEROCK2X4HORIZONTAL = 367\n    DESTRUCTIBLEROCK2X6VERTICAL = 368\n    DESTRUCTIBLEROCK2X6HORIZONTAL = 369\n    DESTRUCTIBLEROCK4X4 = 370\n    DESTRUCTIBLEROCK6X6 = 371\n    DESTRUCTIBLERAMPDIAGONALHUGEULBR = 372\n    DESTRUCTIBLERAMPDIAGONALHUGEBLUR = 373\n    DESTRUCTIBLERAMPVERTICALHUGE = 374\n    DESTRUCTIBLERAMPHORIZONTALHUGE = 375\n    DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEULBR = 376\n    DESTRUCTIBLEDEBRISRAMPDIAGONALHUGEBLUR = 377\n    OVERLORDGENERATECREEPKEYBIND = 378\n    MENGSKSTATUEALONE = 379\n    MENGSKSTATUE = 380\n    WOLFSTATUE = 381\n    GLOBESTATUE = 382\n    WEAPON = 383\n    GLAIVEWURMBOUNCEWEAPON = 384\n    BROODLORDWEAPON = 385\n    BROODLORDAWEAPON = 386\n    CREEPBLOCKER1X1 = 387\n    PERMANENTCREEPBLOCKER1X1 = 388\n    PATHINGBLOCKER1X1 = 389\n    PATHINGBLOCKER2X2 = 390\n    AUTOTESTATTACKTARGETGROUND = 391\n    AUTOTESTATTACKTARGETAIR = 392\n    AUTOTESTATTACKER = 393\n    HELPEREMITTERSELECTIONARROW = 394\n    MULTIKILLOBJECT = 395\n    SHAPEGOLFBALL = 396\n    SHAPECONE = 397\n    SHAPECUBE = 398\n    SHAPECYLINDER = 399\n    SHAPEDODECAHEDRON = 400\n    SHAPEICOSAHEDRON = 401\n    SHAPEOCTAHEDRON = 402\n    SHAPEPYRAMID = 403\n    SHAPEROUNDEDCUBE = 404\n    SHAPESPHERE = 405\n    SHAPETETRAHEDRON = 406\n    SHAPETHICKTORUS = 407\n    SHAPETHINTORUS = 408\n    SHAPETORUS = 409\n    SHAPE4POINTSTAR = 410\n    SHAPE5POINTSTAR = 411\n    SHAPE6POINTSTAR = 412\n    SHAPE8POINTSTAR = 413\n    SHAPEARROWPOINTER = 414\n    SHAPEBOWL = 415\n    SHAPEBOX = 416\n    SHAPECAPSULE = 417\n    SHAPECRESCENTMOON = 418\n    SHAPEDECAHEDRON = 419\n    SHAPEDIAMOND = 420\n    SHAPEFOOTBALL = 421\n    SHAPEGEMSTONE = 422\n    SHAPEHEART = 423\n    SHAPEJACK = 424\n    SHAPEPLUSSIGN = 425\n    SHAPESHAMROCK = 426\n    SHAPESPADE = 427\n    SHAPETUBE = 428\n    SHAPEEGG = 429\n    SHAPEYENSIGN = 430\n    SHAPEX = 431\n    SHAPEWATERMELON = 432\n    SHAPEWONSIGN = 433\n    SHAPETENNISBALL = 434\n    SHAPESTRAWBERRY = 435\n    SHAPESMILEYFACE = 436\n    SHAPESOCCERBALL = 437\n    SHAPERAINBOW = 438\n    SHAPESADFACE = 439\n    SHAPEPOUNDSIGN = 440\n    SHAPEPEAR = 441\n    SHAPEPINEAPPLE = 442\n    SHAPEORANGE = 443\n    SHAPEPEANUT = 444\n    SHAPEO = 445\n    SHAPELEMON = 446\n    SHAPEMONEYBAG = 447\n    SHAPEHORSESHOE = 448\n    SHAPEHOCKEYSTICK = 449\n    SHAPEHOCKEYPUCK = 450\n    SHAPEHAND = 451\n    SHAPEGOLFCLUB = 452\n    SHAPEGRAPE = 453\n    SHAPEEUROSIGN = 454\n    SHAPEDOLLARSIGN = 455\n    SHAPEBASKETBALL = 456\n    SHAPECARROT = 457\n    SHAPECHERRY = 458\n    SHAPEBASEBALL = 459\n    SHAPEBASEBALLBAT = 460\n    SHAPEBANANA = 461\n    SHAPEAPPLE = 462\n    SHAPECASHLARGE = 463\n    SHAPECASHMEDIUM = 464\n    SHAPECASHSMALL = 465\n    SHAPEFOOTBALLCOLORED = 466\n    SHAPELEMONSMALL = 467\n    SHAPEORANGESMALL = 468\n    SHAPETREASURECHESTOPEN = 469\n    SHAPETREASURECHESTCLOSED = 470\n    SHAPEWATERMELONSMALL = 471\n    UNBUILDABLEROCKSDESTRUCTIBLE = 472\n    UNBUILDABLEBRICKSDESTRUCTIBLE = 473\n    UNBUILDABLEPLATESDESTRUCTIBLE = 474\n    DEBRIS2X2NONCONJOINED = 475\n    ENEMYPATHINGBLOCKER1X1 = 476\n    ENEMYPATHINGBLOCKER2X2 = 477\n    ENEMYPATHINGBLOCKER4X4 = 478\n    ENEMYPATHINGBLOCKER8X8 = 479\n    ENEMYPATHINGBLOCKER16X16 = 480\n    SCOPETEST = 481\n    SENTRYACGLUESCREENDUMMY = 482\n    MINERALFIELD750 = 483\n    HELLIONTANK = 484\n    COLLAPSIBLETERRANTOWERDEBRIS = 485\n    DEBRISRAMPLEFT = 486\n    DEBRISRAMPRIGHT = 487\n    MOTHERSHIPCORE = 488\n    LOCUSTMP = 489\n    COLLAPSIBLEROCKTOWERDEBRIS = 490\n    NYDUSCANALATTACKER = 491\n    NYDUSCANALCREEPER = 492\n    SWARMHOSTBURROWEDMP = 493\n    SWARMHOSTMP = 494\n    ORACLE = 495\n    TEMPEST = 496\n    WARHOUND = 497\n    WIDOWMINE = 498\n    VIPER = 499\n    WIDOWMINEBURROWED = 500\n    LURKERMPEGG = 501\n    LURKERMP = 502\n    LURKERMPBURROWED = 503\n    LURKERDENMP = 504\n    EXTENDINGBRIDGENEWIDE8OUT = 505\n    EXTENDINGBRIDGENEWIDE8 = 506\n    EXTENDINGBRIDGENWWIDE8OUT = 507\n    EXTENDINGBRIDGENWWIDE8 = 508\n    EXTENDINGBRIDGENEWIDE10OUT = 509\n    EXTENDINGBRIDGENEWIDE10 = 510\n    EXTENDINGBRIDGENWWIDE10OUT = 511\n    EXTENDINGBRIDGENWWIDE10 = 512\n    EXTENDINGBRIDGENEWIDE12OUT = 513\n    EXTENDINGBRIDGENEWIDE12 = 514\n    EXTENDINGBRIDGENWWIDE12OUT = 515\n    EXTENDINGBRIDGENWWIDE12 = 516\n    COLLAPSIBLEROCKTOWERDEBRISRAMPRIGHT = 517\n    COLLAPSIBLEROCKTOWERDEBRISRAMPLEFT = 518\n    XELNAGA_CAVERNS_DOORE = 519\n    XELNAGA_CAVERNS_DOOREOPENED = 520\n    XELNAGA_CAVERNS_DOORN = 521\n    XELNAGA_CAVERNS_DOORNE = 522\n    XELNAGA_CAVERNS_DOORNEOPENED = 523\n    XELNAGA_CAVERNS_DOORNOPENED = 524\n    XELNAGA_CAVERNS_DOORNW = 525\n    XELNAGA_CAVERNS_DOORNWOPENED = 526\n    XELNAGA_CAVERNS_DOORS = 527\n    XELNAGA_CAVERNS_DOORSE = 528\n    XELNAGA_CAVERNS_DOORSEOPENED = 529\n    XELNAGA_CAVERNS_DOORSOPENED = 530\n    XELNAGA_CAVERNS_DOORSW = 531\n    XELNAGA_CAVERNS_DOORSWOPENED = 532\n    XELNAGA_CAVERNS_DOORW = 533\n    XELNAGA_CAVERNS_DOORWOPENED = 534\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE8OUT = 535\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE8 = 536\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW8OUT = 537\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW8 = 538\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE10OUT = 539\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE10 = 540\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW10OUT = 541\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW10 = 542\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE12OUT = 543\n    XELNAGA_CAVERNS_FLOATING_BRIDGENE12 = 544\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW12OUT = 545\n    XELNAGA_CAVERNS_FLOATING_BRIDGENW12 = 546\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH8OUT = 547\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH8 = 548\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV8OUT = 549\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV8 = 550\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH10OUT = 551\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH10 = 552\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV10OUT = 553\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV10 = 554\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH12OUT = 555\n    XELNAGA_CAVERNS_FLOATING_BRIDGEH12 = 556\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV12OUT = 557\n    XELNAGA_CAVERNS_FLOATING_BRIDGEV12 = 558\n    COLLAPSIBLETERRANTOWERPUSHUNITRAMPLEFT = 559\n    COLLAPSIBLETERRANTOWERPUSHUNITRAMPRIGHT = 560\n    COLLAPSIBLEROCKTOWERPUSHUNIT = 561\n    COLLAPSIBLETERRANTOWERPUSHUNIT = 562\n    COLLAPSIBLEROCKTOWERPUSHUNITRAMPRIGHT = 563\n    COLLAPSIBLEROCKTOWERPUSHUNITRAMPLEFT = 564\n    DIGESTERCREEPSPRAYTARGETUNIT = 565\n    DIGESTERCREEPSPRAYUNIT = 566\n    NYDUSCANALATTACKERWEAPON = 567\n    VIPERCONSUMESTRUCTUREWEAPON = 568\n    RESOURCEBLOCKER = 569\n    TEMPESTWEAPON = 570\n    YOINKMISSILE = 571\n    YOINKVIKINGAIRMISSILE = 572\n    YOINKVIKINGGROUNDMISSILE = 573\n    YOINKSIEGETANKMISSILE = 574\n    WARHOUNDWEAPON = 575\n    EYESTALKWEAPON = 576\n    WIDOWMINEWEAPON = 577\n    WIDOWMINEAIRWEAPON = 578\n    MOTHERSHIPCOREWEAPONWEAPON = 579\n    TORNADOMISSILEWEAPON = 580\n    TORNADOMISSILEDUMMYWEAPON = 581\n    TALONSMISSILEWEAPON = 582\n    CREEPTUMORMISSILE = 583\n    LOCUSTMPEGGAMISSILEWEAPON = 584\n    LOCUSTMPEGGBMISSILEWEAPON = 585\n    LOCUSTMPWEAPON = 586\n    REPULSORCANNONWEAPON = 587\n    COLLAPSIBLEROCKTOWERDIAGONAL = 588\n    COLLAPSIBLETERRANTOWERDIAGONAL = 589\n    COLLAPSIBLETERRANTOWERRAMPLEFT = 590\n    COLLAPSIBLETERRANTOWERRAMPRIGHT = 591\n    ICE2X2NONCONJOINED = 592\n    ICEPROTOSSCRATES = 593\n    PROTOSSCRATES = 594\n    TOWERMINE = 595\n    PICKUPPALLETGAS = 596\n    PICKUPPALLETMINERALS = 597\n    PICKUPSCRAPSALVAGE1X1 = 598\n    PICKUPSCRAPSALVAGE2X2 = 599\n    PICKUPSCRAPSALVAGE3X3 = 600\n    ROUGHTERRAIN = 601\n    UNBUILDABLEBRICKSSMALLUNIT = 602\n    UNBUILDABLEPLATESSMALLUNIT = 603\n    UNBUILDABLEPLATESUNIT = 604\n    UNBUILDABLEROCKSSMALLUNIT = 605\n    XELNAGAHEALINGSHRINE = 606\n    INVISIBLETARGETDUMMY = 607\n    PROTOSSVESPENEGEYSER = 608\n    COLLAPSIBLEROCKTOWER = 609\n    COLLAPSIBLETERRANTOWER = 610\n    THORNLIZARD = 611\n    CLEANINGBOT = 612\n    DESTRUCTIBLEROCK6X6WEAK = 613\n    PROTOSSSNAKESEGMENTDEMO = 614\n    PHYSICSCAPSULE = 615\n    PHYSICSCUBE = 616\n    PHYSICSCYLINDER = 617\n    PHYSICSKNOT = 618\n    PHYSICSL = 619\n    PHYSICSPRIMITIVES = 620\n    PHYSICSSPHERE = 621\n    PHYSICSSTAR = 622\n    CREEPBLOCKER4X4 = 623\n    DESTRUCTIBLECITYDEBRIS2X4VERTICAL = 624\n    DESTRUCTIBLECITYDEBRIS2X4HORIZONTAL = 625\n    DESTRUCTIBLECITYDEBRIS2X6VERTICAL = 626\n    DESTRUCTIBLECITYDEBRIS2X6HORIZONTAL = 627\n    DESTRUCTIBLECITYDEBRIS4X4 = 628\n    DESTRUCTIBLECITYDEBRIS6X6 = 629\n    DESTRUCTIBLECITYDEBRISHUGEDIAGONALBLUR = 630\n    DESTRUCTIBLECITYDEBRISHUGEDIAGONALULBR = 631\n    TESTZERG = 632\n    PATHINGBLOCKERRADIUS1 = 633\n    DESTRUCTIBLEROCKEX12X4VERTICAL = 634\n    DESTRUCTIBLEROCKEX12X4HORIZONTAL = 635\n    DESTRUCTIBLEROCKEX12X6VERTICAL = 636\n    DESTRUCTIBLEROCKEX12X6HORIZONTAL = 637\n    DESTRUCTIBLEROCKEX14X4 = 638\n    DESTRUCTIBLEROCKEX16X6 = 639\n    DESTRUCTIBLEROCKEX1DIAGONALHUGEULBR = 640\n    DESTRUCTIBLEROCKEX1DIAGONALHUGEBLUR = 641\n    DESTRUCTIBLEROCKEX1VERTICALHUGE = 642\n    DESTRUCTIBLEROCKEX1HORIZONTALHUGE = 643\n    DESTRUCTIBLEICE2X4VERTICAL = 644\n    DESTRUCTIBLEICE2X4HORIZONTAL = 645\n    DESTRUCTIBLEICE2X6VERTICAL = 646\n    DESTRUCTIBLEICE2X6HORIZONTAL = 647\n    DESTRUCTIBLEICE4X4 = 648\n    DESTRUCTIBLEICE6X6 = 649\n    DESTRUCTIBLEICEDIAGONALHUGEULBR = 650\n    DESTRUCTIBLEICEDIAGONALHUGEBLUR = 651\n    DESTRUCTIBLEICEVERTICALHUGE = 652\n    DESTRUCTIBLEICEHORIZONTALHUGE = 653\n    DESERTPLANETSEARCHLIGHT = 654\n    DESERTPLANETSTREETLIGHT = 655\n    UNBUILDABLEBRICKSUNIT = 656\n    UNBUILDABLEROCKSUNIT = 657\n    ZERUSDESTRUCTIBLEARCH = 658\n    ARTOSILOPE = 659\n    ANTEPLOTT = 660\n    LABBOT = 661\n    CRABEETLE = 662\n    COLLAPSIBLEROCKTOWERRAMPRIGHT = 663\n    COLLAPSIBLEROCKTOWERRAMPLEFT = 664\n    LABMINERALFIELD = 665\n    LABMINERALFIELD750 = 666\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8OUT = 667\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENESHORT8 = 668\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8OUT = 669\n    SNOWREFINERY_TERRAN_EXTENDINGBRIDGENWSHORT8 = 670\n    TARSONIS_DOORN = 671\n    TARSONIS_DOORNLOWERED = 672\n    TARSONIS_DOORNE = 673\n    TARSONIS_DOORNELOWERED = 674\n    TARSONIS_DOORE = 675\n    TARSONIS_DOORELOWERED = 676\n    TARSONIS_DOORNW = 677\n    TARSONIS_DOORNWLOWERED = 678\n    COMPOUNDMANSION_DOORN = 679\n    COMPOUNDMANSION_DOORNLOWERED = 680\n    COMPOUNDMANSION_DOORNE = 681\n    COMPOUNDMANSION_DOORNELOWERED = 682\n    COMPOUNDMANSION_DOORE = 683\n    COMPOUNDMANSION_DOORELOWERED = 684\n    COMPOUNDMANSION_DOORNW = 685\n    COMPOUNDMANSION_DOORNWLOWERED = 686\n    RAVAGERCOCOON = 687\n    RAVAGER = 688\n    LIBERATOR = 689\n    RAVAGERBURROWED = 690\n    THORAP = 691\n    CYCLONE = 692\n    LOCUSTMPFLYING = 693\n    DISRUPTOR = 694\n    AIURLIGHTBRIDGENE8OUT = 695\n    AIURLIGHTBRIDGENE8 = 696\n    AIURLIGHTBRIDGENE10OUT = 697\n    AIURLIGHTBRIDGENE10 = 698\n    AIURLIGHTBRIDGENE12OUT = 699\n    AIURLIGHTBRIDGENE12 = 700\n    AIURLIGHTBRIDGENW8OUT = 701\n    AIURLIGHTBRIDGENW8 = 702\n    AIURLIGHTBRIDGENW10OUT = 703\n    AIURLIGHTBRIDGENW10 = 704\n    AIURLIGHTBRIDGENW12OUT = 705\n    AIURLIGHTBRIDGENW12 = 706\n    AIURTEMPLEBRIDGENE8OUT = 707\n    AIURTEMPLEBRIDGENE10OUT = 708\n    AIURTEMPLEBRIDGENE12OUT = 709\n    AIURTEMPLEBRIDGENW8OUT = 710\n    AIURTEMPLEBRIDGENW10OUT = 711\n    AIURTEMPLEBRIDGENW12OUT = 712\n    SHAKURASLIGHTBRIDGENE8OUT = 713\n    SHAKURASLIGHTBRIDGENE8 = 714\n    SHAKURASLIGHTBRIDGENE10OUT = 715\n    SHAKURASLIGHTBRIDGENE10 = 716\n    SHAKURASLIGHTBRIDGENE12OUT = 717\n    SHAKURASLIGHTBRIDGENE12 = 718\n    SHAKURASLIGHTBRIDGENW8OUT = 719\n    SHAKURASLIGHTBRIDGENW8 = 720\n    SHAKURASLIGHTBRIDGENW10OUT = 721\n    SHAKURASLIGHTBRIDGENW10 = 722\n    SHAKURASLIGHTBRIDGENW12OUT = 723\n    SHAKURASLIGHTBRIDGENW12 = 724\n    VOIDMPIMMORTALREVIVECORPSE = 725\n    GUARDIANCOCOONMP = 726\n    GUARDIANMP = 727\n    DEVOURERCOCOONMP = 728\n    DEVOURERMP = 729\n    DEFILERMPBURROWED = 730\n    DEFILERMP = 731\n    ORACLESTASISTRAP = 732\n    DISRUPTORPHASED = 733\n    LIBERATORAG = 734\n    AIURLIGHTBRIDGEABANDONEDNE8OUT = 735\n    AIURLIGHTBRIDGEABANDONEDNE8 = 736\n    AIURLIGHTBRIDGEABANDONEDNE10OUT = 737\n    AIURLIGHTBRIDGEABANDONEDNE10 = 738\n    AIURLIGHTBRIDGEABANDONEDNE12OUT = 739\n    AIURLIGHTBRIDGEABANDONEDNE12 = 740\n    AIURLIGHTBRIDGEABANDONEDNW8OUT = 741\n    AIURLIGHTBRIDGEABANDONEDNW8 = 742\n    AIURLIGHTBRIDGEABANDONEDNW10OUT = 743\n    AIURLIGHTBRIDGEABANDONEDNW10 = 744\n    AIURLIGHTBRIDGEABANDONEDNW12OUT = 745\n    AIURLIGHTBRIDGEABANDONEDNW12 = 746\n    COLLAPSIBLEPURIFIERTOWERDEBRIS = 747\n    PORTCITY_BRIDGE_UNITNE8OUT = 748\n    PORTCITY_BRIDGE_UNITNE8 = 749\n    PORTCITY_BRIDGE_UNITSE8OUT = 750\n    PORTCITY_BRIDGE_UNITSE8 = 751\n    PORTCITY_BRIDGE_UNITNW8OUT = 752\n    PORTCITY_BRIDGE_UNITNW8 = 753\n    PORTCITY_BRIDGE_UNITSW8OUT = 754\n    PORTCITY_BRIDGE_UNITSW8 = 755\n    PORTCITY_BRIDGE_UNITNE10OUT = 756\n    PORTCITY_BRIDGE_UNITNE10 = 757\n    PORTCITY_BRIDGE_UNITSE10OUT = 758\n    PORTCITY_BRIDGE_UNITSE10 = 759\n    PORTCITY_BRIDGE_UNITNW10OUT = 760\n    PORTCITY_BRIDGE_UNITNW10 = 761\n    PORTCITY_BRIDGE_UNITSW10OUT = 762\n    PORTCITY_BRIDGE_UNITSW10 = 763\n    PORTCITY_BRIDGE_UNITNE12OUT = 764\n    PORTCITY_BRIDGE_UNITNE12 = 765\n    PORTCITY_BRIDGE_UNITSE12OUT = 766\n    PORTCITY_BRIDGE_UNITSE12 = 767\n    PORTCITY_BRIDGE_UNITNW12OUT = 768\n    PORTCITY_BRIDGE_UNITNW12 = 769\n    PORTCITY_BRIDGE_UNITSW12OUT = 770\n    PORTCITY_BRIDGE_UNITSW12 = 771\n    PORTCITY_BRIDGE_UNITN8OUT = 772\n    PORTCITY_BRIDGE_UNITN8 = 773\n    PORTCITY_BRIDGE_UNITS8OUT = 774\n    PORTCITY_BRIDGE_UNITS8 = 775\n    PORTCITY_BRIDGE_UNITE8OUT = 776\n    PORTCITY_BRIDGE_UNITE8 = 777\n    PORTCITY_BRIDGE_UNITW8OUT = 778\n    PORTCITY_BRIDGE_UNITW8 = 779\n    PORTCITY_BRIDGE_UNITN10OUT = 780\n    PORTCITY_BRIDGE_UNITN10 = 781\n    PORTCITY_BRIDGE_UNITS10OUT = 782\n    PORTCITY_BRIDGE_UNITS10 = 783\n    PORTCITY_BRIDGE_UNITE10OUT = 784\n    PORTCITY_BRIDGE_UNITE10 = 785\n    PORTCITY_BRIDGE_UNITW10OUT = 786\n    PORTCITY_BRIDGE_UNITW10 = 787\n    PORTCITY_BRIDGE_UNITN12OUT = 788\n    PORTCITY_BRIDGE_UNITN12 = 789\n    PORTCITY_BRIDGE_UNITS12OUT = 790\n    PORTCITY_BRIDGE_UNITS12 = 791\n    PORTCITY_BRIDGE_UNITE12OUT = 792\n    PORTCITY_BRIDGE_UNITE12 = 793\n    PORTCITY_BRIDGE_UNITW12OUT = 794\n    PORTCITY_BRIDGE_UNITW12 = 795\n    PURIFIERRICHMINERALFIELD = 796\n    PURIFIERRICHMINERALFIELD750 = 797\n    COLLAPSIBLEPURIFIERTOWERPUSHUNIT = 798\n    LOCUSTMPPRECURSOR = 799\n    RELEASEINTERCEPTORSBEACON = 800\n    ADEPTPHASESHIFT = 801\n    RAVAGERCORROSIVEBILEMISSILE = 802\n    HYDRALISKIMPALEMISSILE = 803\n    CYCLONEMISSILELARGEAIR = 804\n    CYCLONEMISSILE = 805\n    CYCLONEMISSILELARGE = 806\n    THORAALANCE = 807\n    ORACLEWEAPON = 808\n    TEMPESTWEAPONGROUND = 809\n    RAVAGERWEAPONMISSILE = 810\n    SCOUTMPAIRWEAPONLEFT = 811\n    SCOUTMPAIRWEAPONRIGHT = 812\n    ARBITERMPWEAPONMISSILE = 813\n    GUARDIANMPWEAPON = 814\n    DEVOURERMPWEAPONMISSILE = 815\n    DEFILERMPDARKSWARMWEAPON = 816\n    QUEENMPENSNAREMISSILE = 817\n    QUEENMPSPAWNBROODLINGSMISSILE = 818\n    LIGHTNINGBOMBWEAPON = 819\n    HERCPLACEMENT = 820\n    GRAPPLEWEAPON = 821\n    CAUSTICSPRAYMISSILE = 822\n    PARASITICBOMBMISSILE = 823\n    PARASITICBOMBDUMMY = 824\n    ADEPTWEAPON = 825\n    ADEPTUPGRADEWEAPON = 826\n    LIBERATORMISSILE = 827\n    LIBERATORDAMAGEMISSILE = 828\n    LIBERATORAGMISSILE = 829\n    KD8CHARGE = 830\n    KD8CHARGEWEAPON = 831\n    SLAYNELEMENTALGRABWEAPON = 832\n    SLAYNELEMENTALGRABAIRUNIT = 833\n    SLAYNELEMENTALGRABGROUNDUNIT = 834\n    SLAYNELEMENTALWEAPON = 835\n    DESTRUCTIBLEEXPEDITIONGATE6X6 = 836\n    DESTRUCTIBLEZERGINFESTATION3X3 = 837\n    HERC = 838\n    MOOPY = 839\n    REPLICANT = 840\n    SEEKERMISSILE = 841\n    AIURTEMPLEBRIDGEDESTRUCTIBLENE8OUT = 842\n    AIURTEMPLEBRIDGEDESTRUCTIBLENE10OUT = 843\n    AIURTEMPLEBRIDGEDESTRUCTIBLENE12OUT = 844\n    AIURTEMPLEBRIDGEDESTRUCTIBLENW8OUT = 845\n    AIURTEMPLEBRIDGEDESTRUCTIBLENW10OUT = 846\n    AIURTEMPLEBRIDGEDESTRUCTIBLENW12OUT = 847\n    AIURTEMPLEBRIDGEDESTRUCTIBLESW8OUT = 848\n    AIURTEMPLEBRIDGEDESTRUCTIBLESW10OUT = 849\n    AIURTEMPLEBRIDGEDESTRUCTIBLESW12OUT = 850\n    AIURTEMPLEBRIDGEDESTRUCTIBLESE8OUT = 851\n    AIURTEMPLEBRIDGEDESTRUCTIBLESE10OUT = 852\n    AIURTEMPLEBRIDGEDESTRUCTIBLESE12OUT = 853\n    FLYOVERUNIT = 854\n    CORSAIRMP = 855\n    SCOUTMP = 856\n    ARBITERMP = 857\n    SCOURGEMP = 858\n    DEFILERMPPLAGUEWEAPON = 859\n    QUEENMP = 860\n    XELNAGADESTRUCTIBLERAMPBLOCKER6S = 861\n    XELNAGADESTRUCTIBLERAMPBLOCKER6SE = 862\n    XELNAGADESTRUCTIBLERAMPBLOCKER6E = 863\n    XELNAGADESTRUCTIBLERAMPBLOCKER6NE = 864\n    XELNAGADESTRUCTIBLERAMPBLOCKER6N = 865\n    XELNAGADESTRUCTIBLERAMPBLOCKER6NW = 866\n    XELNAGADESTRUCTIBLERAMPBLOCKER6W = 867\n    XELNAGADESTRUCTIBLERAMPBLOCKER6SW = 868\n    XELNAGADESTRUCTIBLERAMPBLOCKER8S = 869\n    XELNAGADESTRUCTIBLERAMPBLOCKER8SE = 870\n    XELNAGADESTRUCTIBLERAMPBLOCKER8E = 871\n    XELNAGADESTRUCTIBLERAMPBLOCKER8NE = 872\n    XELNAGADESTRUCTIBLERAMPBLOCKER8N = 873\n    XELNAGADESTRUCTIBLERAMPBLOCKER8NW = 874\n    XELNAGADESTRUCTIBLERAMPBLOCKER8W = 875\n    XELNAGADESTRUCTIBLERAMPBLOCKER8SW = 876\n    REPTILECRATE = 877\n    SLAYNSWARMHOSTSPAWNFLYER = 878\n    SLAYNELEMENTAL = 879\n    PURIFIERVESPENEGEYSER = 880\n    SHAKURASVESPENEGEYSER = 881\n    COLLAPSIBLEPURIFIERTOWERDIAGONAL = 882\n    CREEPONLYBLOCKER4X4 = 883\n    PURIFIERMINERALFIELD = 884\n    PURIFIERMINERALFIELD750 = 885\n    BATTLESTATIONMINERALFIELD = 886\n    BATTLESTATIONMINERALFIELD750 = 887\n    BEACON_NOVA = 888\n    BEACON_NOVASMALL = 889\n    URSULA = 890\n    ELSECARO_COLONIST_HUT = 891\n    TRANSPORTOVERLORDCOCOON = 892\n    OVERLORDTRANSPORT = 893\n    PYLONOVERCHARGED = 894\n    BYPASSARMORDRONE = 895\n    ADEPTPIERCINGWEAPON = 896\n    CORROSIVEPARASITEWEAPON = 897\n    INFESTEDTERRAN = 898\n    MERCCOMPOUND = 899\n    SUPPLYDEPOTDROP = 900\n    LURKERDEN = 901\n    D8CHARGE = 902\n    THORWRECKAGE = 903\n    GOLIATH = 904\n    TECHREACTOR = 905\n    SS_POWERUPBOMB = 906\n    SS_POWERUPHEALTH = 907\n    SS_POWERUPSIDEMISSILES = 908\n    SS_POWERUPSTRONGERMISSILES = 909\n    LURKEREGG = 910\n    LURKER = 911\n    LURKERBURROWED = 912\n    ARCHIVESEALED = 913\n    INFESTEDCIVILIAN = 914\n    FLAMINGBETTY = 915\n    INFESTEDCIVILIANBURROWED = 916\n    SELENDISINTERCEPTOR = 917\n    SIEGEBREAKERSIEGED = 918\n    SIEGEBREAKER = 919\n    PERDITIONTURRETUNDERGROUND = 920\n    PERDITIONTURRET = 921\n    SENTRYGUNUNDERGROUND = 922\n    SENTRYGUN = 923\n    WARPIG = 924\n    DEVILDOG = 925\n    SPARTANCOMPANY = 926\n    HAMMERSECURITY = 927\n    HELSANGELFIGHTER = 928\n    DUSKWING = 929\n    DUKESREVENGE = 930\n    ODINWRECKAGE = 931\n    HERONUKE = 932\n    KERRIGANCHARBURROWED = 933\n    KERRIGANCHAR = 934\n    SPIDERMINEBURROWED = 935\n    SPIDERMINE = 936\n    ZERATUL = 937\n    URUN = 938\n    MOHANDAR = 939\n    SELENDIS = 940\n    SCOUT = 941\n    OMEGALISKBURROWED = 942\n    OMEGALISK = 943\n    INFESTEDABOMINATIONBURROWED = 944\n    INFESTEDABOMINATION = 945\n    HUNTERKILLERBURROWED = 946\n    HUNTERKILLER = 947\n    INFESTEDTERRANCAMPAIGNBURROWED = 948\n    INFESTEDTERRANCAMPAIGN = 949\n    CHECKSTATION = 950\n    CHECKSTATIONDIAGONALBLUR = 951\n    CHECKSTATIONDIAGONALULBR = 952\n    CHECKSTATIONVERTICAL = 953\n    CHECKSTATIONOPENED = 954\n    CHECKSTATIONDIAGONALBLUROPENED = 955\n    CHECKSTATIONDIAGONALULBROPENED = 956\n    CHECKSTATIONVERTICALOPENED = 957\n    BARRACKSTECHREACTOR = 958\n    FACTORYTECHREACTOR = 959\n    STARPORTTECHREACTOR = 960\n    SPECTRENUKE = 961\n    COLONISTSHIPFLYING = 962\n    COLONISTSHIP = 963\n    BIODOMECOMMANDFLYING = 964\n    BIODOMECOMMAND = 965\n    HERCULESLANDERFLYING = 966\n    HERCULESLANDER = 967\n    ZHAKULDASLIGHTBRIDGEOFF = 968\n    ZHAKULDASLIGHTBRIDGE = 969\n    ZHAKULDASLIBRARYUNITBURROWED = 970\n    ZHAKULDASLIBRARYUNIT = 971\n    XELNAGATEMPLEDOORBURROWED = 972\n    XELNAGATEMPLEDOOR = 973\n    XELNAGATEMPLEDOORURDLBURROWED = 974\n    XELNAGATEMPLEDOORURDL = 975\n    HELSANGELASSAULT = 976\n    AUTOMATEDREFINERY = 977\n    BATTLECRUISERHELIOSMORPH = 978\n    HEALINGPOTIONTESTINSTANT = 979\n    SPACEPLATFORMCLIFFDOOROPEN0 = 980\n    SPACEPLATFORMCLIFFDOOR0 = 981\n    SPACEPLATFORMCLIFFDOOROPEN1 = 982\n    SPACEPLATFORMCLIFFDOOR1 = 983\n    DESTRUCTIBLEGATEDIAGONALBLURLOWERED = 984\n    DESTRUCTIBLEGATEDIAGONALULBRLOWERED = 985\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALBFLOWERED = 986\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALLOWERED = 987\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLFLOWERED = 988\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLOWERED = 989\n    DESTRUCTIBLEGATEDIAGONALBLUR = 990\n    DESTRUCTIBLEGATEDIAGONALULBR = 991\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTALBF = 992\n    DESTRUCTIBLEGATESTRAIGHTHORIZONTAL = 993\n    DESTRUCTIBLEGATESTRAIGHTVERTICALLF = 994\n    DESTRUCTIBLEGATESTRAIGHTVERTICAL = 995\n    METALGATEDIAGONALBLURLOWERED = 996\n    METALGATEDIAGONALULBRLOWERED = 997\n    METALGATESTRAIGHTHORIZONTALBFLOWERED = 998\n    METALGATESTRAIGHTHORIZONTALLOWERED = 999\n    METALGATESTRAIGHTVERTICALLFLOWERED = 1000\n    METALGATESTRAIGHTVERTICALLOWERED = 1001\n    METALGATEDIAGONALBLUR = 1002\n    METALGATEDIAGONALULBR = 1003\n    METALGATESTRAIGHTHORIZONTALBF = 1004\n    METALGATESTRAIGHTHORIZONTAL = 1005\n    METALGATESTRAIGHTVERTICALLF = 1006\n    METALGATESTRAIGHTVERTICAL = 1007\n    SECURITYGATEDIAGONALBLURLOWERED = 1008\n    SECURITYGATEDIAGONALULBRLOWERED = 1009\n    SECURITYGATESTRAIGHTHORIZONTALBFLOWERED = 1010\n    SECURITYGATESTRAIGHTHORIZONTALLOWERED = 1011\n    SECURITYGATESTRAIGHTVERTICALLFLOWERED = 1012\n    SECURITYGATESTRAIGHTVERTICALLOWERED = 1013\n    SECURITYGATEDIAGONALBLUR = 1014\n    SECURITYGATEDIAGONALULBR = 1015\n    SECURITYGATESTRAIGHTHORIZONTALBF = 1016\n    SECURITYGATESTRAIGHTHORIZONTAL = 1017\n    SECURITYGATESTRAIGHTVERTICALLF = 1018\n    SECURITYGATESTRAIGHTVERTICAL = 1019\n    TERRAZINENODEDEADTERRAN = 1020\n    TERRAZINENODEHAPPYPROTOSS = 1021\n    ZHAKULDASLIGHTBRIDGEOFFTOPRIGHT = 1022\n    ZHAKULDASLIGHTBRIDGETOPRIGHT = 1023\n    BATTLECRUISERHELIOS = 1024\n    NUKESILONOVA = 1025\n    ODIN = 1026\n    PYGALISKCOCOON = 1027\n    DEVOURERTISSUEDOODAD = 1028\n    SS_BATTLECRUISERMISSILELAUNCHER = 1029\n    SS_TERRATRONMISSILESPINNERMISSILE = 1030\n    SS_TERRATRONSAW = 1031\n    SS_BATTLECRUISERHUNTERSEEKERMISSILE = 1032\n    SS_LEVIATHANBOMB = 1033\n    DEVOURERTISSUEMISSILE = 1034\n    SS_INTERCEPTOR = 1035\n    SS_LEVIATHANBOMBMISSILE = 1036\n    SS_LEVIATHANSPAWNBOMBMISSILE = 1037\n    SS_FIGHTERMISSILELEFT = 1038\n    SS_FIGHTERMISSILERIGHT = 1039\n    SS_INTERCEPTORSPAWNMISSILE = 1040\n    SS_CARRIERBOSSMISSILE = 1041\n    SS_LEVIATHANTENTACLETARGET = 1042\n    SS_LEVIATHANTENTACLEL2MISSILE = 1043\n    SS_LEVIATHANTENTACLER1MISSILE = 1044\n    SS_LEVIATHANTENTACLER2MISSILE = 1045\n    SS_LEVIATHANTENTACLEL1MISSILE = 1046\n    SS_TERRATRONMISSILE = 1047\n    SS_WRAITHMISSILE = 1048\n    SS_SCOURGEMISSILE = 1049\n    SS_CORRUPTORMISSILE = 1050\n    SS_SWARMGUARDIANMISSILE = 1051\n    SS_STRONGMISSILE1 = 1052\n    SS_STRONGMISSILE2 = 1053\n    SS_FIGHTERDRONEMISSILE = 1054\n    SS_PHOENIXMISSILE = 1055\n    SS_SCOUTMISSILE = 1056\n    SS_INTERCEPTORMISSILE = 1057\n    SS_SCIENCEVESSELMISSILE = 1058\n    SS_BATTLECRUISERMISSILE = 1059\n    D8CLUSTERBOMBWEAPON = 1060\n    D8CLUSTERBOMB = 1061\n    BROODLORDEGG = 1062\n    BROODLORDEGGMISSILE = 1063\n    CIVILIANWEAPON = 1064\n    BATTLECRUISERHELIOSALMWEAPON = 1065\n    BATTLECRUISERLOKILMWEAPON = 1066\n    BATTLECRUISERHELIOSGLMWEAPON = 1067\n    BIOSTASISMISSILE = 1068\n    INFESTEDVENTBROODLORDEGG = 1069\n    INFESTEDVENTCORRUPTOREGG = 1070\n    TENTACLEAMISSILE = 1071\n    TENTACLEBMISSILE = 1072\n    TENTACLECMISSILE = 1073\n    TENTACLEDMISSILE = 1074\n    MUTALISKEGG = 1075\n    INFESTEDVENTMUTALISKEGG = 1076\n    MUTALISKEGGMISSILE = 1077\n    INFESTEDVENTEGGMISSILE = 1078\n    SPORECANNONFIREMISSILE = 1079\n    EXPERIMENTALPLASMAGUNWEAPON = 1080\n    BRUTALISKWEAPON = 1081\n    LOKIHURRICANEMISSILELEFT = 1082\n    LOKIHURRICANEMISSILERIGHT = 1083\n    ODINAAWEAPON = 1084\n    DUSKWINGWEAPON = 1085\n    KERRIGANWEAPON = 1086\n    ULTRASONICPULSEWEAPON = 1087\n    KERRIGANCHARWEAPON = 1088\n    DEVASTATORMISSILEWEAPON = 1089\n    SWANNWEAPON = 1090\n    HAMMERSECURITYLMWEAPON = 1091\n    CONSUMEDNAFEEDBACKWEAPON = 1092\n    URUNWEAPONLEFT = 1093\n    URUNWEAPONRIGHT = 1094\n    HAILSTORMMISSILESWEAPON = 1095\n    COLONYINFESTATIONWEAPON = 1096\n    VOIDSEEKERPHASEMINEBLASTWEAPON = 1097\n    VOIDSEEKERPHASEMINEBLASTSECONDARYWEAPON = 1098\n    TOSSGRENADEWEAPON = 1099\n    TYCHUSGRENADEWEAPON = 1100\n    VILESTREAMWEAPON = 1101\n    WRAITHAIRWEAPONRIGHT = 1102\n    WRAITHAIRWEAPONLEFT = 1103\n    WRAITHGROUNDWEAPON = 1104\n    WEAPONHYBRIDD = 1105\n    KARASSWEAPON = 1106\n    HYBRIDCPLASMAWEAPON = 1107\n    WARBOTBMISSILE = 1108\n    LOKIYAMATOWEAPON = 1109\n    HYPERIONYAMATOSPECIALWEAPON = 1110\n    HYPERIONLMWEAPON = 1111\n    HYPERIONALMWEAPON = 1112\n    VULTUREWEAPON = 1113\n    SCOUTAIRWEAPONLEFT = 1114\n    SCOUTAIRWEAPONRIGHT = 1115\n    HUNTERKILLERWEAPON = 1116\n    GOLIATHAWEAPON = 1117\n    SPARTANCOMPANYAWEAPON = 1118\n    LEVIATHANSCOURGEMISSILE = 1119\n    BIOPLASMIDDISCHARGEWEAPON = 1120\n    VOIDSEEKERWEAPON = 1121\n    HELSANGELFIGHTERWEAPON = 1122\n    DRBATTLECRUISERALMWEAPON = 1123\n    DRBATTLECRUISERGLMWEAPON = 1124\n    HURRICANEMISSILERIGHT = 1125\n    HURRICANEMISSILELEFT = 1126\n    HYBRIDSINGULARITYFEEDBACKWEAPON = 1127\n    DOMINIONKILLTEAMLMWEAPON = 1128\n    ITEMGRENADESWEAPON = 1129\n    ITEMGRAVITYBOMBSWEAPON = 1130\n    TESTHEROTHROWMISSILE = 1131\n    TESTHERODEBUGMISSILEABILITY1WEAPON = 1132\n    TESTHERODEBUGMISSILEABILITY2WEAPON = 1133\n    SPECTRE = 1134\n    VULTURE = 1135\n    LOKI = 1136\n    WRAITH = 1137\n    DOMINIONKILLTEAM = 1138\n    FIREBAT = 1139\n    DIAMONDBACK = 1140\n    G4CHARGEWEAPON = 1141\n    SS_BLACKEDGEBORDER = 1142\n    DEVOURERTISSUESAMPLETUBE = 1143\n    MONOLITH = 1144\n    OBELISK = 1145\n    ARCHIVE = 1146\n    ARTIFACTVAULT = 1147\n    AVERNUSGATECONTROL = 1148\n    GATECONTROLUNIT = 1149\n    BLIMPADS = 1150\n    BLOCKER6X6 = 1151\n    BLOCKER8X8 = 1152\n    BLOCKER16X16 = 1153\n    CARGOTRUCKUNITFLATBED = 1154\n    CARGOTRUCKUNITTRAILER = 1155\n    BLIMP = 1156\n    CASTANARWINDOWLARGEDIAGONALULBRUNIT = 1157\n    BLOCKER4X4 = 1158\n    HOMELARGE = 1159\n    HOMESMALL = 1160\n    ELEVATORBLOCKER = 1161\n    QUESTIONMARK = 1162\n    NYDUSWORMLAVADEATH = 1163\n    SS_BACKGROUNDSPACELARGE = 1164\n    SS_BACKGROUNDSPACETERRAN00 = 1165\n    SS_BACKGROUNDSPACETERRAN02 = 1166\n    SS_BACKGROUNDSPACEZERG00 = 1167\n    SS_BACKGROUNDSPACEZERG02 = 1168\n    SS_CARRIERBOSS = 1169\n    SS_BATTLECRUISER = 1170\n    SS_TERRATRONMISSILESPINNERLAUNCHER = 1171\n    SS_TERRATRONMISSILESPINNER = 1172\n    SS_TERRATRONBEAMTARGET = 1173\n    SS_LIGHTNINGPROJECTORFACERIGHT = 1174\n    SS_SCOURGE = 1175\n    SS_CORRUPTOR = 1176\n    SS_TERRATRONMISSILELAUNCHER = 1177\n    SS_LIGHTNINGPROJECTORFACELEFT = 1178\n    SS_WRAITH = 1179\n    SS_SWARMGUARDIAN = 1180\n    SS_SCOUT = 1181\n    SS_LEVIATHAN = 1182\n    SS_SCIENCEVESSEL = 1183\n    SS_TERRATRON = 1184\n    SECRETDOCUMENTS = 1185\n    PREDATOR = 1186\n    DEFILERBONESAMPLE = 1187\n    DEVOURERTISSUESAMPLE = 1188\n    PROTOSSPSIELEMENTS = 1189\n    TASSADAR = 1190\n    SCIENCEFACILITY = 1191\n    INFESTEDCOCOON = 1192\n    FUSIONREACTOR = 1193\n    BUBBACOMMERCIAL = 1194\n    XELNAGAPRISONHEIGHT2 = 1195\n    XELNAGAPRISON = 1196\n    XELNAGAPRISONNORTH = 1197\n    XELNAGAPRISONNORTHHEIGHT2 = 1198\n    ZERGDROPPODCREEP = 1199\n    IPISTOLAD = 1200\n    L800ETC_AD = 1201\n    NUKENOODLESCOMMERCIAL = 1202\n    PSIOPSCOMMERCIAL = 1203\n    SHIPALARM = 1204\n    SPACEPLATFORMDESTRUCTIBLEJUMBOBLOCKER = 1205\n    SPACEPLATFORMDESTRUCTIBLELARGEBLOCKER = 1206\n    SPACEPLATFORMDESTRUCTIBLEMEDIUMBLOCKER = 1207\n    SPACEPLATFORMDESTRUCTIBLESMALLBLOCKER = 1208\n    TALDARIMMOTHERSHIP = 1209\n    PLASMATORPEDOESWEAPON = 1210\n    PSIDISRUPTOR = 1211\n    HIVEMINDEMULATOR = 1212\n    RAYNOR01 = 1213\n    SCIENCEVESSEL = 1214\n    SCOURGE = 1215\n    SPACEPLATFORMREACTORPATHINGBLOCKER = 1216\n    TAURENOUTHOUSE = 1217\n    TYCHUSEJECTMISSILE = 1218\n    FEEDERLING = 1219\n    ULAANSMOKEBRIDGE = 1220\n    TALDARIMPRISONCRYSTAL = 1221\n    SPACEDIABLO = 1222\n    MURLOCMARINE = 1223\n    XELNAGAPRISONCONSOLE = 1224\n    TALDARIMPRISON = 1225\n    ADJUTANTCAPSULE = 1226\n    XELNAGAVAULT = 1227\n    HOLDINGPEN = 1228\n    SCRAPHUGE = 1229\n    PRISONERCIVILIAN = 1230\n    BIODOMEHALFBUILT = 1231\n    BIODOME = 1232\n    DESTRUCTIBLEKORHALFLAG = 1233\n    DESTRUCTIBLEKORHALPODIUM = 1234\n    DESTRUCTIBLEKORHALTREE = 1235\n    DESTRUCTIBLEKORHALFOLIAGE = 1236\n    DESTRUCTIBLESANDBAGS = 1237\n    CASTANARWINDOWLARGEDIAGONALBLURUNIT = 1238\n    CARGOTRUCKUNITBARRELS = 1239\n    SPORECANNON = 1240\n    STETMANN = 1241\n    BRIDGEBLOCKER4X12 = 1242\n    CIVILIANSHIPWRECKED = 1243\n    SWANN = 1244\n    DRAKKENLASERDRILL = 1245\n    MINDSIPHONRETURNWEAPON = 1246\n    KERRIGANEGG = 1247\n    CHRYSALISEGG = 1248\n    PRISONERSPECTRE = 1249\n    PRISONZEALOT = 1250\n    SCRAPSALVAGE1X1 = 1251\n    SCRAPSALVAGE2X2 = 1252\n    SCRAPSALVAGE3X3 = 1253\n    RAYNORCOMMANDO = 1254\n    OVERMIND = 1255\n    OVERMINDREMAINS = 1256\n    INFESTEDMERCHAVEN = 1257\n    MONLYTHARTIFACTFORCEFIELD = 1258\n    MONLYTHFORCEFIELDSTATUE = 1259\n    VIROPHAGE = 1260\n    PSISHOCKWEAPON = 1261\n    TYCHUSCOMMANDO = 1262\n    BRUTALISK = 1263\n    PYGALISK = 1264\n    VALHALLABASEDESTRUCTIBLEDOORDEAD = 1265\n    VALHALLABASEDESTRUCTIBLEDOOR = 1266\n    VOIDSEEKER = 1267\n    MINDSIPHONWEAPON = 1268\n    WARBOT = 1269\n    PLATFORMCONNECTOR = 1270\n    ARTANIS = 1271\n    TERRAZINECANISTER = 1272\n    HERCULES = 1273\n    MERCENARYFORTRESS = 1274\n    RAYNOR = 1275\n    ARTIFACTPIECE1 = 1276\n    ARTIFACTPIECE2 = 1277\n    ARTIFACTPIECE4 = 1278\n    ARTIFACTPIECE3 = 1279\n    ARTIFACTPIECE5 = 1280\n    RIPFIELDGENERATOR = 1281\n    RIPFIELDGENERATORSMALL = 1282\n    XELNAGAWORLDSHIPVAULT = 1283\n    TYCHUSCHAINGUN = 1284\n    ARTIFACT = 1285\n    CELLBLOCKB = 1286\n    GHOSTLASERLINES = 1287\n    MAINCELLBLOCK = 1288\n    KERRIGAN = 1289\n    DATACORE = 1290\n    SPECIALOPSDROPSHIP = 1291\n    TOSH = 1292\n    CASTANARULTRALISKSHACKLEDUNIT = 1293\n    KARASS = 1294\n    INVISIBLEPYLON = 1295\n    MAAR = 1296\n    HYBRIDDESTROYER = 1297\n    HYBRIDREAVER = 1298\n    HYBRID = 1299\n    TERRAZINENODE = 1300\n    TRANSPORTTRUCK = 1301\n    WALLOFFIRE = 1302\n    WEAPONHYBRIDC = 1303\n    XELNAGATEMPLE = 1304\n    EXPLODINGBARRELLARGE = 1305\n    SUPERWARPGATE = 1306\n    TERRAZINETANK = 1307\n    XELNAGASHRINE = 1308\n    SMCAMERABRIDGE = 1309\n    SMMARSARABARTYCHUSCAMERAS = 1310\n    SMHYPERIONBRIDGESTAGE1HANSONCAMERAS = 1311\n    SMHYPERIONBRIDGESTAGE1HORNERCAMERAS = 1312\n    SMHYPERIONBRIDGESTAGE1TYCHUSCAMERAS = 1313\n    SMHYPERIONBRIDGESTAGE1TOSHCAMERAS = 1314\n    SMHYPERIONARMORYSTAGE1SWANNCAMERAS = 1315\n    SMHYPERIONCANTINATOSHCAMERAS = 1316\n    SMHYPERIONCANTINATYCHUSCAMERAS = 1317\n    SMHYPERIONCANTINAYBARRACAMERAS = 1318\n    SMHYPERIONLABADJUTANTCAMERAS = 1319\n    SMHYPERIONLABCOWINCAMERAS = 1320\n    SMHYPERIONLABHANSONCAMERAS = 1321\n    SMHYPERIONBRIDGETRAYNOR03BRIEFINGCAMERA = 1322\n    SMTESTCAMERA = 1323\n    SMCAMERATERRAN01 = 1324\n    SMCAMERATERRAN02A = 1325\n    SMCAMERATERRAN02B = 1326\n    SMCAMERATERRAN03 = 1327\n    SMCAMERATERRAN04 = 1328\n    SMCAMERATERRAN04A = 1329\n    SMCAMERATERRAN04B = 1330\n    SMCAMERATERRAN05 = 1331\n    SMCAMERATERRAN06A = 1332\n    SMCAMERATERRAN06B = 1333\n    SMCAMERATERRAN06C = 1334\n    SMCAMERATERRAN07 = 1335\n    SMCAMERATERRAN08 = 1336\n    SMCAMERATERRAN09 = 1337\n    SMCAMERATERRAN10 = 1338\n    SMCAMERATERRAN11 = 1339\n    SMCAMERATERRAN12 = 1340\n    SMCAMERATERRAN13 = 1341\n    SMCAMERATERRAN14 = 1342\n    SMCAMERATERRAN15 = 1343\n    SMCAMERATERRAN16 = 1344\n    SMCAMERATERRAN17 = 1345\n    SMCAMERATERRAN20 = 1346\n    SMFIRSTOFFICER = 1347\n    SMHYPERIONBRIDGEBRIEFINGLEFT = 1348\n    SMHYPERIONBRIDGEBRIEFINGRIGHT = 1349\n    SMHYPERIONMEDLABBRIEFING = 1350\n    SMHYPERIONMEDLABBRIEFINGCENTER = 1351\n    SMHYPERIONMEDLABBRIEFINGLEFT = 1352\n    SMHYPERIONMEDLABBRIEFINGRIGHT = 1353\n    SMTOSHSHUTTLESET = 1354\n    SMKERRIGANPHOTO = 1355\n    SMTOSHSHUTTLESET2 = 1356\n    SMMARSARABARJUKEBOXHS = 1357\n    SMMARSARABARKERRIGANPHOTOHS = 1358\n    SMVALERIANFLAGSHIPCORRIDORSSET = 1359\n    SMVALERIANFLAGSHIPCORRIDORSSET2 = 1360\n    SMVALERIANFLAGSHIPCORRIDORSSET3 = 1361\n    SMVALERIANFLAGSHIPCORRIDORSSET4 = 1362\n    SMVALERIANOBSERVATORYSET = 1363\n    SMVALERIANOBSERVATORYSET2 = 1364\n    SMVALERIANOBSERVATORYSET3 = 1365\n    SMVALERIANOBSERVATORYPAINTINGHS = 1366\n    SMCHARBATTLEZONEFLAG = 1367\n    SMUNNSET = 1368\n    SMTERRANREADYROOMSET = 1369\n    SMCHARBATTLEZONESET = 1370\n    SMCHARBATTLEZONESET2 = 1371\n    SMCHARBATTLEZONESET3 = 1372\n    SMCHARBATTLEZONESET4 = 1373\n    SMCHARBATTLEZONESET5 = 1374\n    SMCHARBATTLEZONEARTIFACTHS = 1375\n    SMCHARBATTLEZONERADIOHS = 1376\n    SMCHARBATTLEZONEDROPSHIPHS = 1377\n    SMCHARBATTLEZONEBRIEFCASEHS = 1378\n    SMCHARBATTLEZONEBRIEFINGSET = 1379\n    SMCHARBATTLEZONEBRIEFINGSET2 = 1380\n    SMCHARBATTLEZONEBRIEFINGSETLEFT = 1381\n    SMCHARBATTLEZONEBRIEFINGSETRIGHT = 1382\n    SMMARSARABARBADGEHS = 1383\n    SMHYPERIONCANTINABADGEHS = 1384\n    SMHYPERIONCANTINAPOSTER1HS = 1385\n    SMHYPERIONCANTINAPOSTER2HS = 1386\n    SMHYPERIONCANTINAPOSTER3HS = 1387\n    SMHYPERIONCANTINAPOSTER4HS = 1388\n    SMHYPERIONCANTINAPOSTER5HS = 1389\n    SMFLY = 1390\n    SMBRIDGEWINDOWSPACE = 1391\n    SMBRIDGEPLANETSPACE = 1392\n    SMBRIDGEPLANETSPACEASTEROIDS = 1393\n    SMBRIDGEPLANETAGRIA = 1394\n    SMBRIDGEPLANETAIUR = 1395\n    SMBRIDGEPLANETAVERNUS = 1396\n    SMBRIDGEPLANETBELSHIR = 1397\n    SMBRIDGEPLANETCASTANAR = 1398\n    SMBRIDGEPLANETCHAR = 1399\n    SMBRIDGEPLANETHAVEN = 1400\n    SMBRIDGEPLANETKORHAL = 1401\n    SMBRIDGEPLANETMEINHOFF = 1402\n    SMBRIDGEPLANETMONLYTH = 1403\n    SMBRIDGEPLANETNEWFOLSOM = 1404\n    SMBRIDGEPLANETPORTZION = 1405\n    SMBRIDGEPLANETREDSTONE = 1406\n    SMBRIDGEPLANETSHAKURAS = 1407\n    SMBRIDGEPLANETTARSONIS = 1408\n    SMBRIDGEPLANETTYPHON = 1409\n    SMBRIDGEPLANETTYRADOR = 1410\n    SMBRIDGEPLANETULAAN = 1411\n    SMBRIDGEPLANETULNAR = 1412\n    SMBRIDGEPLANETVALHALLA = 1413\n    SMBRIDGEPLANETXIL = 1414\n    SMBRIDGEPLANETZHAKULDAS = 1415\n    SMMARSARAPLANET = 1416\n    SMNOVA = 1417\n    SMHAVENPLANET = 1418\n    SMHYPERIONBRIDGEBRIEFING = 1419\n    SMHYPERIONBRIDGEBRIEFINGCENTER = 1420\n    SMCHARBATTLEFIELDENDPROPS = 1421\n    SMCHARBATTLEZONETURRET = 1422\n    SMTERRAN01FX = 1423\n    SMTERRAN03FX = 1424\n    SMTERRAN05FX = 1425\n    SMTERRAN05FXMUTALISKS = 1426\n    SMTERRAN05PROPS = 1427\n    SMTERRAN06AFX = 1428\n    SMTERRAN06BFX = 1429\n    SMTERRAN06CFX = 1430\n    SMTERRAN12FX = 1431\n    SMTERRAN14FX = 1432\n    SMTERRAN15FX = 1433\n    SMTERRAN06APROPS = 1434\n    SMTERRAN06BPROPS = 1435\n    SMTERRAN07PROPS = 1436\n    SMTERRAN07FX = 1437\n    SMTERRAN08PROPS = 1438\n    SMTERRAN09FX = 1439\n    SMTERRAN09PROPS = 1440\n    SMTERRAN11FX = 1441\n    SMTERRAN11FXMISSILES = 1442\n    SMTERRAN11FXEXPLOSIONS = 1443\n    SMTERRAN11FXBLOOD = 1444\n    SMTERRAN11FXDEBRIS = 1445\n    SMTERRAN11FXDEBRIS1 = 1446\n    SMTERRAN11FXDEBRIS2 = 1447\n    SMTERRAN11PROPS = 1448\n    SMTERRAN11PROPSBURROWROCKS = 1449\n    SMTERRAN11PROPSRIFLESHELLS = 1450\n    SMTERRAN12PROPS = 1451\n    SMTERRAN13PROPS = 1452\n    SMTERRAN14PROPS = 1453\n    SMTERRAN15PROPS = 1454\n    SMTERRAN16FX = 1455\n    SMTERRAN16FXFLAK = 1456\n    SMTERRAN17PROPS = 1457\n    SMTERRAN17FX = 1458\n    SMMARSARABARPROPS = 1459\n    SMHYPERIONCORRIDORPROPS = 1460\n    ZERATULCRYSTALCHARGE = 1461\n    SMRAYNORHANDS = 1462\n    SMPRESSROOMPROPS = 1463\n    SMRAYNORGUN = 1464\n    SMMARINERIFLE = 1465\n    SMTOSHKNIFE = 1466\n    SMTOSHSHUTTLEPROPS = 1467\n    SMHYPERIONEXTERIOR = 1468\n    SMHYPERIONEXTERIORLOW = 1469\n    SMHYPERIONEXTERIORHOLOGRAM = 1470\n    SMCHARCUTSCENES00 = 1471\n    SMCHARCUTSCENES01 = 1472\n    SMCHARCUTSCENES02 = 1473\n    SMCHARCUTSCENES03 = 1474\n    SMMARSARABARBRIEFINGSET = 1475\n    SMMARSARABARBRIEFINGSET2 = 1476\n    SMMARSARABARBRIEFINGSETLEFT = 1477\n    SMMARSARABARBRIEFINGSETRIGHT = 1478\n    SMMARSARABARBRIEFINGTVMAIN = 1479\n    SMMARSARABARBRIEFINGTVMAIN2 = 1480\n    SMMARSARABARBRIEFINGTVMAIN3 = 1481\n    SMMARSARABARBRIEFINGTVPORTRAIT1 = 1482\n    SMMARSARABARBRIEFINGTVPORTRAIT2 = 1483\n    SMMARSARABARBRIEFINGTVPORTRAIT3 = 1484\n    SMMARSARABARBRIEFINGTVPORTRAIT4 = 1485\n    SMMARSARABARBRIEFINGTVPORTRAIT5 = 1486\n    SMMARSARABARSET = 1487\n    SMMARSARABARSET2 = 1488\n    SMMARSARABARSTARMAPHS = 1489\n    SMMARSARABARTVHS = 1490\n    SMMARSARABARHYDRALISKSKULLHS = 1491\n    SMMARSARABARCORKBOARDHS = 1492\n    SMMARSARABARCORKBOARDBACKGROUND = 1493\n    SMMARSARABARCORKBOARDITEM1HS = 1494\n    SMMARSARABARCORKBOARDITEM2HS = 1495\n    SMMARSARABARCORKBOARDITEM3HS = 1496\n    SMMARSARABARCORKBOARDITEM4HS = 1497\n    SMMARSARABARCORKBOARDITEM5HS = 1498\n    SMMARSARABARCORKBOARDITEM6HS = 1499\n    SMMARSARABARCORKBOARDITEM7HS = 1500\n    SMMARSARABARCORKBOARDITEM8HS = 1501\n    SMMARSARABARCORKBOARDITEM9HS = 1502\n    SMMARSARABARBOTTLESHS = 1503\n    SMVALERIANOBSERVATORYPROPS = 1504\n    SMVALERIANOBSERVATORYSTARMAP = 1505\n    SMBANSHEE = 1506\n    SMVIKING = 1507\n    SMARMORYBANSHEE = 1508\n    SMARMORYDROPSHIP = 1509\n    SMARMORYTANK = 1510\n    SMARMORYVIKING = 1511\n    SMARMORYSPIDERMINE = 1512\n    SMARMORYGHOSTCRATE = 1513\n    SMARMORYSPECTRECRATE = 1514\n    SMARMORYBANSHEEPHCRATE = 1515\n    SMARMORYDROPSHIPPHCRATE = 1516\n    SMARMORYTANKPHCRATE = 1517\n    SMARMORYVIKINGPHCRATE = 1518\n    SMARMORYSPIDERMINEPHCRATE = 1519\n    SMARMORYGHOSTCRATEPHCRATE = 1520\n    SMARMORYSPECTRECRATEPHCRATE = 1521\n    SMARMORYRIFLE = 1522\n    SMDROPSHIP = 1523\n    SMDROPSHIPBLUE = 1524\n    SMHYPERIONARMORYVIKING = 1525\n    SMCHARGATLINGGUN = 1526\n    SMBOUNTYHUNTER = 1527\n    SMCIVILIAN = 1528\n    SMZERGEDHANSON = 1529\n    SMLABASSISTANT = 1530\n    SMHYPERIONARMORER = 1531\n    SMUNNSCREEN = 1532\n    NEWSARCTURUSINTERVIEWSET = 1533\n    NEWSARCTURUSPRESSROOM = 1534\n    SMDONNYVERMILLIONSET = 1535\n    NEWSMEINHOFFREFUGEECENTER = 1536\n    NEWSRAYNORLOGO = 1537\n    NEWSTVEFFECT = 1538\n    SMUNNCAMERA = 1539\n    SMLEEKENOSET = 1540\n    SMTVSTATIC = 1541\n    SMDONNYVERMILLION = 1542\n    SMDONNYVERMILLIONDEATH = 1543\n    SMLEEKENO = 1544\n    SMKATELOCKWELL = 1545\n    SMMIKELIBERTY = 1546\n    SMTERRANREADYROOMLEFTTV = 1547\n    SMTERRANREADYROOMMAINTV = 1548\n    SMTERRANREADYROOMRIGHTTV = 1549\n    SMHYPERIONARMORYSTAGE1SET = 1550\n    SMHYPERIONARMORYSTAGE1SET01 = 1551\n    SMHYPERIONARMORYSTAGE1SET02 = 1552\n    SMHYPERIONARMORYSTAGE1SET03 = 1553\n    SMHYPERIONARMORYSPACELIGHTING = 1554\n    SMHYPERIONARMORYSTAGE1TECHNOLOGYCONSOLEHS = 1555\n    SMHYPERIONBRIDGESTAGE1BOW = 1556\n    SMHYPERIONBRIDGESTAGE1SET = 1557\n    SMHYPERIONBRIDGESTAGE1SET2 = 1558\n    SMHYPERIONBRIDGESTAGE1SET3 = 1559\n    SMHYPERIONBRIDGEHOLOMAP = 1560\n    SMHYPERIONCANTINASTAGE1SET = 1561\n    SMHYPERIONCANTINASTAGE1SET2 = 1562\n    SMHYPERIONCANTINASTAGE1WALLPIECE = 1563\n    SMHYPERIONBRIDGEPROPS = 1564\n    SMHYPERIONCANTINAPROPS = 1565\n    SMHYPERIONMEDLABPROPS = 1566\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE0HS = 1567\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE1HS = 1568\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE2HS = 1569\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE3HS = 1570\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE4HS = 1571\n    SMHYPERIONMEDLABPROTOSSCRYOTUBE5HS = 1572\n    SMHYPERIONMEDLABZERGCRYOTUBE0HS = 1573\n    SMHYPERIONMEDLABZERGCRYOTUBE1HS = 1574\n    SMHYPERIONMEDLABZERGCRYOTUBE2HS = 1575\n    SMHYPERIONMEDLABZERGCRYOTUBE3HS = 1576\n    SMHYPERIONMEDLABZERGCRYOTUBE4HS = 1577\n    SMHYPERIONMEDLABZERGCRYOTUBE5HS = 1578\n    SMHYPERIONMEDLABCRYOTUBEA = 1579\n    SMHYPERIONMEDLABCRYOTUBEB = 1580\n    SMHYPERIONCANTINASTAGE1EXITHS = 1581\n    SMHYPERIONCANTINASTAGE1STAIRCASEHS = 1582\n    SMHYPERIONCANTINASTAGE1TVHS = 1583\n    SMHYPERIONCANTINASTAGE1ARCADEGAMEHS = 1584\n    SMHYPERIONCANTINASTAGE1JUKEBOXHS = 1585\n    SMHYPERIONCANTINASTAGE1CORKBOARDHS = 1586\n    SMHYPERIONCANTINAPROGRESSFRAME = 1587\n    SMHYPERIONCANTINAHYDRACLAWSHS = 1588\n    SMHYPERIONCANTINAMERCCOMPUTERHS = 1589\n    SMHYPERIONCANTINASTAGE1PROGRESS1HS = 1590\n    SMHYPERIONCANTINASTAGE1PROGRESS2HS = 1591\n    SMHYPERIONCANTINASTAGE1PROGRESS3HS = 1592\n    SMHYPERIONCANTINASTAGE1PROGRESS4HS = 1593\n    SMHYPERIONCANTINASTAGE1PROGRESS5HS = 1594\n    SMHYPERIONCANTINASTAGE1PROGRESS6HS = 1595\n    SMHYPERIONCORRIDORSET = 1596\n    SMHYPERIONBRIDGESTAGE1BATTLEREPORTSHS = 1597\n    SMHYPERIONBRIDGESTAGE1CENTERCONSOLEHS = 1598\n    SMHYPERIONBRIDGESTAGE1BATTLECOMMANDHS = 1599\n    SMHYPERIONBRIDGESTAGE1CANTINAHS = 1600\n    SMHYPERIONBRIDGESTAGE1WINDOWHS = 1601\n    SMHYPERIONMEDLABSTAGE1SET = 1602\n    SMHYPERIONMEDLABSTAGE1SET2 = 1603\n    SMHYPERIONMEDLABSTAGE1SETLIGHTS = 1604\n    SMHYPERIONMEDLABSTAGE1CONSOLEHS = 1605\n    SMHYPERIONMEDLABSTAGE1DOORHS = 1606\n    SMHYPERIONMEDLABSTAGE1CRYSTALHS = 1607\n    SMHYPERIONMEDLABSTAGE1ARTIFACTHS = 1608\n    SMHYPERIONLABARTIFACTPART1HS = 1609\n    SMHYPERIONLABARTIFACTPART2HS = 1610\n    SMHYPERIONLABARTIFACTPART3HS = 1611\n    SMHYPERIONLABARTIFACTPART4HS = 1612\n    SMHYPERIONLABARTIFACTBASEHS = 1613\n    SMSHADOWBOX = 1614\n    SMCHARBATTLEZONESHADOWBOX = 1615\n    SMCHARINTERACTIVESKYPARALLAX = 1616\n    SMCHARINTERACTIVE02SKYPARALLAX = 1617\n    SMRAYNORCOMMANDER = 1618\n    SMADJUTANT = 1619\n    SMADJUTANTHOLOGRAM = 1620\n    SMMARAUDER = 1621\n    SMFIREBAT = 1622\n    SMMARAUDERPHCRATE = 1623\n    SMFIREBATPHCRATE = 1624\n    SMRAYNORMARINE = 1625\n    SMMARINE01 = 1626\n    SMMARINE02 = 1627\n    SMMARINE02AOD = 1628\n    SMMARINE03 = 1629\n    SMMARINE04 = 1630\n    SMCADE = 1631\n    SMHALL = 1632\n    SMBRALIK = 1633\n    SMANNABELLE = 1634\n    SMEARL = 1635\n    SMKACHINSKY = 1636\n    SMGENERICMALEGREASEMONKEY01 = 1637\n    SMGENERICMALEGREASEMONKEY02 = 1638\n    SMGENERICMALEOFFICER01 = 1639\n    SMGENERICMALEOFFICER02 = 1640\n    SMSTETMANN = 1641\n    SMCOOPER = 1642\n    SMHILL = 1643\n    SMYBARRA = 1644\n    SMVALERIANMENGSK = 1645\n    SMARCTURUSMENGSK = 1646\n    SMARCTURUSHOLOGRAM = 1647\n    SMZERATUL = 1648\n    SMHYDRALISK = 1649\n    SMHYDRALISKDEAD = 1650\n    SMMUTALISK = 1651\n    SMZERGLING = 1652\n    SCIENTIST = 1653\n    MINERMALE = 1654\n    CIVILIAN = 1655\n    COLONIST = 1656\n    CIVILIANFEMALE = 1657\n    COLONISTFEMALE = 1658\n    HUT = 1659\n    COLONISTHUT = 1660\n    INFESTABLEHUT = 1661\n    INFESTABLECOLONISTHUT = 1662\n    XELNAGASHRINEXIL = 1663\n    PROTOSSRELIC = 1664\n    PICKUPGRENADES = 1665\n    PICKUPPLASMAGUN = 1666\n    PICKUPPLASMAROUNDS = 1667\n    PICKUPMEDICRECHARGE = 1668\n    PICKUPMANARECHARGE = 1669\n    PICKUPRESTORATIONCHARGE = 1670\n    PICKUPCHRONORIFTDEVICE = 1671\n    PICKUPCHRONORIFTCHARGE = 1672\n    GASCANISTER = 1673\n    GASCANISTERPROTOSS = 1674\n    GASCANISTERZERG = 1675\n    MINERALCRYSTAL = 1676\n    PALLETGAS = 1677\n    PALLETMINERALS = 1678\n    NATURALGAS = 1679\n    NATURALMINERALS = 1680\n    NATURALMINERALSRED = 1681\n    PICKUPHEALTH25 = 1682\n    PICKUPHEALTH50 = 1683\n    PICKUPHEALTH100 = 1684\n    PICKUPHEALTHFULL = 1685\n    PICKUPENERGY25 = 1686\n    PICKUPENERGY50 = 1687\n    PICKUPENERGY100 = 1688\n    PICKUPENERGYFULL = 1689\n    PICKUPMINES = 1690\n    PICKUPPSISTORM = 1691\n    CIVILIANCARSUNIT = 1692\n    CRUISERBIKE = 1693\n    TERRANBUGGY = 1694\n    COLONISTVEHICLEUNIT = 1695\n    COLONISTVEHICLEUNIT01 = 1696\n    DUMPTRUCK = 1697\n    TANKERTRUCK = 1698\n    FLATBEDTRUCK = 1699\n    COLONISTSHIPTHANSON02A = 1700\n    PURIFIER = 1701\n    INFESTEDARMORY = 1702\n    INFESTEDBARRACKS = 1703\n    INFESTEDBUNKER = 1704\n    INFESTEDCC = 1705\n    INFESTEDENGBAY = 1706\n    INFESTEDFACTORY = 1707\n    INFESTEDREFINERY = 1708\n    INFESTEDSTARPORT = 1709\n    INFESTEDMISSILETURRET = 1710\n    LOGISTICSHEADQUARTERS = 1711\n    INFESTEDSUPPLY = 1712\n    TARSONISENGINE = 1713\n    TARSONISENGINEFAST = 1714\n    FREIGHTCAR = 1715\n    CABOOSE = 1716\n    HYPERION = 1717\n    MENGSKHOLOGRAMBILLBOARD = 1718\n    TRAYNOR01SIGNSDESTRUCTIBLE1 = 1719\n    ABANDONEDBUILDING = 1720\n    NOVA = 1721\n    FOOD1000 = 1722\n    PSIINDOCTRINATOR = 1723\n    JORIUMSTOCKPILE = 1724\n    ZERGDROPPOD = 1725\n    TERRANDROPPOD = 1726\n    COLONISTBIODOME = 1727\n    COLONISTBIODOMEHALFBUILT = 1728\n    INFESTABLEBIODOME = 1729\n    INFESTABLECOLONISTBIODOME = 1730\n    MEDIC = 1731\n    VIKINGSKY_UNIT = 1732\n    SS_FIGHTER = 1733\n    SS_PHOENIX = 1734\n    SS_CARRIER = 1735\n    SS_BACKGROUNDZERG01 = 1736\n    SS_BACKGROUNDSPACE00 = 1737\n    SS_BACKGROUNDSPACE01 = 1738\n    SS_BACKGROUNDSPACE02 = 1739\n    SS_BACKGROUNDSPACEPROT00 = 1740\n    SS_BACKGROUNDSPACEPROT01 = 1741\n    SS_BACKGROUNDSPACEPROT02 = 1742\n    SS_BACKGROUNDSPACEPROT03 = 1743\n    SS_BACKGROUNDSPACEPROT04 = 1744\n    SS_BACKGROUNDSPACEPROTOSSLARGE = 1745\n    SS_BACKGROUNDSPACEZERGLARGE = 1746\n    SS_BACKGROUNDSPACETERRANLARGE = 1747\n    SS_BACKGROUNDSPACEZERG01 = 1748\n    SS_BACKGROUNDSPACETERRAN01 = 1749\n    BREACHINGCHARGE = 1750\n    INFESTATIONSPIRE = 1751\n    SPACEPLATFORMVENTSUNIT = 1752\n    STONEZEALOT = 1753\n    PRESERVERPRISON = 1754\n    PORTJUNKER = 1755\n    LEVIATHAN = 1756\n    SWARMLING = 1757\n    VALHALLADESTRUCTIBLEWALL = 1758\n    NEWFOLSOMPRISONENTRANCE = 1759\n    ODINBUILD = 1760\n    NUKEPACK = 1761\n    CHARDESTRUCTIBLEROCKCOVER = 1762\n    CHARDESTRUCTIBLEROCKCOVERV = 1763\n    CHARDESTRUCTIBLEROCKCOVERULDR = 1764\n    CHARDESTRUCTIBLEROCKCOVERURDL = 1765\n    MAARWARPINUNIT = 1766\n    EGGPURPLE = 1767\n    TRUCKFLATBEDUNIT = 1768\n    TRUCKSEMIUNIT = 1769\n    TRUCKUTILITYUNIT = 1770\n    INFESTEDCOLONISTSHIP = 1771\n    CASTANARDESTRUCTIBLEDEBRIS = 1772\n    COLONISTTRANSPORT = 1773\n    PRESERVERBASE = 1774\n    PRESERVERA = 1775\n    PRESERVERB = 1776\n    PRESERVERC = 1777\n    TAURENSPACEMARINE = 1778\n    MARSARABRIDGEBLUR = 1779\n    MARSARABRIDGEBRUL = 1780\n    SHORTBRIDGEVERTICAL = 1781\n    SHORTBRIDGEHORIZONTAL = 1782\n    TESTHERO = 1783\n    TESTSHOP = 1784\n    HEALINGPOTIONTESTTARGET = 1785\n    _4SLOTBAG = 1786\n    _6SLOTBAG = 1787\n    _8SLOTBAG = 1788\n    _10SLOTBAG = 1789\n    _12SLOTBAG = 1790\n    _14SLOTBAG = 1791\n    _16SLOTBAG = 1792\n    _18SLOTBAG = 1793\n    _20SLOTBAG = 1794\n    _22SLOTBAG = 1795\n    _24SLOTBAG = 1796\n    REPULSERFIELD6 = 1797\n    REPULSERFIELD8 = 1798\n    REPULSERFIELD10 = 1799\n    REPULSERFIELD12 = 1800\n    DESTRUCTIBLEWALLCORNER45ULBL = 1801\n    DESTRUCTIBLEWALLCORNER45ULUR = 1802\n    DESTRUCTIBLEWALLCORNER45URBR = 1803\n    DESTRUCTIBLEWALLCORNER45 = 1804\n    DESTRUCTIBLEWALLCORNER45UR90L = 1805\n    DESTRUCTIBLEWALLCORNER45UL90B = 1806\n    DESTRUCTIBLEWALLCORNER45BL90R = 1807\n    DESTRUCTIBLEWALLCORNER45BR90T = 1808\n    DESTRUCTIBLEWALLCORNER90L45BR = 1809\n    DESTRUCTIBLEWALLCORNER90T45BL = 1810\n    DESTRUCTIBLEWALLCORNER90R45UL = 1811\n    DESTRUCTIBLEWALLCORNER90B45UR = 1812\n    DESTRUCTIBLEWALLCORNER90TR = 1813\n    DESTRUCTIBLEWALLCORNER90BR = 1814\n    DESTRUCTIBLEWALLCORNER90LB = 1815\n    DESTRUCTIBLEWALLCORNER90LT = 1816\n    DESTRUCTIBLEWALLDIAGONALBLUR = 1817\n    DESTRUCTIBLEWALLDIAGONALBLURLF = 1818\n    DESTRUCTIBLEWALLDIAGONALULBRLF = 1819\n    DESTRUCTIBLEWALLDIAGONALULBR = 1820\n    DESTRUCTIBLEWALLSTRAIGHTVERTICAL = 1821\n    DESTRUCTIBLEWALLVERTICALLF = 1822\n    DESTRUCTIBLEWALLSTRAIGHTHORIZONTAL = 1823\n    DESTRUCTIBLEWALLSTRAIGHTHORIZONTALBF = 1824\n    DEFENSEWALLE = 1825\n    DEFENSEWALLS = 1826\n    DEFENSEWALLW = 1827\n    DEFENSEWALLN = 1828\n    DEFENSEWALLNE = 1829\n    DEFENSEWALLSW = 1830\n    DEFENSEWALLNW = 1831\n    DEFENSEWALLSE = 1832\n    WRECKEDBATTLECRUISERHELIOSFINAL = 1833\n    FIREWORKSBLUE = 1834\n    FIREWORKSRED = 1835\n    FIREWORKSYELLOW = 1836\n    PURIFIERBLASTMARKUNIT = 1837\n    ITEMGRAVITYBOMBS = 1838\n    ITEMGRENADES = 1839\n    ITEMMEDKIT = 1840\n    ITEMMINES = 1841\n    REAPERPLACEMENT = 1842\n    QUEENZAGARAACGLUESCREENDUMMY = 1843\n    OVERSEERZAGARAACGLUESCREENDUMMY = 1844\n    STUKOVINFESTEDCIVILIANACGLUESCREENDUMMY = 1845\n    STUKOVINFESTEDMARINEACGLUESCREENDUMMY = 1846\n    STUKOVINFESTEDSIEGETANKACGLUESCREENDUMMY = 1847\n    STUKOVINFESTEDDIAMONDBACKACGLUESCREENDUMMY = 1848\n    STUKOVINFESTEDBANSHEEACGLUESCREENDUMMY = 1849\n    SILIBERATORACGLUESCREENDUMMY = 1850\n    STUKOVINFESTEDBUNKERACGLUESCREENDUMMY = 1851\n    STUKOVINFESTEDMISSILETURRETACGLUESCREENDUMMY = 1852\n    STUKOVBROODQUEENACGLUESCREENDUMMY = 1853\n    ZEALOTFENIXACGLUESCREENDUMMY = 1854\n    SENTRYFENIXACGLUESCREENDUMMY = 1855\n    ADEPTFENIXACGLUESCREENDUMMY = 1856\n    IMMORTALFENIXACGLUESCREENDUMMY = 1857\n    COLOSSUSFENIXACGLUESCREENDUMMY = 1858\n    DISRUPTORACGLUESCREENDUMMY = 1859\n    OBSERVERFENIXACGLUESCREENDUMMY = 1860\n    SCOUTACGLUESCREENDUMMY = 1861\n    CARRIERFENIXACGLUESCREENDUMMY = 1862\n    PHOTONCANNONFENIXACGLUESCREENDUMMY = 1863\n    PRIMALZERGLINGACGLUESCREENDUMMY = 1864\n    RAVASAURACGLUESCREENDUMMY = 1865\n    PRIMALROACHACGLUESCREENDUMMY = 1866\n    FIREROACHACGLUESCREENDUMMY = 1867\n    PRIMALGUARDIANACGLUESCREENDUMMY = 1868\n    PRIMALHYDRALISKACGLUESCREENDUMMY = 1869\n    PRIMALMUTALISKACGLUESCREENDUMMY = 1870\n    PRIMALIMPALERACGLUESCREENDUMMY = 1871\n    PRIMALSWARMHOSTACGLUESCREENDUMMY = 1872\n    CREEPERHOSTACGLUESCREENDUMMY = 1873\n    PRIMALULTRALISKACGLUESCREENDUMMY = 1874\n    TYRANNOZORACGLUESCREENDUMMY = 1875\n    PRIMALWURMACGLUESCREENDUMMY = 1876\n    HHREAPERACGLUESCREENDUMMY = 1877\n    HHWIDOWMINEACGLUESCREENDUMMY = 1878\n    HHHELLIONTANKACGLUESCREENDUMMY = 1879\n    HHWRAITHACGLUESCREENDUMMY = 1880\n    HHVIKINGACGLUESCREENDUMMY = 1881\n    HHBATTLECRUISERACGLUESCREENDUMMY = 1882\n    HHRAVENACGLUESCREENDUMMY = 1883\n    HHBOMBERPLATFORMACGLUESCREENDUMMY = 1884\n    HHMERCSTARPORTACGLUESCREENDUMMY = 1885\n    HHMISSILETURRETACGLUESCREENDUMMY = 1886\n    HIGHTEMPLARSKINPREVIEW = 1887\n    WARPPRISMSKINPREVIEW = 1888\n    SIEGETANKSKINPREVIEW = 1889\n    LIBERATORSKINPREVIEW = 1890\n    VIKINGSKINPREVIEW = 1891\n    STUKOVINFESTEDTROOPERACGLUESCREENDUMMY = 1892\n    XELNAGADESTRUCTIBLEBLOCKER6S = 1893\n    XELNAGADESTRUCTIBLEBLOCKER6SE = 1894\n    XELNAGADESTRUCTIBLEBLOCKER6E = 1895\n    XELNAGADESTRUCTIBLEBLOCKER6NE = 1896\n    XELNAGADESTRUCTIBLEBLOCKER6N = 1897\n    XELNAGADESTRUCTIBLEBLOCKER6NW = 1898\n    XELNAGADESTRUCTIBLEBLOCKER6W = 1899\n    XELNAGADESTRUCTIBLEBLOCKER6SW = 1900\n    XELNAGADESTRUCTIBLEBLOCKER8S = 1901\n    XELNAGADESTRUCTIBLEBLOCKER8SE = 1902\n    XELNAGADESTRUCTIBLEBLOCKER8E = 1903\n    XELNAGADESTRUCTIBLEBLOCKER8NE = 1904\n    XELNAGADESTRUCTIBLEBLOCKER8N = 1905\n    XELNAGADESTRUCTIBLEBLOCKER8NW = 1906\n    XELNAGADESTRUCTIBLEBLOCKER8W = 1907\n    XELNAGADESTRUCTIBLEBLOCKER8SW = 1908\n    SNOWGLAZESTARTERMP = 1909\n    SHIELDBATTERY = 1910\n    OBSERVERSIEGEMODE = 1911\n    OVERSEERSIEGEMODE = 1912\n    RAVENREPAIRDRONE = 1913\n    HIGHTEMPLARWEAPONMISSILE = 1914\n    CYCLONEMISSILELARGEAIRALTERNATIVE = 1915\n    RAVENSCRAMBLERMISSILE = 1916\n    RAVENREPAIRDRONERELEASEWEAPON = 1917\n    RAVENSHREDDERMISSILEWEAPON = 1918\n    INFESTEDACIDSPINESWEAPON = 1919\n    INFESTORENSNAREATTACKMISSILE = 1920\n    SNARE_PLACEHOLDER = 1921\n    TYCHUSREAPERACGLUESCREENDUMMY = 1922\n    TYCHUSFIREBATACGLUESCREENDUMMY = 1923\n    TYCHUSSPECTREACGLUESCREENDUMMY = 1924\n    TYCHUSMEDICACGLUESCREENDUMMY = 1925\n    TYCHUSMARAUDERACGLUESCREENDUMMY = 1926\n    TYCHUSWARHOUNDACGLUESCREENDUMMY = 1927\n    TYCHUSHERCACGLUESCREENDUMMY = 1928\n    TYCHUSGHOSTACGLUESCREENDUMMY = 1929\n    TYCHUSSCVAUTOTURRETACGLUESCREENDUMMY = 1930\n    ZERATULSTALKERACGLUESCREENDUMMY = 1931\n    ZERATULSENTRYACGLUESCREENDUMMY = 1932\n    ZERATULDARKTEMPLARACGLUESCREENDUMMY = 1933\n    ZERATULIMMORTALACGLUESCREENDUMMY = 1934\n    ZERATULOBSERVERACGLUESCREENDUMMY = 1935\n    ZERATULDISRUPTORACGLUESCREENDUMMY = 1936\n    ZERATULWARPPRISMACGLUESCREENDUMMY = 1937\n    ZERATULPHOTONCANNONACGLUESCREENDUMMY = 1938\n    RENEGADELONGBOLTMISSILEWEAPON = 1939\n    VIKING = 1940\n    RENEGADEMISSILETURRET = 1941\n    PARASITICBOMBRELAYDUMMY = 1942\n    REFINERYRICH = 1943\n    MECHAZERGLINGACGLUESCREENDUMMY = 1944\n    MECHABANELINGACGLUESCREENDUMMY = 1945\n    MECHAHYDRALISKACGLUESCREENDUMMY = 1946\n    MECHAINFESTORACGLUESCREENDUMMY = 1947\n    MECHACORRUPTORACGLUESCREENDUMMY = 1948\n    MECHAULTRALISKACGLUESCREENDUMMY = 1949\n    MECHAOVERSEERACGLUESCREENDUMMY = 1950\n    MECHALURKERACGLUESCREENDUMMY = 1951\n    MECHABATTLECARRIERLORDACGLUESCREENDUMMY = 1952\n    MECHASPINECRAWLERACGLUESCREENDUMMY = 1953\n    MECHASPORECRAWLERACGLUESCREENDUMMY = 1954\n    TROOPERMENGSKACGLUESCREENDUMMY = 1955\n    MEDIVACMENGSKACGLUESCREENDUMMY = 1956\n    BLIMPMENGSKACGLUESCREENDUMMY = 1957\n    MARAUDERMENGSKACGLUESCREENDUMMY = 1958\n    GHOSTMENGSKACGLUESCREENDUMMY = 1959\n    SIEGETANKMENGSKACGLUESCREENDUMMY = 1960\n    THORMENGSKACGLUESCREENDUMMY = 1961\n    VIKINGMENGSKACGLUESCREENDUMMY = 1962\n    BATTLECRUISERMENGSKACGLUESCREENDUMMY = 1963\n    BUNKERDEPOTMENGSKACGLUESCREENDUMMY = 1964\n    MISSILETURRETMENGSKACGLUESCREENDUMMY = 1965\n    ARTILLERYMENGSKACGLUESCREENDUMMY = 1966\n    LOADOUTSPRAY1 = 1967\n    LOADOUTSPRAY2 = 1968\n    LOADOUTSPRAY3 = 1969\n    LOADOUTSPRAY4 = 1970\n    LOADOUTSPRAY5 = 1971\n    LOADOUTSPRAY6 = 1972\n    LOADOUTSPRAY7 = 1973\n    LOADOUTSPRAY8 = 1974\n    LOADOUTSPRAY9 = 1975\n    LOADOUTSPRAY10 = 1976\n    LOADOUTSPRAY11 = 1977\n    LOADOUTSPRAY12 = 1978\n    LOADOUTSPRAY13 = 1979\n    LOADOUTSPRAY14 = 1980\n    PREVIEWBUNKERUPGRADED = 1981\n    INHIBITORZONESMALL = 1982\n    INHIBITORZONEMEDIUM = 1983\n    INHIBITORZONELARGE = 1984\n    ACCELERATIONZONESMALL = 1985\n    ACCELERATIONZONEMEDIUM = 1986\n    ACCELERATIONZONELARGE = 1987\n    ACCELERATIONZONEFLYINGSMALL = 1988\n    ACCELERATIONZONEFLYINGMEDIUM = 1989\n    ACCELERATIONZONEFLYINGLARGE = 1990\n    INHIBITORZONEFLYINGSMALL = 1991\n    INHIBITORZONEFLYINGMEDIUM = 1992\n    INHIBITORZONEFLYINGLARGE = 1993\n    ASSIMILATORRICH = 1994\n    EXTRACTORRICH = 1995\n    MINERALFIELD450 = 1996\n    MINERALFIELDOPAQUE = 1997\n    MINERALFIELDOPAQUE900 = 1998\n    COLLAPSIBLEROCKTOWERDEBRISRAMPLEFTGREEN = 1999\n    COLLAPSIBLEROCKTOWERDEBRISRAMPRIGHTGREEN = 2000\n    COLLAPSIBLEROCKTOWERPUSHUNITRAMPLEFTGREEN = 2001\n    COLLAPSIBLEROCKTOWERPUSHUNITRAMPRIGHTGREEN = 2002\n    COLLAPSIBLEROCKTOWERRAMPLEFTGREEN = 2003\n    COLLAPSIBLEROCKTOWERRAMPRIGHTGREEN = 2004\n\n    def __repr__(self) -> str:\n        return f\"UnitTypeId.{self.name}\"\n\n\nfor item in UnitTypeId:\n    globals()[item.name] = item\n"
  },
  {
    "path": "sc2/ids/upgrade_id.py",
    "content": "from __future__ import annotations\n\n# DO NOT EDIT!\n# This file was automatically generated by \"generate_ids.py\"\nimport enum\n\n\nclass UpgradeId(enum.Enum):\n    NULL = 0\n    CARRIERLAUNCHSPEEDUPGRADE = 1\n    GLIALRECONSTITUTION = 2\n    TUNNELINGCLAWS = 3\n    CHITINOUSPLATING = 4\n    HISECAUTOTRACKING = 5\n    TERRANBUILDINGARMOR = 6\n    TERRANINFANTRYWEAPONSLEVEL1 = 7\n    TERRANINFANTRYWEAPONSLEVEL2 = 8\n    TERRANINFANTRYWEAPONSLEVEL3 = 9\n    NEOSTEELFRAME = 10\n    TERRANINFANTRYARMORSLEVEL1 = 11\n    TERRANINFANTRYARMORSLEVEL2 = 12\n    TERRANINFANTRYARMORSLEVEL3 = 13\n    REAPERSPEED = 14\n    STIMPACK = 15\n    SHIELDWALL = 16\n    PUNISHERGRENADES = 17\n    SIEGETECH = 18\n    HIGHCAPACITYBARRELS = 19\n    BANSHEECLOAK = 20\n    MEDIVACCADUCEUSREACTOR = 21\n    RAVENCORVIDREACTOR = 22\n    HUNTERSEEKER = 23\n    DURABLEMATERIALS = 24\n    PERSONALCLOAKING = 25\n    GHOSTMOEBIUSREACTOR = 26\n    TERRANVEHICLEARMORSLEVEL1 = 27\n    TERRANVEHICLEARMORSLEVEL2 = 28\n    TERRANVEHICLEARMORSLEVEL3 = 29\n    TERRANVEHICLEWEAPONSLEVEL1 = 30\n    TERRANVEHICLEWEAPONSLEVEL2 = 31\n    TERRANVEHICLEWEAPONSLEVEL3 = 32\n    TERRANSHIPARMORSLEVEL1 = 33\n    TERRANSHIPARMORSLEVEL2 = 34\n    TERRANSHIPARMORSLEVEL3 = 35\n    TERRANSHIPWEAPONSLEVEL1 = 36\n    TERRANSHIPWEAPONSLEVEL2 = 37\n    TERRANSHIPWEAPONSLEVEL3 = 38\n    PROTOSSGROUNDWEAPONSLEVEL1 = 39\n    PROTOSSGROUNDWEAPONSLEVEL2 = 40\n    PROTOSSGROUNDWEAPONSLEVEL3 = 41\n    PROTOSSGROUNDARMORSLEVEL1 = 42\n    PROTOSSGROUNDARMORSLEVEL2 = 43\n    PROTOSSGROUNDARMORSLEVEL3 = 44\n    PROTOSSSHIELDSLEVEL1 = 45\n    PROTOSSSHIELDSLEVEL2 = 46\n    PROTOSSSHIELDSLEVEL3 = 47\n    OBSERVERGRAVITICBOOSTER = 48\n    GRAVITICDRIVE = 49\n    EXTENDEDTHERMALLANCE = 50\n    HIGHTEMPLARKHAYDARINAMULET = 51\n    PSISTORMTECH = 52\n    ZERGMELEEWEAPONSLEVEL1 = 53\n    ZERGMELEEWEAPONSLEVEL2 = 54\n    ZERGMELEEWEAPONSLEVEL3 = 55\n    ZERGGROUNDARMORSLEVEL1 = 56\n    ZERGGROUNDARMORSLEVEL2 = 57\n    ZERGGROUNDARMORSLEVEL3 = 58\n    ZERGMISSILEWEAPONSLEVEL1 = 59\n    ZERGMISSILEWEAPONSLEVEL2 = 60\n    ZERGMISSILEWEAPONSLEVEL3 = 61\n    OVERLORDSPEED = 62\n    OVERLORDTRANSPORT = 63\n    BURROW = 64\n    ZERGLINGATTACKSPEED = 65\n    ZERGLINGMOVEMENTSPEED = 66\n    HYDRALISKSPEED = 67\n    ZERGFLYERWEAPONSLEVEL1 = 68\n    ZERGFLYERWEAPONSLEVEL2 = 69\n    ZERGFLYERWEAPONSLEVEL3 = 70\n    ZERGFLYERARMORSLEVEL1 = 71\n    ZERGFLYERARMORSLEVEL2 = 72\n    ZERGFLYERARMORSLEVEL3 = 73\n    INFESTORENERGYUPGRADE = 74\n    CENTRIFICALHOOKS = 75\n    BATTLECRUISERENABLESPECIALIZATIONS = 76\n    BATTLECRUISERBEHEMOTHREACTOR = 77\n    PROTOSSAIRWEAPONSLEVEL1 = 78\n    PROTOSSAIRWEAPONSLEVEL2 = 79\n    PROTOSSAIRWEAPONSLEVEL3 = 80\n    PROTOSSAIRARMORSLEVEL1 = 81\n    PROTOSSAIRARMORSLEVEL2 = 82\n    PROTOSSAIRARMORSLEVEL3 = 83\n    WARPGATERESEARCH = 84\n    HALTECH = 85\n    CHARGE = 86\n    BLINKTECH = 87\n    ANABOLICSYNTHESIS = 88\n    OBVERSEINCUBATION = 89\n    VIKINGJOTUNBOOSTERS = 90\n    ORGANICCARAPACE = 91\n    INFESTORPERISTALSIS = 92\n    ABDOMINALFORTITUDE = 93\n    HYDRALISKSPEEDUPGRADE = 94\n    BANELINGBURROWMOVE = 95\n    COMBATDRUGS = 96\n    STRIKECANNONS = 97\n    TRANSFORMATIONSERVOS = 98\n    PHOENIXRANGEUPGRADE = 99\n    TEMPESTRANGEUPGRADE = 100\n    NEURALPARASITE = 101\n    LOCUSTLIFETIMEINCREASE = 102\n    ULTRALISKBURROWCHARGEUPGRADE = 103\n    ORACLEENERGYUPGRADE = 104\n    RESTORESHIELDS = 105\n    PROTOSSHEROSHIPWEAPON = 106\n    PROTOSSHEROSHIPDETECTOR = 107\n    PROTOSSHEROSHIPSPELL = 108\n    REAPERJUMP = 109\n    INCREASEDRANGE = 110\n    ZERGBURROWMOVE = 111\n    ANIONPULSECRYSTALS = 112\n    TERRANVEHICLEANDSHIPWEAPONSLEVEL1 = 113\n    TERRANVEHICLEANDSHIPWEAPONSLEVEL2 = 114\n    TERRANVEHICLEANDSHIPWEAPONSLEVEL3 = 115\n    TERRANVEHICLEANDSHIPARMORSLEVEL1 = 116\n    TERRANVEHICLEANDSHIPARMORSLEVEL2 = 117\n    TERRANVEHICLEANDSHIPARMORSLEVEL3 = 118\n    FLYINGLOCUSTS = 119\n    ROACHSUPPLY = 120\n    IMMORTALREVIVE = 121\n    DRILLCLAWS = 122\n    CYCLONELOCKONRANGEUPGRADE = 123\n    CYCLONEAIRUPGRADE = 124\n    LIBERATORMORPH = 125\n    ADEPTSHIELDUPGRADE = 126\n    LURKERRANGE = 127\n    IMMORTALBARRIER = 128\n    ADEPTKILLBOUNCE = 129\n    ADEPTPIERCINGATTACK = 130\n    CINEMATICMODE = 131\n    CURSORDEBUG = 132\n    MAGFIELDLAUNCHERS = 133\n    EVOLVEGROOVEDSPINES = 134\n    EVOLVEMUSCULARAUGMENTS = 135\n    BANSHEESPEED = 136\n    MEDIVACRAPIDDEPLOYMENT = 137\n    RAVENRECALIBRATEDEXPLOSIVES = 138\n    MEDIVACINCREASESPEEDBOOST = 139\n    LIBERATORAGRANGEUPGRADE = 140\n    DARKTEMPLARBLINKUPGRADE = 141\n    RAVAGERRANGE = 142\n    RAVENDAMAGEUPGRADE = 143\n    CYCLONELOCKONDAMAGEUPGRADE = 144\n    ARESCLASSWEAPONSSYSTEMVIKING = 145\n    AUTOHARVESTER = 146\n    HYBRIDCPLASMAUPGRADEHARD = 147\n    HYBRIDCPLASMAUPGRADEINSANE = 148\n    INTERCEPTORLIMIT4 = 149\n    INTERCEPTORLIMIT6 = 150\n    _330MMBARRAGECANNONS = 151\n    NOTPOSSIBLESIEGEMODE = 152\n    NEOSTEELFRAME_2 = 153\n    NEOSTEELANDSHRIKETURRETICONUPGRADE = 154\n    OCULARIMPLANTS = 155\n    CROSSSPECTRUMDAMPENERS = 156\n    ORBITALSTRIKE = 157\n    CLUSTERBOMB = 158\n    SHAPEDHULL = 159\n    SPECTRETOOLTIPUPGRADE = 160\n    ULTRACAPACITORS = 161\n    VANADIUMPLATING = 162\n    COMMANDCENTERREACTOR = 163\n    REGENERATIVEBIOSTEEL = 164\n    CELLULARREACTORS = 165\n    BANSHEECLOAKEDDAMAGE = 166\n    DISTORTIONBLASTERS = 167\n    EMPTOWER = 168\n    SUPPLYDEPOTDROP = 169\n    HIVEMINDEMULATOR = 170\n    FORTIFIEDBUNKERCARAPACE = 171\n    PREDATOR = 172\n    SCIENCEVESSEL = 173\n    DUALFUSIONWELDERS = 174\n    ADVANCEDCONSTRUCTION = 175\n    ADVANCEDMEDICTRAINING = 176\n    PROJECTILEACCELERATORS = 177\n    REINFORCEDSUPERSTRUCTURE = 178\n    MULE = 179\n    ORBITALRELAY = 180\n    RAZORWIRE = 181\n    ADVANCEDHEALINGAI = 182\n    TWINLINKEDFLAMETHROWERS = 183\n    NANOCONSTRUCTOR = 184\n    CERBERUSMINES = 185\n    HYPERFLUXOR = 186\n    TRILITHIUMPOWERCELLS = 187\n    PERMANENTCLOAKGHOST = 188\n    PERMANENTCLOAKSPECTRE = 189\n    ULTRASONICPULSE = 190\n    SURVIVALPODS = 191\n    ENERGYSTORAGE = 192\n    FULLBORECANISTERAMMO = 193\n    CAMPAIGNJOTUNBOOSTERS = 194\n    MICROFILTERING = 195\n    PARTICLECANNONAIR = 196\n    VULTUREAUTOREPAIR = 197\n    PSIDISRUPTOR = 198\n    SCIENCEVESSELENERGYMANIPULATION = 199\n    SCIENCEVESSELPLASMAWEAPONRY = 200\n    SHOWGATLINGGUN = 201\n    TECHREACTOR = 202\n    TECHREACTORAI = 203\n    TERRANDEFENSERANGEBONUS = 204\n    X88TNAPALMUPGRADE = 205\n    HURRICANEMISSILES = 206\n    MECHANICALREBIRTH = 207\n    MARINESTIMPACK = 208\n    DARKTEMPLARTACTICS = 209\n    CLUSTERWARHEADS = 210\n    CLOAKDISTORTIONFIELD = 211\n    DEVASTATORMISSILES = 212\n    DISTORTIONTHRUSTERS = 213\n    DYNAMICPOWERROUTING = 214\n    IMPALERROUNDS = 215\n    KINETICFIELDS = 216\n    BURSTCAPACITORS = 217\n    HAILSTORMMISSILEPODS = 218\n    RAPIDDEPLOYMENT = 219\n    REAPERSTIMPACK = 220\n    REAPERD8CHARGE = 221\n    TYCHUS05BATTLECRUISERPENETRATION = 222\n    VIRALPLASMA = 223\n    FIREBATJUGGERNAUTPLATING = 224\n    MULTILOCKTARGETINGSYSTEMS = 225\n    TURBOCHARGEDENGINES = 226\n    DISTORTIONSENSORS = 227\n    INFERNALPREIGNITERS = 228\n    HELLIONCAMPAIGNINFERNALPREIGNITER = 229\n    NAPALMFUELTANKS = 230\n    AUXILIARYMEDBOTS = 231\n    JUGGERNAUTPLATING = 232\n    MARAUDERLIFEBOOST = 233\n    COMBATSHIELD = 234\n    REAPERU238ROUNDS = 235\n    MAELSTROMROUNDS = 236\n    SIEGETANKSHAPEDBLAST = 237\n    TUNGSTENSPIKES = 238\n    BEARCLAWNOZZLES = 239\n    NANOBOTINJECTORS = 240\n    STABILIZERMEDPACKS = 241\n    HALOROCKETS = 242\n    SCAVENGINGSYSTEMS = 243\n    EXTRAMINES = 244\n    ARESCLASSWEAPONSSYSTEM = 245\n    WHITENAPALM = 246\n    VIRALMUNITIONS = 247\n    JACKHAMMERCONCUSSIONGRENADES = 248\n    FIRESUPPRESSIONSYSTEMS = 249\n    FLARERESEARCH = 250\n    MODULARCONSTRUCTION = 251\n    EXPANDEDHULL = 252\n    SHRIKETURRET = 253\n    MICROFUSIONREACTORS = 254\n    WRAITHCLOAK = 255\n    SINGULARITYCHARGE = 256\n    GRAVITICTHRUSTERS = 257\n    YAMATOCANNON = 258\n    DEFENSIVEMATRIX = 259\n    DARKPROTOSS = 260\n    TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL1 = 261\n    TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL2 = 262\n    TERRANINFANTRYWEAPONSULTRACAPACITORSLEVEL3 = 263\n    TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL1 = 264\n    TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL2 = 265\n    TERRANINFANTRYARMORSVANADIUMPLATINGLEVEL3 = 266\n    TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL1 = 267\n    TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL2 = 268\n    TERRANVEHICLEWEAPONSULTRACAPACITORSLEVEL3 = 269\n    TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL1 = 270\n    TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL2 = 271\n    TERRANVEHICLEARMORSVANADIUMPLATINGLEVEL3 = 272\n    TERRANSHIPWEAPONSULTRACAPACITORSLEVEL1 = 273\n    TERRANSHIPWEAPONSULTRACAPACITORSLEVEL2 = 274\n    TERRANSHIPWEAPONSULTRACAPACITORSLEVEL3 = 275\n    TERRANSHIPARMORSVANADIUMPLATINGLEVEL1 = 276\n    TERRANSHIPARMORSVANADIUMPLATINGLEVEL2 = 277\n    TERRANSHIPARMORSVANADIUMPLATINGLEVEL3 = 278\n    HIREKELMORIANMINERSPH = 279\n    HIREDEVILDOGSPH = 280\n    HIRESPARTANCOMPANYPH = 281\n    HIREHAMMERSECURITIESPH = 282\n    HIRESIEGEBREAKERSPH = 283\n    HIREHELSANGELSPH = 284\n    HIREDUSKWINGPH = 285\n    HIREDUKESREVENGE = 286\n    TOSHEASYMODE = 287\n    VOIDRAYSPEEDUPGRADE = 288\n    SMARTSERVOS = 289\n    ARMORPIERCINGROCKETS = 290\n    CYCLONERAPIDFIRELAUNCHERS = 291\n    RAVENENHANCEDMUNITIONS = 292\n    DIGGINGCLAWS = 293\n    CARRIERCARRIERCAPACITY = 294\n    CARRIERLEASHRANGEUPGRADE = 295\n    HURRICANETHRUSTERS = 296\n    TEMPESTGROUNDATTACKUPGRADE = 297\n    FRENZY = 298\n    MICROBIALSHROUD = 299\n    INTERFERENCEMATRIX = 300\n    SUNDERINGIMPACT = 301\n    AMPLIFIEDSHIELDING = 302\n    PSIONICAMPLIFIERS = 303\n    SECRETEDCOATING = 304\n    ENHANCEDSHOCKWAVES = 305\n\n    def __repr__(self) -> str:\n        return f\"UpgradeId.{self.name}\"\n\n\nfor item in UpgradeId:\n    globals()[item.name] = item\n"
  },
  {
    "path": "sc2/main.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport platform\nimport signal\nimport sys\nfrom contextlib import suppress\nfrom dataclasses import dataclass\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Any\n\nimport mpyq\nimport portpicker\nfrom aiohttp import ClientSession, ClientWebSocketResponse\nfrom loguru import logger\n\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2.bot_ai import BotAI\nfrom sc2.client import Client\nfrom sc2.controller import Controller\nfrom sc2.data import CreateGameError, Result, Status\nfrom sc2.game_state import GameState\nfrom sc2.maps import Map\nfrom sc2.observer_ai import ObserverAI\nfrom sc2.player import AbstractPlayer, Bot, BotProcess, Computer, Human\nfrom sc2.portconfig import Portconfig\nfrom sc2.protocol import ConnectionAlreadyClosedError, ProtocolError\nfrom sc2.proxy import Proxy\nfrom sc2.sc2process import KillSwitch, SC2Process\n\n# Set the global logging level\nlogger.remove()\nlogger.add(sys.stdout, level=\"INFO\")\n\n\n@dataclass\nclass GameMatch:\n    \"\"\"Dataclass for hosting a match of SC2.\n    This contains all of the needed information for RequestCreateGame.\n    :param sc2_config: dicts of arguments to unpack into sc2process's construction, one per player\n        second sc2_config will be ignored if only one sc2_instance is spawned\n        e.g. sc2_args=[{\"fullscreen\": True}, {}]: only player 1's sc2instance will be fullscreen\n    :param game_time_limit: The time (in seconds) until a match is artificially declared a Tie\n    \"\"\"\n\n    map_sc2: Map\n    players: list[AbstractPlayer]\n    realtime: bool = False\n    random_seed: int | None = None\n    disable_fog: bool | None = None\n    sc2_config: list[dict] | None = None\n    game_time_limit: int | None = None\n\n    def __post_init__(self) -> None:\n        # avoid players sharing names\n        if (\n            len(self.players) > 1\n            and self.players[0].name is not None\n            and self.players[1].name is not None\n            and self.players[0].name == self.players[1].name\n        ):\n            self.players[1].name += \"2\"\n\n        if self.sc2_config is not None:\n            if isinstance(self.sc2_config, dict):\n                self.sc2_config = [self.sc2_config]\n            if len(self.sc2_config) == 0:\n                self.sc2_config = [{}]\n            while len(self.sc2_config) < len(self.players):\n                self.sc2_config += self.sc2_config\n            self.sc2_config = self.sc2_config[: len(self.players)]\n\n    @property\n    def needed_sc2_count(self) -> int:\n        return sum(player.needs_sc2 for player in self.players)\n\n    @property\n    def host_game_kwargs(self) -> dict[str, Any]:\n        return {\n            \"map_settings\": self.map_sc2,\n            \"players\": self.players,\n            \"realtime\": self.realtime,\n            \"random_seed\": self.random_seed,\n            \"disable_fog\": self.disable_fog,\n        }\n\n    def __repr__(self) -> str:\n        p1 = self.players[0]\n        p1 = p1.name if p1.name else p1\n        p2 = self.players[1]\n        p2 = p2.name if p2.name else p2\n        return f\"Map: {self.map_sc2.name}, {p1} vs {p2}, realtime={self.realtime}, seed={self.random_seed}\"\n\n\nasync def _play_game_human(client, player_id, realtime, game_time_limit):\n    while True:\n        state = await client.observation()\n        if client._game_result:\n            return client._game_result[player_id]\n\n        if game_time_limit and state.observation.observation.game_loop / 22.4 > game_time_limit:\n            logger.info(state.observation.game_loop, state.observation.game_loop / 22.4)\n            return Result.Tie\n\n        if not realtime:\n            await client.step()\n\n\nasync def _play_game_ai(\n    client: Client, player_id: int, ai: BotAI, realtime: bool, game_time_limit: int | None\n) -> Result:\n    # pyrefly: ignore\n    gs: GameState = None\n\n    async def initialize_first_step() -> Result | None:\n        nonlocal gs\n        ai._initialize_variables()\n\n        game_data = await client.get_game_data()\n        game_info = await client.get_game_info()\n        ping_response = await client.ping()\n\n        # This game_data will become self.game_data in botAI\n        ai._prepare_start(\n            client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build\n        )\n        state = await client.observation()\n        # check game result every time we get the observation\n        if client._game_result:\n            await ai.on_end(client._game_result[player_id])\n            return client._game_result[player_id]\n        gs = GameState(state.observation)\n        proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())\n        try:\n            ai._prepare_step(gs, proto_game_info)\n            await ai.on_before_start()\n            ai._prepare_first_step()\n            await ai.on_start()\n        # TODO Catching too general exception Exception (broad-except)\n\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception in AI on_start: {e}\")\n            logger.error(\"Resigning due to previous error\")\n            await ai.on_end(Result.Defeat)\n            return Result.Defeat\n\n    result = await initialize_first_step()\n    if result is not None:\n        return result\n\n    async def run_bot_iteration(iteration: int):\n        nonlocal gs\n        logger.debug(f\"Running AI step, it={iteration} {gs.game_loop / 22.4:.2f}s\")\n        # Issue event like unit created or unit destroyed\n        await ai.issue_events()\n        # In on_step various errors can occur - log properly\n        try:\n            await ai.on_step(iteration)\n        except (AttributeError,) as e:\n            logger.exception(f\"Caught exception: {e}\")\n            raise\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n            raise\n        await ai._after_step()\n        logger.debug(\"Running AI step: done\")\n\n    # Only used in realtime=True\n    previous_state_observation = None\n    for iteration in range(10**10):\n        if realtime and gs:\n            # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game']\n            with suppress(ProtocolError):\n                requested_step = gs.game_loop + client.game_step\n                state = await client.observation(requested_step)\n                # If the bot took too long in the previous observation, request another observation one frame after\n                if state.observation.observation.game_loop > requested_step:\n                    logger.debug(\"Skipped a step in realtime=True\")\n                    previous_state_observation = state.observation\n                    state = await client.observation(state.observation.observation.game_loop + 1)\n        else:\n            state = await client.observation()\n\n        # check game result every time we get the observation\n        if client._game_result:\n            await ai.on_end(client._game_result[player_id])\n            return client._game_result[player_id]\n        gs = GameState(state.observation, previous_state_observation)\n        previous_state_observation = None\n        logger.debug(f\"Score: {gs.score.score}\")\n\n        if game_time_limit and gs.game_loop / 22.4 > game_time_limit:\n            await ai.on_end(Result.Tie)\n            return Result.Tie\n        proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())\n        ai._prepare_step(gs, proto_game_info)\n\n        await run_bot_iteration(iteration)  # Main bot loop\n\n        if not realtime:\n            if not client.in_game:  # Client left (resigned) the game\n                await ai.on_end(client._game_result[player_id])\n                return client._game_result[player_id]\n\n            # 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\n            await client.step()\n    return Result.Undecided\n\n\nasync def _play_game(\n    player: Human | Bot,\n    client: Client,\n    realtime: bool,\n    portconfig: Portconfig | None = None,\n    game_time_limit: int | None = None,\n    rgb_render_config: dict[str, Any] | None = None,\n) -> Result:\n    assert isinstance(realtime, bool), repr(realtime)\n\n    player_id = await client.join_game(\n        player.name, player.race, portconfig=portconfig, rgb_render_config=rgb_render_config\n    )\n    logger.info(f\"Player {player_id} - {player.name if player.name else str(player)}\")\n\n    if isinstance(player, Human):\n        result = await _play_game_human(client, player_id, realtime, game_time_limit)\n    else:\n        result = await _play_game_ai(client, player_id, player.ai, realtime, game_time_limit)\n\n    logger.info(\n        f\"Result for player {player_id} - {player.name if player.name else str(player)}: \"\n        f\"{result._name_ if isinstance(result, Result) else result}\"\n    )\n\n    return result\n\n\nasync def _play_replay(client: Client, ai, realtime: bool = False, player_id: int = 0):\n    ai._initialize_variables()\n\n    game_data = await client.get_game_data()\n    game_info = await client.get_game_info()\n    ping_response = await client.ping()\n\n    client.game_step = 1\n    # This game_data will become self._game_data in botAI\n    ai._prepare_start(\n        client, player_id, game_info, game_data, realtime=realtime, base_build=ping_response.ping.base_build\n    )\n    state = await client.observation()\n    # Check game result every time we get the observation\n    if client._game_result:\n        await ai.on_end(client._game_result[player_id])\n        return client._game_result[player_id]\n    gs = GameState(state.observation)\n    proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())\n    ai._prepare_step(gs, proto_game_info)\n    ai._prepare_first_step()\n    try:\n        await ai.on_start()\n    # TODO Catching too general exception Exception (broad-except)\n\n    except Exception as e:\n        logger.exception(f\"Caught unknown exception in AI replay on_start: {e}\")\n        await ai.on_end(Result.Defeat)\n        return Result.Defeat\n\n    iteration = 0\n    while True:\n        if iteration != 0:\n            if realtime:\n                # TODO: check what happens if a bot takes too long to respond, so that the requested\n                #  game_loop might already be in the past\n                state = await client.observation(gs.game_loop + client.game_step)\n            else:\n                state = await client.observation()\n            # check game result every time we get the observation\n            if client._game_result:\n                try:\n                    await ai.on_end(client._game_result[player_id])\n                except TypeError:\n                    return client._game_result[player_id]\n                return client._game_result[player_id]\n            gs = GameState(state.observation)\n            logger.debug(f\"Score: {gs.score.score}\")\n\n            proto_game_info = await client._execute(game_info=sc_pb.RequestGameInfo())\n            ai._prepare_step(gs, proto_game_info)\n\n        logger.debug(f\"Running AI step, it={iteration} {gs.game_loop * 0.725 * (1 / 16):.2f}s\")\n\n        try:\n            # Issue event like unit created or unit destroyed\n            await ai.issue_events()\n            await ai.on_step(iteration)\n            await ai._after_step()\n\n        # TODO Catching too general exception Exception (broad-except)\n        except Exception as e:\n            if isinstance(e, ProtocolError) and e.is_game_over_error:\n                if realtime:\n                    return None\n                await ai.on_end(Result.Victory)\n                return None\n            # NOTE: this message is caught by pytest suite\n            logger.exception(\"AI step threw an error\")  # DO NOT EDIT!\n            logger.error(f\"Error: {e}\")\n            logger.error(\"Resigning due to previous error\")\n            try:\n                await ai.on_end(Result.Defeat)\n            except TypeError:\n                return Result.Defeat\n            return Result.Defeat\n\n        logger.debug(\"Running AI step: done\")\n\n        if not realtime and not client.in_game:  # Client left (resigned) the game\n            await ai.on_end(Result.Victory)\n            return Result.Victory\n\n        await client.step()  # unindent one line to work in realtime\n\n        iteration += 1\n\n\nasync def _setup_host_game(\n    server: Controller, map_settings, players, realtime, random_seed=None, disable_fog=None, save_replay_as=None\n):\n    r = await server.create_game(map_settings, players, realtime, random_seed, disable_fog)\n    if r.create_game.HasField(\"error\"):\n        err = f\"Could not create game: {CreateGameError(r.create_game.error)}\"\n        if r.create_game.HasField(\"error_details\"):\n            err += f\": {r.create_game.error_details}\"\n        logger.critical(err)\n        raise RuntimeError(err)\n\n    return Client(server._ws, save_replay_as)\n\n\nasync def _host_game(\n    map_settings: Map,\n    players: list[Human | Bot | Computer] | list[Human | Bot],\n    realtime: bool = False,\n    portconfig: Portconfig | None = None,\n    save_replay_as: str | None = None,\n    game_time_limit: int | None = None,\n    rgb_render_config: dict[str, Any] | None = None,\n    random_seed: int | None = None,\n    sc2_version: str | None = None,\n    disable_fog: bool = False,\n):\n    assert players, \"Can't create a game without players\"\n\n    assert any((isinstance(p, (Human, Bot))) for p in players)\n    assert isinstance(players[0], (Human, Bot)), \"First player needs to be a Human or a Bot\"\n\n    async with SC2Process(\n        fullscreen=players[0].fullscreen, render=rgb_render_config is not None, sc2_version=sc2_version\n    ) as server:\n        await server.ping()\n\n        client = await _setup_host_game(\n            server, map_settings, players, realtime, random_seed, disable_fog, save_replay_as\n        )\n        # Bot can decide if it wants to launch with 'raw_affects_selection=True'\n        if isinstance(players[0], Bot) and getattr(players[0].ai, \"raw_affects_selection\", None) is not None:\n            client.raw_affects_selection = players[0].ai.raw_affects_selection\n\n        result = await _play_game(players[0], client, realtime, portconfig, game_time_limit, rgb_render_config)\n        if client.save_replay_path is not None:\n            await client.save_replay(client.save_replay_path)\n        try:\n            await client.leave()\n        except ConnectionAlreadyClosedError:\n            logger.error(\"Connection was closed before the game ended\")\n        await client.quit()\n\n        return result\n\n\nasync def _host_game_aiter(\n    map_settings,\n    players,\n    realtime,\n    portconfig,\n    save_replay_as=None,\n    game_time_limit=None,\n):\n    assert players, \"Can't create a game without players\"\n\n    assert any(isinstance(p, (Human, Bot)) for p in players)\n\n    async with SC2Process() as server:\n        while True:\n            await server.ping()\n\n            client = await _setup_host_game(server, map_settings, players, realtime)\n            if not isinstance(players[0], Human) and getattr(players[0].ai, \"raw_affects_selection\", None) is not None:\n                client.raw_affects_selection = players[0].ai.raw_affects_selection\n\n            try:\n                result = await _play_game(players[0], client, realtime, portconfig, game_time_limit)\n\n                if save_replay_as is not None:\n                    await client.save_replay(save_replay_as)\n                await client.leave()\n            except ConnectionAlreadyClosedError:\n                logger.error(\"Connection was closed before the game ended\")\n                return\n\n            new_players = yield result\n            if new_players is not None:\n                players = new_players\n\n\ndef _host_game_iter(*args, **kwargs):\n    game = _host_game_aiter(*args, **kwargs)\n    new_playerconfig = None\n    while True:\n        new_playerconfig = yield asyncio.get_event_loop().run_until_complete(game.asend(new_playerconfig))\n\n\nasync def _join_game(\n    players: list[Human | Bot],\n    realtime: bool,\n    portconfig: Portconfig,\n    save_replay_as: str | None = None,\n    game_time_limit: int | None = None,\n    sc2_version: str | None = None,\n):\n    async with SC2Process(fullscreen=players[1].fullscreen, sc2_version=sc2_version) as server:\n        await server.ping()\n\n        client = Client(server._ws)\n        # Bot can decide if it wants to launch with 'raw_affects_selection=True'\n        if isinstance(players[1], Bot) and getattr(players[1].ai, \"raw_affects_selection\", None) is not None:\n            client.raw_affects_selection = players[1].ai.raw_affects_selection\n\n        result = await _play_game(players[1], client, realtime, portconfig, game_time_limit)\n        if save_replay_as is not None:\n            await client.save_replay(save_replay_as)\n        try:\n            await client.leave()\n        except ConnectionAlreadyClosedError:\n            logger.error(\"Connection was closed before the game ended\")\n        await client.quit()\n\n        return result\n\n\nasync def _setup_replay(server, replay_path, realtime, observed_id):\n    await server.start_replay(replay_path, realtime, observed_id)\n    return Client(server._ws)\n\n\nasync def _host_replay(\n    replay_path, ai: ObserverAI, realtime: bool, _portconfig: Portconfig, base_build, data_version, observed_id\n):\n    async with SC2Process(fullscreen=False, base_build=base_build, data_hash=data_version) as server:\n        client = await _setup_replay(server, replay_path, realtime, observed_id)\n        result = await _play_replay(client, ai, realtime)\n        return result\n\n\ndef get_replay_version(replay_path: str | Path) -> tuple[str, str]:\n    with Path(replay_path).open(\"rb\") as f:\n        replay_data = f.read()\n        replay_io = BytesIO()\n        replay_io.write(replay_data)\n        replay_io.seek(0)\n        archive = mpyq.MPQArchive(replay_io).extract()\n        # pyrefly: ignore\n        metadata = json.loads(archive[b\"replay.gamemetadata.json\"].decode(\"utf-8\"))\n        return metadata[\"BaseBuild\"], metadata[\"DataVersion\"]\n\n\n# TODO Deprecate run_game function in favor of run_multiple_games\ndef run_game(\n    map_settings: Map,\n    players: list[Human | Bot | Computer],\n    realtime: bool,\n    portconfig: Portconfig | None = None,\n    save_replay_as: str | None = None,\n    game_time_limit: int | None = None,\n    rgb_render_config: dict[str, Any] | None = None,\n    random_seed: int | None = None,\n    sc2_version: str | None = None,\n    disable_fog: bool = False,\n) -> Result | list[Result | None]:\n    \"\"\"\n    Returns a single Result enum if the game was against the built-in computer.\n    Returns a list of two Result enums if the game was \"Human vs Bot\" or \"Bot vs Bot\".\n    \"\"\"\n    result: Result | list[Result | None]\n    if sum(isinstance(p, (Human, Bot)) for p in players) > 1:\n        portconfig = Portconfig()\n        players_non_computer: list[Human | Bot] = [p for p in players if isinstance(p, (Human, Bot))]\n\n        async def run_host_and_join():\n            return await asyncio.gather(\n                _host_game(\n                    map_settings,\n                    players_non_computer,\n                    realtime=realtime,\n                    portconfig=portconfig,\n                    save_replay_as=save_replay_as,\n                    game_time_limit=game_time_limit,\n                    rgb_render_config=rgb_render_config,\n                    random_seed=random_seed,\n                    sc2_version=sc2_version,\n                    disable_fog=disable_fog,\n                ),\n                _join_game(\n                    players_non_computer,\n                    realtime=realtime,\n                    portconfig=portconfig,\n                    save_replay_as=save_replay_as,\n                    game_time_limit=game_time_limit,\n                    sc2_version=sc2_version,\n                ),\n                return_exceptions=True,\n            )\n\n        # pyrefly: ignore\n        result = asyncio.run(run_host_and_join())\n        assert isinstance(result, list)\n        assert all(isinstance(r, Result) for r in result)\n    else:\n        result = asyncio.run(\n            _host_game(\n                map_settings,\n                players,\n                realtime=realtime,\n                portconfig=portconfig,\n                save_replay_as=save_replay_as,\n                game_time_limit=game_time_limit,\n                rgb_render_config=rgb_render_config,\n                random_seed=random_seed,\n                sc2_version=sc2_version,\n                disable_fog=disable_fog,\n            )\n        )\n        assert isinstance(result, Result)\n    return result\n\n\ndef run_replay(ai: ObserverAI, replay_path: Path | str, realtime: bool = False, observed_id: int = 0):\n    portconfig = Portconfig()\n    assert Path(replay_path).is_file(), f\"Replay does not exist at the given path: {replay_path}\"\n    assert Path(replay_path).is_absolute(), (\n        f'Replay path has to be an absolute path, e.g. \"C:/replays/my_replay.SC2Replay\" but given path was \"{replay_path}\"'\n    )\n    base_build, data_version = get_replay_version(replay_path)\n    result = asyncio.get_event_loop().run_until_complete(\n        _host_replay(replay_path, ai, realtime, portconfig, base_build, data_version, observed_id)\n    )\n    return result\n\n\nasync def play_from_websocket(\n    ws_connection: str | ClientWebSocketResponse,\n    player: Human | Bot,\n    realtime: bool,\n    portconfig: Portconfig,\n    save_replay_as: str | None = None,\n    game_time_limit: int | None = None,\n    should_close: bool = True,\n):\n    \"\"\"Use this to play when the match is handled externally e.g. for bot ladder games.\n    Portconfig MUST be specified if not playing vs Computer.\n    :param ws_connection: either a string(\"ws://{address}:{port}/sc2api\") or a ClientWebSocketResponse object\n    :param should_close: closes the connection if True. Use False if something else will reuse the connection\n\n    e.g. ladder usage: play_from_websocket(\"ws://127.0.0.1:5162/sc2api\", MyBot, False, portconfig=my_PC)\n    \"\"\"\n    session = None\n    try:\n        if isinstance(ws_connection, str):\n            session = ClientSession()\n            # pyrefly: ignore\n            ws_connection = await session.ws_connect(ws_connection, timeout=120)\n            should_close = True\n        client = Client(ws_connection)\n        result = await _play_game(player, client, realtime, portconfig, game_time_limit=game_time_limit)\n        if save_replay_as is not None:\n            await client.save_replay(save_replay_as)\n    except ConnectionAlreadyClosedError:\n        logger.error(\"Connection was closed before the game ended\")\n        return None\n    finally:\n        if should_close:\n            await ws_connection.close()\n            if session:\n                await session.close()\n\n    return result\n\n\nasync def run_match(controllers: list[Controller], match: GameMatch, close_ws: bool = True):\n    await _setup_host_game(controllers[0], **match.host_game_kwargs)\n\n    # Setup portconfig beforehand, so all players use the same ports\n    startport = None\n    portconfig: Portconfig = None  # pyrefly: ignore\n    if match.needed_sc2_count > 1:\n        if any(isinstance(player, BotProcess) for player in match.players):\n            portconfig = Portconfig.contiguous_ports()\n            # Most ladder bots generate their server and client ports as [s+2, s+3], [s+4, s+5]\n            startport = portconfig.server[0] - 2\n        else:\n            portconfig = Portconfig()\n\n    proxies = []\n    coros = []\n    players_that_need_sc2 = filter(lambda lambda_player: lambda_player.needs_sc2, match.players)\n    for i, player in enumerate(players_that_need_sc2):\n        if isinstance(player, BotProcess):\n            pport = portpicker.pick_unused_port()\n            p = Proxy(controllers[i], player, pport, match.game_time_limit, match.realtime)\n            proxies.append(p)\n            coros.append(p.play_with_proxy(startport))\n        else:\n            coros.append(\n                play_from_websocket(\n                    controllers[i]._ws,\n                    player,\n                    match.realtime,\n                    portconfig,\n                    should_close=close_ws,\n                    game_time_limit=match.game_time_limit,\n                )\n            )\n\n    async_results = await asyncio.gather(*coros, return_exceptions=True)\n\n    for i, a in enumerate(async_results):\n        if isinstance(a, Exception):\n            logger.error(f\"Exception[{a}] thrown by {[p for p in match.players if p.needs_sc2][i]}\")\n\n    # TODO async_results may contain exceptions\n    # pyrefly: ignore\n    return process_results(match.players, async_results)\n\n\ndef process_results(players: list[AbstractPlayer], async_results: list[Result]) -> dict[AbstractPlayer, Result]:\n    opp_res = {Result.Victory: Result.Defeat, Result.Defeat: Result.Victory, Result.Tie: Result.Tie}\n    result: dict[AbstractPlayer, Result] = {}\n    i = 0\n    for player in players:\n        if player.needs_sc2:\n            if sum(r == Result.Victory for r in async_results) <= 1:\n                result[player] = async_results[i]\n            else:\n                result[player] = Result.Undecided\n            i += 1\n        else:\n            # Computer\n            other_result = async_results[0]\n            result[player] = Result.Undecided\n            if other_result in opp_res:\n                result[player] = opp_res[other_result]\n\n    return result\n\n\nasync def maintain_SCII_count(count: int, controllers: list[Controller], proc_args: list[dict] | None = None) -> None:\n    \"\"\"Modifies the given list of controllers to reflect the desired amount of SCII processes\"\"\"\n    # kill unhealthy ones.\n    if controllers:\n        to_remove = []\n        alive = await asyncio.wait_for(\n            # pyrefly: ignore\n            asyncio.gather(*(c.ping() for c in controllers if not c._ws.closed), return_exceptions=True),\n            timeout=20,\n        )\n        i = 0  # for alive\n        for controller in controllers:\n            if controller._ws.closed:\n                if controller._process._session is not None and not controller._process._session.closed:\n                    await controller._process._session.close()\n                to_remove.append(controller)\n            else:\n                if not isinstance(alive[i], sc_pb.Response):\n                    try:\n                        await controller._process._close_connection()\n                    finally:\n                        to_remove.append(controller)\n                i += 1\n        for c in to_remove:\n            c._process._clean(verbose=False)\n            if c._process in KillSwitch._to_kill:\n                KillSwitch._to_kill.remove(c._process)\n            controllers.remove(c)\n\n    # spawn more\n    if len(controllers) < count:\n        needed = count - len(controllers)\n        if proc_args:\n            index = len(controllers) % len(proc_args)\n        else:\n            proc_args = [{} for _ in range(needed)]\n            index = 0\n        extra = [SC2Process(**proc_args[(index + _) % len(proc_args)]) for _ in range(needed)]\n        logger.info(f\"Creating {needed} more SC2 Processes\")\n        for _ in range(3):\n            if platform.system() == \"Linux\":\n                # Works on linux: start one client after the other\n\n                new_controllers = [await asyncio.wait_for(sc.__aenter__(), timeout=50) for sc in extra]\n            else:\n                # Doesnt seem to work on linux: starting 2 clients nearly at the same time\n                new_controllers = await asyncio.wait_for(\n                    # pyrefly: ignore\n                    asyncio.gather(*[sc.__aenter__() for sc in extra], return_exceptions=True),\n                    timeout=50,\n                )\n\n            controllers.extend(c for c in new_controllers if isinstance(c, Controller))\n            if len(controllers) == count:\n                # pyrefly: ignore\n                await asyncio.wait_for(asyncio.gather(*(c.ping() for c in controllers)), timeout=20)\n                break\n            extra = [\n                extra[i] for i, result in enumerate(new_controllers) if not isinstance(new_controllers, Controller)\n            ]\n        else:\n            logger.critical(\"Could not launch sufficient SC2\")\n            raise RuntimeError\n\n    # kill excess\n    while len(controllers) > count:\n        proc = controllers.pop()\n        proc = proc._process\n        logger.info(f\"Removing SCII listening to {proc._port}\")\n        await proc._close_connection()\n        proc._clean(verbose=False)\n        if proc in KillSwitch._to_kill:\n            KillSwitch._to_kill.remove(proc)\n\n\ndef run_multiple_games(matches: list[GameMatch]):\n    return asyncio.get_event_loop().run_until_complete(a_run_multiple_games(matches))\n\n\n# TODO Catching too general exception Exception (broad-except)\n\n\nasync def a_run_multiple_games(matches: list[GameMatch]) -> list[dict[AbstractPlayer, Result]]:\n    \"\"\"Run multiple matches.\n    Non-python bots are supported.\n    When playing bot vs bot, this is less likely to fatally crash than repeating run_game()\n    \"\"\"\n    if not matches:\n        return []\n\n    results: list[dict[AbstractPlayer, Result]] = []\n    controllers: list[Controller] = []\n    for m in matches:\n        result = None\n        dont_restart = m.needed_sc2_count == 2\n        try:\n            await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config)\n            result = await run_match(controllers, m, close_ws=dont_restart)\n        except SystemExit as e:\n            logger.info(f\"Game exit'ed as {e} during match {m}\")\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n            logger.info(f\"Exception {e} thrown in match {m}\")\n        finally:\n            if dont_restart:  # Keeping them alive after a non-computer match can cause crashes\n                await maintain_SCII_count(0, controllers, m.sc2_config)\n            if result is not None:\n                results.append(result)\n    KillSwitch.kill_all()\n    return results\n\n\n# TODO Catching too general exception Exception (broad-except)\n\n\nasync def a_run_multiple_games_nokill(matches: list[GameMatch]) -> list[dict[AbstractPlayer, Result]]:\n    \"\"\"Run multiple matches while reusing SCII processes.\n    Prone to crashes and stalls\n    \"\"\"\n    # FIXME: check whether crashes between bot-vs-bot are avoidable or not\n    if not matches:\n        return []\n\n    # Start the matches\n    results: list[dict[AbstractPlayer, Result]] = []\n    controllers: list[Controller] = []\n    for m in matches:\n        logger.info(f\"Starting match {1 + len(results)} / {len(matches)}: {m}\")\n        result = None\n        try:\n            await maintain_SCII_count(m.needed_sc2_count, controllers, m.sc2_config)\n            result = await run_match(controllers, m, close_ws=False)\n        except SystemExit as e:\n            logger.critical(f\"Game sys.exit'ed as {e} during match {m}\")\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n            logger.info(f\"Exception {e} thrown in match {m}\")\n        finally:\n            for c in controllers:\n                try:\n                    await c.ping()\n                    if c._status != Status.launched:\n                        await c._execute(leave_game=sc_pb.RequestLeaveGame())\n                except Exception as e:\n                    logger.exception(f\"Caught unknown exception: {e}\")\n                    if not (isinstance(e, ProtocolError) and e.is_game_over_error):\n                        logger.info(f\"controller {c.__dict__} threw {e}\")\n            if result is not None:\n                results.append(result)\n\n    # Fire the killswitch manually, instead of letting the winning player fire it.\n    # pyrefly: ignore\n    await asyncio.wait_for(asyncio.gather(*(c._process._close_connection() for c in controllers)), timeout=50)\n    KillSwitch.kill_all()\n    signal.signal(signal.SIGINT, signal.SIG_DFL)\n\n    return results\n"
  },
  {
    "path": "sc2/maps.py",
    "content": "from __future__ import annotations\n\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom sc2.paths import Paths\n\n\ndef get(name: str) -> Map:\n    # Iterate through 2 folder depths\n    for map_dir in (p for p in Paths.MAPS.iterdir()):\n        if map_dir.is_dir():\n            for map_file in (p for p in map_dir.iterdir()):\n                if Map.matches_target_map_name(map_file, name):\n                    return Map(map_file)\n        elif Map.matches_target_map_name(map_dir, name):\n            return Map(map_dir)\n\n    raise KeyError(f\"Map '{name}' was not found. Please put the map file in \\\"/StarCraft II/Maps/\\\".\")\n\n\nclass Map:\n    def __init__(self, path: Path) -> None:\n        self.path = path\n\n        if self.path.is_absolute():\n            try:\n                self.relative_path = self.path.relative_to(Paths.MAPS)\n            except ValueError:  # path not relative to basedir\n                logger.warning(f\"Using absolute path: {self.path}\")\n                self.relative_path = self.path\n        else:\n            self.relative_path = self.path\n\n    @property\n    def name(self) -> str:\n        return self.path.stem\n\n    @property\n    def data(self) -> bytes:\n        with Path(self.path).open(\"rb\") as f:\n            return f.read()\n\n    def __repr__(self) -> str:\n        return f\"Map({self.path})\"\n\n    @classmethod\n    def is_map_file(cls, file: Path) -> bool:\n        return file.is_file() and file.suffix == \".SC2Map\"\n\n    @classmethod\n    def matches_target_map_name(cls, file: Path, name: str) -> bool:\n        return cls.is_map_file(file) and file.stem == name\n"
  },
  {
    "path": "sc2/observer_ai.py",
    "content": "\"\"\"\nThis class is very experimental and probably not up to date and needs to be refurbished.\nIf it works, you can watch replays with it.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom sc2.bot_ai_internal import BotAIInternal\nfrom sc2.data import Alert, Result\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass ObserverAI(BotAIInternal):\n    \"\"\"Base class for bots.\"\"\"\n\n    @property\n    def time(self) -> float:\n        \"\"\"Returns time in seconds, assumes the game is played on 'faster'\"\"\"\n        return self.state.game_loop / 22.4  # / (1/1.4) * (1/16)\n\n    @property\n    def time_formatted(self) -> str:\n        \"\"\"Returns time as string in min:sec format\"\"\"\n        t = self.time\n        return f\"{int(t // 60):02}:{int(t % 60):02}\"\n\n    def alert(self, alert_code: Alert) -> bool:\n        \"\"\"\n        Check if alert is triggered in the current step.\n        Possible alerts are listed here https://github.com/Blizzard/s2client-proto/blob/e38efed74c03bec90f74b330ea1adda9215e655f/s2clientprotocol/sc2api.proto#L679-L702\n\n        Example use:\n\n            from sc2.data import Alert\n            if self.alert(Alert.AddOnComplete):\n                print(\"Addon Complete\")\n\n        Alert codes::\n\n            AlertError\n            AddOnComplete\n            BuildingComplete\n            BuildingUnderAttack\n            LarvaHatched\n            MergeComplete\n            MineralsExhausted\n            MorphComplete\n            MothershipComplete\n            MULEExpired\n            NuclearLaunchDetected\n            NukeComplete\n            NydusWormDetected\n            ResearchComplete\n            TrainError\n            TrainUnitComplete\n            TrainWorkerComplete\n            TransformationComplete\n            UnitUnderAttack\n            UpgradeComplete\n            VespeneExhausted\n            WarpInComplete\n\n        :param alert_code:\n        \"\"\"\n        assert isinstance(alert_code, Alert), f\"alert_code {alert_code} is no Alert\"\n        return alert_code.value in self.state.alerts\n\n    @property\n    def start_location(self) -> Point2:\n        \"\"\"\n        Returns the spawn location of the bot, using the position of the first created townhall.\n        This will be None if the bot is run on an arcade or custom map that does not feature townhalls at game start.\n        \"\"\"\n        return self.game_info.player_start_location\n\n    @property\n    def enemy_start_locations(self) -> list[Point2]:\n        \"\"\"Possible start locations for enemies.\"\"\"\n        return self.game_info.start_locations\n\n    async def get_available_abilities(\n        self, units: list[Unit] | Units, ignore_resource_requirements: bool = False\n    ) -> list[list[AbilityId]]:\n        \"\"\"Returns available abilities of one or more units. Right now only checks cooldown, energy cost, and whether the ability has been researched.\n\n        Examples::\n\n            units_abilities = await self.get_available_abilities(self.units)\n\n        or::\n\n            units_abilities = await self.get_available_abilities([self.units.random])\n\n        :param units:\n        :param ignore_resource_requirements:\"\"\"\n        return await self.client.query_available_abilities(units, ignore_resource_requirements)\n\n    async def on_unit_destroyed(self, unit_tag: int) -> None:\n        \"\"\"\n        Override this in your bot class.\n        This will event will be called when a unit (or structure, friendly or enemy) dies.\n        For enemy units, this only works if the enemy unit was in vision on death.\n\n        :param unit_tag:\n        \"\"\"\n\n    async def on_unit_created(self, unit: Unit) -> None:\n        \"\"\"Override this in your bot class. This function is called when a unit is created.\n\n        :param unit:\"\"\"\n\n    async def on_building_construction_started(self, unit: Unit) -> None:\n        \"\"\"\n        Override this in your bot class.\n        This function is called when a building construction has started.\n\n        :param unit:\n        \"\"\"\n\n    async def on_building_construction_complete(self, unit: Unit) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called when a building\n        construction is completed.\n\n        :param unit:\n        \"\"\"\n\n    async def on_upgrade_complete(self, upgrade: UpgradeId) -> None:\n        \"\"\"\n        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.\n\n        :param upgrade:\n        \"\"\"\n\n    async def on_start(self) -> None:\n        \"\"\"\n        Override this in your bot class. This function is called after \"on_start\".\n        At this point, game_data, game_info and the first iteration of game_state (self.state) are available.\n        \"\"\"\n\n    async def on_step(self, iteration: int):\n        \"\"\"\n        You need to implement this function!\n        Override this in your bot class.\n        This function is called on every game step (looped in realtime mode).\n\n        :param iteration:\n        \"\"\"\n        raise NotImplementedError\n\n    async def on_end(self, game_result: Result) -> None:\n        \"\"\"Override this in your bot class. This function is called at the end of a game.\n\n        :param game_result:\"\"\"\n"
  },
  {
    "path": "sc2/paths.py",
    "content": "from __future__ import annotations\n\nimport os\nimport platform\nimport re\nimport sys\nfrom contextlib import suppress\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom sc2 import wsl\n\nBASEDIR = {\n    \"Windows\": \"C:/Program Files (x86)/StarCraft II\",\n    \"WSL1\": \"/mnt/c/Program Files (x86)/StarCraft II\",\n    \"WSL2\": \"/mnt/c/Program Files (x86)/StarCraft II\",\n    \"Darwin\": \"/Applications/StarCraft II\",\n    \"Linux\": \"~/StarCraftII\",\n    \"WineLinux\": \"~/.wine/drive_c/Program Files (x86)/StarCraft II\",\n}\n\nUSERPATH: dict[str, str | None] = {\n    \"Windows\": \"Documents\\\\StarCraft II\\\\ExecuteInfo.txt\",\n    \"WSL1\": \"Documents/StarCraft II/ExecuteInfo.txt\",\n    \"WSL2\": \"Documents/StarCraft II/ExecuteInfo.txt\",\n    \"Darwin\": \"Library/Application Support/Blizzard/StarCraft II/ExecuteInfo.txt\",\n    \"Linux\": None,\n    \"WineLinux\": None,\n}\n\nBINPATH = {\n    \"Windows\": \"SC2_x64.exe\",\n    \"WSL1\": \"SC2_x64.exe\",\n    \"WSL2\": \"SC2_x64.exe\",\n    \"Darwin\": \"SC2.app/Contents/MacOS/SC2\",\n    \"Linux\": \"SC2_x64\",\n    \"WineLinux\": \"SC2_x64.exe\",\n}\n\nCWD: dict[str, str | None] = {\n    \"Windows\": \"Support64\",\n    \"WSL1\": \"Support64\",\n    \"WSL2\": \"Support64\",\n    \"Darwin\": None,\n    \"Linux\": None,\n    \"WineLinux\": \"Support64\",\n}\n\n\ndef platform_detect():\n    pf = os.environ.get(\"SC2PF\", platform.system())\n    if pf == \"Linux\":\n        return wsl.detect() or pf\n    return pf\n\n\nPF: str = platform_detect()\n\n\ndef get_home():\n    \"\"\"Get home directory of user, using Windows home directory for WSL.\"\"\"\n    if PF in {\"WSL1\", \"WSL2\"}:\n        return wsl.get_wsl_home() or Path.home().expanduser()\n    return Path.home().expanduser()\n\n\ndef get_user_sc2_install():\n    \"\"\"Attempts to find a user's SC2 install if their OS has ExecuteInfo.txt\"\"\"\n    if USERPATH[PF]:\n        einfo = str(get_home() / Path(USERPATH[PF]))  # pyrefly: ignore\n        if Path(einfo).is_file():\n            with Path(einfo).open() as f:\n                content = f.read()\n            if content:\n                base = re.search(r\" = (.*)Versions\", content).group(1)  # pyrefly: ignore\n                if PF in {\"WSL1\", \"WSL2\"}:\n                    base = str(wsl.win_path_to_wsl_path(base))\n\n                if Path(base).exists():\n                    return base\n    return None\n\n\ndef get_env() -> None:\n    # TODO: Linux env conf from: https://github.com/deepmind/pysc2/blob/master/pysc2/run_configs/platforms.py\n    return None\n\n\ndef get_runner_args(cwd):\n    wine_path = os.environ.get(\"WINE\")\n    if wine_path is not None:\n        runner_file = Path(wine_path)\n        runner_file = runner_file if runner_file.is_file() else runner_file / \"wine\"\n        \"\"\"\n        TODO Is converting linux path really necessary?\n        That would convert\n        '/home/burny/Games/battlenet/drive_c/Program Files (x86)/StarCraft II/Support64'\n        to\n        'Z:\\\\home\\\\burny\\\\Games\\\\battlenet\\\\drive_c\\\\Program Files (x86)\\\\StarCraft II\\\\Support64'\n        \"\"\"\n        return [runner_file, \"start\", \"/d\", cwd, \"/unix\"]\n    return []\n\n\ndef latest_executeble(versions_dir, base_build=None):\n    latest = None\n\n    if base_build is not None:\n        with suppress(ValueError):\n            latest = (\n                int(base_build[4:]),\n                max(p for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith(str(base_build))),\n            )\n\n    if base_build is None or latest is None:\n        latest = max((int(p.name[4:]), p) for p in versions_dir.iterdir() if p.is_dir() and p.name.startswith(\"Base\"))\n\n    version, path = latest\n\n    if version < 55958:\n        logger.critical(\"Your SC2 binary is too old. Upgrade to 3.16.1 or newer.\")\n        sys.exit(1)\n    return path / BINPATH[PF]\n\n\nclass _MetaPaths(type):\n    \"\"\" \"Lazily loads paths to allow importing the library even if SC2 isn't installed.\"\"\"\n\n    def __setup(cls):\n        if PF not in BASEDIR:\n            logger.critical(f\"Unsupported platform '{PF}'\")\n            sys.exit(1)\n\n        try:\n            base = os.environ.get(\"SC2PATH\") or get_user_sc2_install() or BASEDIR[PF]\n            cls.BASE = Path(base).expanduser()  # pyrefly: ignore\n            cls.EXECUTABLE = latest_executeble(cls.BASE / \"Versions\")\n            cls.CWD = cls.BASE / CWD[PF] if CWD[PF] else None  # pyrefly: ignore\n\n            cls.REPLAYS = cls.BASE / \"Replays\"  # pyrefly: ignore\n\n            if (cls.BASE / \"maps\").exists():\n                cls.MAPS = cls.BASE / \"maps\"  # pyrefly: ignore\n            else:\n                cls.MAPS = cls.BASE / \"Maps\"  # pyrefly: ignore\n        except FileNotFoundError as e:\n            logger.critical(f\"SC2 installation not found: File '{e.filename}' does not exist.\")\n            sys.exit(1)\n\n    def __getattr__(cls, attr):\n        cls.__setup()\n        return getattr(cls, attr)\n\n\nclass Paths(metaclass=_MetaPaths):\n    \"\"\"Paths for SC2 folders, lazily loaded using the above metaclass.\"\"\"\n"
  },
  {
    "path": "sc2/pixel_map.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom pathlib import Path\n\nimport numpy as np\n\nfrom s2clientprotocol.common_pb2 import ImageData\nfrom sc2.position import Point2, _PointLike\n\n\nclass PixelMap:\n    def __init__(self, proto: ImageData, in_bits: bool = False) -> None:\n        \"\"\"\n        :param proto:\n        :param in_bits:\n        \"\"\"\n        self._proto = proto\n        # Used for copying pixelmaps\n        self._in_bits: bool = in_bits\n\n        assert self.width * self.height == (8 if in_bits else 1) * len(self._proto.data), (\n            f\"{self.width * self.height} {(8 if in_bits else 1) * len(self._proto.data)}\"\n        )\n        buffer_data = np.frombuffer(self._proto.data, dtype=np.uint8)\n        if in_bits:\n            buffer_data = np.unpackbits(buffer_data)\n        self.data_numpy = buffer_data.reshape(self._proto.size.y, self._proto.size.x)\n\n    @property\n    def width(self) -> int:\n        return self._proto.size.x\n\n    @property\n    def height(self) -> int:\n        return self._proto.size.y\n\n    @property\n    def bits_per_pixel(self) -> int:\n        return self._proto.bits_per_pixel\n\n    @property\n    def bytes_per_pixel(self) -> int:\n        return self._proto.bits_per_pixel // 8\n\n    def __getitem__(self, pos: _PointLike) -> int:\n        \"\"\"Example usage: is_pathable = self._game_info.pathing_grid[Point2((20, 20))] != 0\"\"\"\n        assert 0 <= pos[0] < self.width, f\"x is {pos[0]}, self.width is {self.width}\"\n        assert 0 <= pos[1] < self.height, f\"y is {pos[1]}, self.height is {self.height}\"\n        # pyrefly: ignore\n        return int(self.data_numpy[pos[1], pos[0]])\n\n    def __setitem__(self, pos: _PointLike, value: int) -> None:\n        \"\"\"Example usage: self._game_info.pathing_grid[Point2((20, 20))] = 255\"\"\"\n        assert 0 <= pos[0] < self.width, f\"x is {pos[0]}, self.width is {self.width}\"\n        assert 0 <= pos[1] < self.height, f\"y is {pos[1]}, self.height is {self.height}\"\n        assert 0 <= value <= 254 * self._in_bits + 1, (\n            f\"value is {value}, it should be between 0 and {254 * self._in_bits + 1}\"\n        )\n        assert isinstance(value, int), f\"value is of type {type(value)}, it should be an integer\"\n        # pyrefly: ignore\n        self.data_numpy[pos[1], pos[0]] = value\n\n    def is_set(self, p: tuple[int, int]) -> bool:\n        return self[p] != 0\n\n    def is_empty(self, p: tuple[int, int]) -> bool:\n        return not self.is_set(p)\n\n    def copy(self) -> PixelMap:\n        return PixelMap(self._proto, in_bits=self._in_bits)\n\n    def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> set[Point2]:\n        nodes: set[Point2] = set()\n        # pyrefly: ignore\n        queue: list[tuple[int, int]] = [start_point]\n\n        while queue:\n            x, y = queue.pop()\n\n            if not (0 <= x < self.width and 0 <= y < self.height):\n                continue\n\n            if Point2((x, y)) in nodes:\n                continue\n\n            if pred(self[x, y]):\n                nodes.add(Point2((x, y)))\n                queue += [(x + a, y + b) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)]\n        return nodes\n\n    def flood_fill_all(self, pred: Callable[[int], bool]) -> set[frozenset[Point2]]:\n        groups: set[frozenset[Point2]] = set()\n\n        for x in range(self.width):\n            for y in range(self.height):\n                if any((x, y) in g for g in groups):\n                    continue\n\n                if pred(self[x, y]):\n                    groups.add(frozenset(self.flood_fill(Point2((x, y)), pred)))\n\n        return groups\n\n    def print(self, wide: bool = False) -> None:\n        for y in range(self.height):\n            for x in range(self.width):\n                print(\"#\" if self.is_set((x, y)) else \" \", end=(\" \" if wide else \"\"))\n            print(\"\")\n\n    def save_image(self, filename: str | Path) -> None:\n        data = [(0, 0, self[x, y]) for y in range(self.height) for x in range(self.width)]\n\n        from PIL import Image\n\n        im = Image.new(\"RGB\", (self.width, self.height))\n        im.putdata(data)\n        im.save(filename)\n\n    def plot(self) -> None:\n        import matplotlib.pyplot as plt\n\n        plt.imshow(self.data_numpy, origin=\"lower\")\n        plt.show()\n"
  },
  {
    "path": "sc2/player.py",
    "content": "from __future__ import annotations\n\nfrom abc import ABC\nfrom pathlib import Path\n\nfrom s2clientprotocol import sc2api_pb2\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import AIBuild, Difficulty, PlayerType, Race\n\n\nclass AbstractPlayer(ABC):\n    def __init__(\n        self,\n        p_type: PlayerType,\n        race: Race | None = None,\n        name: str | None = None,\n        difficulty: Difficulty | None = None,\n        ai_build: AIBuild | None = None,\n        fullscreen: bool = False,\n    ) -> None:\n        assert isinstance(p_type, PlayerType), f\"p_type is of type {type(p_type)}\"\n        assert name is None or isinstance(name, str), f\"name is of type {type(name)}\"\n\n        self.name = name\n        self.type = p_type\n        self.fullscreen = fullscreen\n        if race is not None:\n            self.race = race\n        if p_type == PlayerType.Computer:\n            assert isinstance(difficulty, Difficulty), f\"difficulty is of type {type(difficulty)}\"\n            # Workaround, proto information does not carry ai_build info\n            # We cant set that in the Player classmethod\n            assert ai_build is None or isinstance(ai_build, AIBuild), f\"ai_build is of type {type(ai_build)}\"\n            self.difficulty = difficulty\n            self.ai_build = ai_build\n\n        elif p_type == PlayerType.Observer:\n            assert race is None\n            assert difficulty is None\n            assert ai_build is None\n\n        else:\n            assert isinstance(race, Race), f\"race is of type {type(race)}\"\n            assert difficulty is None\n            assert ai_build is None\n\n    @property\n    def needs_sc2(self) -> bool:\n        return not isinstance(self, Computer)\n\n\nclass Human(AbstractPlayer):\n    def __init__(self, race: Race, name: str | None = None, fullscreen: bool = False) -> None:\n        super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen)\n\n    def __str__(self) -> str:\n        if self.name is not None:\n            return f\"Human({self.race._name_}, name={self.name!r})\"\n        return f\"Human({self.race._name_})\"\n\n\nclass Bot(AbstractPlayer):\n    def __init__(self, race: Race, ai: BotAI, name: str | None = None, fullscreen: bool = False) -> None:\n        \"\"\"\n        AI can be None if this player object is just used to inform the\n        server about player types.\n        \"\"\"\n        assert isinstance(ai, BotAI) or ai is None, f\"ai is of type {type(ai)}, inherit BotAI from bot_ai.py\"\n        super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen)\n        self.ai: BotAI = ai\n\n    def __str__(self) -> str:\n        if self.name is not None:\n            return f\"Bot {self.ai.__class__.__name__}({self.race._name_}), name={self.name!r})\"\n        return f\"Bot {self.ai.__class__.__name__}({self.race._name_})\"\n\n\nclass Computer(AbstractPlayer):\n    def __init__(\n        self, race: Race, difficulty: Difficulty = Difficulty.Easy, ai_build: AIBuild = AIBuild.RandomBuild\n    ) -> None:\n        super().__init__(PlayerType.Computer, race, difficulty=difficulty, ai_build=ai_build)\n\n    def __str__(self) -> str:\n        if self.ai_build is not None:\n            return f\"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})\"\n        return f\"Computer {self.difficulty._name_}({self.race._name_})\"\n\n\nclass Observer(AbstractPlayer):\n    def __init__(self) -> None:\n        super().__init__(PlayerType.Observer)\n\n    def __str__(self) -> str:\n        return \"Observer\"\n\n\nclass Player(AbstractPlayer):\n    def __init__(\n        self,\n        player_id: int,\n        p_type: PlayerType,\n        # None in case of observer\n        requested_race: Race | None,\n        difficulty: Difficulty | None = None,\n        actual_race: Race | None = None,\n        name: str | None = None,\n        ai_build: AIBuild | None = None,\n    ) -> None:\n        super().__init__(p_type, requested_race, difficulty=difficulty, name=name, ai_build=ai_build)\n        self.id: int = player_id\n        self.actual_race: Race | None = actual_race\n\n    @classmethod\n    def from_proto(cls, proto: sc2api_pb2.PlayerInfo) -> Player:\n        if PlayerType(proto.type) == PlayerType.Observer:\n            return cls(proto.player_id, PlayerType(proto.type), None, None, None)\n        return cls(\n            proto.player_id,\n            PlayerType(proto.type),\n            Race(proto.race_requested),\n            Difficulty(proto.difficulty) if proto.HasField(\"difficulty\") else None,\n            Race(proto.race_actual) if proto.HasField(\"race_actual\") else None,\n            proto.player_name if proto.HasField(\"player_name\") else None,\n        )\n\n\nclass BotProcess(AbstractPlayer):\n    \"\"\"\n    Class for handling bots launched externally, including non-python bots.\n    Default parameters comply with sc2ai and aiarena ladders.\n\n    :param path: the executable file's path\n    :param launch_list: list of strings that launches the bot e.g. [\"python\", \"run.py\"] or [\"run.exe\"]\n    :param race: bot's race\n    :param name: bot's name\n    :param sc2port_arg: the accepted argument name for the port of the sc2 instance to listen to\n    :param hostaddress_arg: the accepted argument name for the address of the sc2 instance to listen to\n    :param match_arg: the accepted argument name for the starting port to generate a portconfig from\n    :param realtime_arg: the accepted argument name for specifying realtime\n    :param other_args: anything else that is needed\n\n    e.g. to call a bot capable of running on the bot ladders:\n        BotProcess(os.getcwd(), \"python run.py\", Race.Terran, \"INnoVation\")\n    \"\"\"\n\n    def __init__(\n        self,\n        path: str | Path,\n        launch_list: list[str],\n        race: Race,\n        name: str | None = None,\n        sc2port_arg: str = \"--GamePort\",\n        hostaddress_arg: str = \"--LadderServer\",\n        match_arg: str = \"--StartPort\",\n        realtime_arg: str = \"--RealTime\",\n        other_args: str | None = None,\n        stdout: str | None = None,\n    ) -> None:\n        super().__init__(PlayerType.Participant, race, name=name)\n        assert Path(path).exists()\n        self.path = path\n        self.launch_list = launch_list\n        self.sc2port_arg = sc2port_arg\n        self.match_arg = match_arg\n        self.hostaddress_arg = hostaddress_arg\n        self.realtime_arg = realtime_arg\n        self.other_args = other_args\n        self.stdout = stdout\n\n    def __repr__(self) -> str:\n        if self.name is not None:\n            return f\"Bot {self.name}({self.race.name} from {self.launch_list})\"\n        return f\"Bot({self.race.name} from {self.launch_list})\"\n\n    def cmd_line(\n        self, sc2port: int | str, matchport: int | str | None, hostaddress: str, realtime: bool = False\n    ) -> list[str]:\n        \"\"\"\n\n        :param sc2port: the port that the launched sc2 instance listens to\n        :param matchport: some starting port that both bots use to generate identical portconfigs.\n                Note: This will not be sent if playing vs computer\n        :param hostaddress: the address the sc2 instances used\n        :param realtime: 1 or 0, indicating whether the match is played in realtime or not\n        :return: string that will be used to start the bot's process\n        \"\"\"\n        cmd_line = [\n            *self.launch_list,\n            self.sc2port_arg,\n            str(sc2port),\n            self.hostaddress_arg,\n            hostaddress,\n        ]\n        if matchport is not None:\n            cmd_line.extend([self.match_arg, str(matchport)])\n        if self.other_args is not None:\n            cmd_line.append(self.other_args)\n        if realtime:\n            cmd_line.extend([self.realtime_arg])\n        return cmd_line\n"
  },
  {
    "path": "sc2/portconfig.py",
    "content": "from __future__ import annotations\n\nimport json\n\n# pyre-fixme[21]\nimport portpicker\n\n\nclass Portconfig:\n    \"\"\"\n    A data class for ports used by participants to join a match.\n\n    EVERY participant joining the match must send the same sets of ports to join successfully.\n    SC2 needs 2 ports per connection (one for data, one as a 'header'), which is why the ports come in pairs.\n\n    :param guests: number of non-hosting participants in a match (i.e. 1 less than the number of participants)\n    :param server_ports: [int portA, int portB]\n    :param player_ports: [[int port1A, int port1B], [int port2A, int port2B], ... ]\n\n    .shared is deprecated, and should TODO be removed soon (once ladderbots' __init__.py doesnt specify them).\n\n    .server contains the pair of ports used by the participant 'hosting' the match\n\n    .players contains a pair of ports for every 'guest' (non-hosting participants) in the match\n    E.g. for 1v1, there will be only 1 guest. For 2v2 (coming soonTM), there would be 3 guests.\n    \"\"\"\n\n    def __init__(\n        self, guests: int = 1, server_ports: list[int] | None = None, player_ports: list[list[int]] | None = None\n    ) -> None:\n        self.shared = None\n        self._picked_ports: list[int] = []\n        if server_ports:\n            self.server: list[int] = server_ports\n        else:\n            self.server = [portpicker.pick_unused_port() for _ in range(2)]\n            self._picked_ports.extend(self.server)\n        if player_ports:\n            self.players: list[list[int]] = player_ports\n        else:\n            self.players = [[portpicker.pick_unused_port() for _ in range(2)] for _ in range(guests)]\n            self._picked_ports.extend([port for player in self.players for port in player])\n\n    def clean(self) -> None:\n        while self._picked_ports:\n            portpicker.return_port(self._picked_ports.pop())\n\n    def __str__(self) -> str:\n        return f\"Portconfig(shared={self.shared}, server={self.server}, players={self.players})\"\n\n    @property\n    def as_json(self) -> str:\n        return json.dumps({\"shared\": self.shared, \"server\": self.server, \"players\": self.players})\n\n    @classmethod\n    def contiguous_ports(cls, guests: int = 1, attempts: int = 40) -> Portconfig:\n        \"\"\"Returns a Portconfig with adjacent ports\"\"\"\n        for _ in range(attempts):\n            start = portpicker.pick_unused_port()\n            others = [start + j for j in range(1, 2 + guests * 2)]\n            if all(portpicker.is_port_free(p) for p in others):\n                server_ports = [start, others.pop(0)]\n                player_ports = []\n                while others:\n                    player_ports.append([others.pop(0), others.pop(0)])\n                pc = cls(server_ports=server_ports, player_ports=player_ports)\n                pc._picked_ports.append(start)\n                return pc\n        raise portpicker.NoFreePortFoundError()\n\n    @classmethod\n    def from_json(cls, json_data: bytearray | bytes | str) -> Portconfig:\n        data = json.loads(json_data)\n        return cls(server_ports=data[\"server\"], player_ports=data[\"players\"])\n"
  },
  {
    "path": "sc2/position.py",
    "content": "from __future__ import annotations\n\nimport itertools\nimport math\nimport random\nfrom collections.abc import Iterable\nfrom typing import (\n    Any,\n    Protocol,\n    SupportsFloat,\n    SupportsIndex,\n    TypeVar,\n    Union,\n)\n\nfrom s2clientprotocol import common_pb2 as common_pb\n\n\nclass HasPosition2D(Protocol):\n    @property\n    def position(self) -> Point2: ...\n\n\n_PointLike = Union[tuple[float, float], tuple[float, float], tuple[float, ...]]\n_PosLike = Union[HasPosition2D, _PointLike]\n_TPosLike = TypeVar(\"_TPosLike\", bound=_PosLike)\n\nEPSILON: float = 10**-8\n\n\ndef _sign(num: SupportsFloat | SupportsIndex) -> float:\n    return math.copysign(1, num)\n\n\nclass Pointlike(tuple[float, ...]):\n    T = TypeVar(\"T\", bound=\"Pointlike\")\n\n    @property\n    def position(self: T) -> T:\n        return self\n\n    def distance_to(self, target: _PosLike) -> float:\n        \"\"\"Calculate a single distance from a point or unit to another point or unit\n\n        :param target:\"\"\"\n        # pyrefly: ignore\n        position: tuple[float, ...] = target if isinstance(target, tuple) else target.position\n        return math.hypot(self[0] - position[0], self[1] - position[1])\n\n    def distance_to_point2(self, p: _PointLike) -> float:\n        \"\"\"Same as the function above, but should be a bit faster because of the dropped asserts\n        and conversion.\n\n        :param p:\"\"\"\n        return math.hypot(self[0] - p[0], self[1] - p[1])\n\n    def _distance_squared(self, p2: _PointLike) -> float:\n        \"\"\"Function used to not take the square root as the distances will stay proportionally the same.\n        This is to speed up the sorting process.\n\n        :param p2:\"\"\"\n        return (self[0] - p2[0]) ** 2 + (self[1] - p2[1]) ** 2\n\n    def sort_by_distance(self, ps: Iterable[_TPosLike]) -> list[_TPosLike]:\n        \"\"\"This returns the target points sorted as list.\n        You should not pass a set or dict since those are not sortable.\n        If you want to sort your units towards a point, use 'units.sorted_by_distance_to(point)' instead.\n\n        :param ps:\"\"\"\n        return sorted(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position))\n\n    def closest(self, ps: Iterable[_TPosLike]) -> _TPosLike:\n        \"\"\"This function assumes the 2d distance is meant\n\n        :param ps:\"\"\"\n        assert ps, \"ps is empty\"\n\n        return min(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position))\n\n    def distance_to_closest(self, ps: Iterable[_TPosLike]) -> float:\n        \"\"\"This function assumes the 2d distance is meant\n        :param ps:\"\"\"\n        assert ps, \"ps is empty\"\n        closest_distance = math.inf\n        for p in ps:\n            # pyrefly: ignore\n            p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position\n            distance = self.distance_to_point2(p2)\n            if distance <= closest_distance:\n                closest_distance = distance\n        return closest_distance\n\n    def furthest(self, ps: Iterable[_TPosLike]) -> _TPosLike:\n        \"\"\"This function assumes the 2d distance is meant\n\n        :param ps: Units object, or iterable of Unit or Point2\"\"\"\n        assert ps, \"ps is empty\"\n\n        return max(ps, key=lambda p: self.distance_to_point2(p if isinstance(p, tuple) else p.position))\n\n    def distance_to_furthest(self, ps: Iterable[_PosLike]) -> float:\n        \"\"\"This function assumes the 2d distance is meant\n\n        :param ps:\"\"\"\n        assert ps, \"ps is empty\"\n        furthest_distance = -math.inf\n        for p in ps:\n            # pyrefly: ignore\n            p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position\n            distance = self.distance_to_point2(p2)\n            if distance >= furthest_distance:\n                furthest_distance = distance\n        return furthest_distance\n\n    def offset(self: T, p: _PointLike) -> T:\n        \"\"\"\n\n        :param p:\n        \"\"\"\n        return self.__class__(a + b for a, b in itertools.zip_longest(self, p[: len(self)], fillvalue=0))\n\n    def unit_axes_towards(self: T, p: _PointLike) -> T:\n        \"\"\"\n\n        :param p:\n        \"\"\"\n        return self.__class__(_sign(b - a) for a, b in itertools.zip_longest(self, p[: len(self)], fillvalue=0))\n\n    def towards(self: T, p: _PosLike, distance: float = 1, limit: bool = False) -> T:\n        \"\"\"\n\n        :param p:\n        :param distance:\n        :param limit:\n        \"\"\"\n        # pyrefly: ignore\n        p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position\n        # assert self != p, f\"self is {self}, p is {p}\"\n        # TODO test and fix this if statement\n        if self == p2:\n            return self\n        # end of test\n        d = self.distance_to_point2(p2)\n        if limit:\n            distance = min(d, distance)\n        return self.__class__(\n            a + (b - a) / d * distance for a, b in itertools.zip_longest(self, p2[: len(self)], fillvalue=0)\n        )\n\n    def __eq__(self, other: Any) -> bool:\n        try:\n            return all(abs(a - b) <= EPSILON for a, b in itertools.zip_longest(self, other, fillvalue=0))\n        except TypeError:\n            return False\n\n    def __hash__(self) -> int:\n        return hash(tuple(self))\n\n\nclass Point2(Pointlike):\n    T = TypeVar(\"T\", bound=\"Point2\")\n\n    @classmethod\n    def from_proto(\n        cls, data: common_pb.Point | common_pb.Point2D | common_pb.Size2DI | common_pb.PointI | Point2 | Point3\n    ) -> Point2:\n        \"\"\"\n        :param data:\n        \"\"\"\n        return cls((data.x, data.y))\n\n    @property\n    def as_Point2D(self) -> common_pb.Point2D:\n        return common_pb.Point2D(x=self.x, y=self.y)\n\n    @property\n    def as_PointI(self) -> common_pb.PointI:\n        \"\"\"Represents points on the minimap. Values must be between 0 and 64.\"\"\"\n        return common_pb.PointI(x=int(self[0]), y=int(self[1]))\n\n    @property\n    def rounded(self) -> Point2:\n        return Point2((math.floor(self[0]), math.floor(self[1])))\n\n    @property\n    def length(self) -> float:\n        \"\"\"This property exists in case Point2 is used as a vector.\"\"\"\n        return math.hypot(self[0], self[1])\n\n    @property\n    def normalized(self: Point2 | Point3) -> Point2:\n        \"\"\"This property exists in case Point2 is used as a vector.\"\"\"\n        length = self.length\n        # Cannot normalize if length is zero\n        assert length\n        return Point2((self[0] / length, self[1] / length))\n\n    @property\n    def x(self) -> float:\n        return self[0]\n\n    @property\n    def y(self) -> float:\n        return self[1]\n\n    @property\n    def to2(self) -> Point2:\n        return Point2(self[:2])\n\n    @property\n    def to3(self) -> Point3:\n        return Point3((*self, 0))\n\n    def round(self, decimals: int) -> Point2:\n        \"\"\"Rounds each number in the tuple to the amount of given decimals.\"\"\"\n        return Point2((round(self[0], decimals), round(self[1], decimals)))\n\n    def offset(self: T, p: _PointLike) -> T:\n        return self.__class__((self[0] + p[0], self[1] + p[1]))\n\n    def random_on_distance(self, distance: float | tuple[float, float] | list[float]) -> Point2:\n        if isinstance(distance, (tuple, list)):  # interval\n            dist = distance[0] + random.random() * (distance[1] - distance[0])\n        else:\n            dist = distance\n        assert dist > 0, \"Distance is not greater than 0\"\n        angle = random.random() * 2 * math.pi\n\n        dx, dy = math.cos(angle), math.sin(angle)\n        return Point2((self.x + dx * dist, self.y + dy * dist))\n\n    def towards_with_random_angle(\n        self,\n        p: Point2 | Point3,\n        distance: int | float = 1,\n        max_difference: int | float = (math.pi / 4),\n    ) -> Point2:\n        tx, ty = self.to2.towards(p.to2, 1)\n        angle = math.atan2(ty - self.y, tx - self.x)\n        angle = (angle - max_difference) + max_difference * 2 * random.random()\n        return Point2((self.x + math.cos(angle) * distance, self.y + math.sin(angle) * distance))\n\n    def circle_intersection(self, p: Point2, r: float) -> set[Point2]:\n        \"\"\"self is point1, p is point2, r is the radius for circles originating in both points\n        Used in ramp finding\n\n        :param p:\n        :param r:\"\"\"\n        assert self != p, \"self is equal to p\"\n        distance_between_points = self.distance_to(p)\n        assert r >= distance_between_points / 2\n        # remaining distance from center towards the intersection, using pythagoras\n        remaining_distance_from_center = (r**2 - (distance_between_points / 2) ** 2) ** 0.5\n        # center of both points\n        offset_to_center = Point2(((p.x - self.x) / 2, (p.y - self.y) / 2))\n        center = self.offset(offset_to_center)\n\n        # stretch offset vector in the ratio of remaining distance from center to intersection\n        vector_stretch_factor = remaining_distance_from_center / (distance_between_points / 2)\n        v = offset_to_center\n        offset_to_center_stretched = Point2((v.x * vector_stretch_factor, v.y * vector_stretch_factor))\n\n        # rotate vector by 90° and -90°\n        vector_rotated_1 = Point2((offset_to_center_stretched.y, -offset_to_center_stretched.x))\n        vector_rotated_2 = Point2((-offset_to_center_stretched.y, offset_to_center_stretched.x))\n        intersect1 = center.offset(vector_rotated_1)\n        intersect2 = center.offset(vector_rotated_2)\n        return {intersect1, intersect2}\n\n    @property\n    def neighbors4(self: T) -> set[T]:\n        return {\n            self.__class__((self[0] - 1, self[1])),\n            self.__class__((self[0] + 1, self[1])),\n            self.__class__((self[0], self[1] - 1)),\n            self.__class__((self[0], self[1] + 1)),\n        }\n\n    @property\n    def neighbors8(self: T) -> set[T]:\n        return self.neighbors4 | {\n            self.__class__((self[0] - 1, self[1] - 1)),\n            self.__class__((self[0] - 1, self[1] + 1)),\n            self.__class__((self[0] + 1, self[1] - 1)),\n            self.__class__((self[0] + 1, self[1] + 1)),\n        }\n\n    def negative_offset(self: T, other: Point2) -> T:\n        return self.__class__((self[0] - other[0], self[1] - other[1]))\n\n    def __add__(self, other: Point2) -> Point2:\n        return self.offset(other)\n\n    def __sub__(self, other: Point2) -> Point2:\n        return self.negative_offset(other)\n\n    def __neg__(self: T) -> T:\n        return self.__class__(-a for a in self)\n\n    def __abs__(self) -> float:\n        return math.hypot(self[0], self[1])\n\n    def __bool__(self) -> bool:\n        return self[0] != 0 or self[1] != 0\n\n    def __mul__(self, other: _PointLike | float) -> Point2:\n        if isinstance(other, (int, float)):\n            return Point2((self[0] * other, self[1] * other))\n        return Point2((self[0] * other[0], self[1] * other[1]))\n\n    def __rmul__(self, other: _PointLike | float) -> Point2:\n        return self.__mul__(other)\n\n    def __truediv__(self, other: float | Point2) -> Point2:\n        if isinstance(other, (int, float)):\n            return self.__class__((self[0] / other, self[1] / other))\n        return self.__class__((self[0] / other[0], self[1] / other[1]))\n\n    def is_same_as(self, other: Point2, dist: float = 0.001) -> bool:\n        return self.distance_to_point2(other) <= dist\n\n    def direction_vector(self, other: Point2) -> Point2:\n        \"\"\"Converts a vector to a direction that can face vertically, horizontally or diagonal or be zero, e.g. (0, 0), (1, -1), (1, 0)\"\"\"\n        return self.__class__((_sign(other[0] - self[0]), _sign(other[1] - self[1])))\n\n    def manhattan_distance(self, other: Point2) -> float:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return abs(other[0] - self[0]) + abs(other[1] - self[1])\n\n    @staticmethod\n    def center(points: list[Point2]) -> Point2:\n        \"\"\"Returns the central point for points in list\n\n        :param points:\"\"\"\n        s = Point2((0, 0))\n        for p in points:\n            s += p\n        return s / len(points)\n\n\nclass Point3(Point2):\n    @classmethod\n    def from_proto(cls, data: common_pb.Point | Point3) -> Point3:\n        \"\"\"\n        :param data:\n        \"\"\"\n        return cls((data.x, data.y, data.z))\n\n    @property\n    def as_Point(self) -> common_pb.Point:\n        return common_pb.Point(x=self.x, y=self.y, z=self.z)\n\n    @property\n    def rounded(self) -> Point3:\n        return Point3((math.floor(self[0]), math.floor(self[1]), math.floor(self[2])))\n\n    @property\n    def z(self) -> float:\n        return self[2]\n\n    @property\n    def to3(self) -> Point3:\n        return Point3(self)\n\n    def __add__(self, other: Point2 | Point3) -> Point3:\n        if not isinstance(other, Point3):\n            return Point3((self[0] + other[0], self[1] + other[1], self[2]))\n        return Point3((self[0] + other[0], self[1] + other[1], self[2] + other[2]))\n\n\nclass Size(Point2):\n    @classmethod\n    def from_proto(\n        cls, data: common_pb.Point | common_pb.Point2D | common_pb.Size2DI | common_pb.PointI | Point2\n    ) -> Size:\n        \"\"\"\n        :param data:\n        \"\"\"\n        return cls((data.x, data.y))\n\n    @property\n    def width(self) -> float:\n        return self[0]\n\n    @property\n    def height(self) -> float:\n        return self[1]\n\n\nclass Rect(Point2):\n    @classmethod\n    def from_proto(cls, data: common_pb.RectangleI) -> Rect:\n        \"\"\"\n        :param data:\n        \"\"\"\n        assert data.p0.x < data.p1.x and data.p0.y < data.p1.y\n        return cls((data.p0.x, data.p0.y, data.p1.x - data.p0.x, data.p1.y - data.p0.y))\n\n    @property\n    def x(self) -> float:\n        return self[0]\n\n    @property\n    def y(self) -> float:\n        return self[1]\n\n    @property\n    def width(self) -> float:\n        return self[2]\n\n    @property\n    def height(self) -> float:\n        return self[3]\n\n    @property\n    def right(self) -> float:\n        \"\"\"Returns the x-coordinate of the rectangle of its right side.\"\"\"\n        return self.x + self.width\n\n    @property\n    def top(self) -> float:\n        \"\"\"Returns the y-coordinate of the rectangle of its top side.\"\"\"\n        return self.y + self.height\n\n    @property\n    def size(self) -> Size:\n        return Size((self[2], self[3]))\n\n    @property\n    def center(self) -> Point2:\n        return Point2((self.x + self.width / 2, self.y + self.height / 2))\n\n    def offset(self, p: _PointLike) -> Rect:\n        return self.__class__((self[0] + p[0], self[1] + p[1], self[2], self[3]))\n"
  },
  {
    "path": "sc2/power_source.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\n\nfrom s2clientprotocol import raw_pb2\nfrom sc2.position import Point2\n\n\n@dataclass\nclass PowerSource:\n    position: Point2\n    radius: float\n    unit_tag: int\n\n    def __post_init__(self) -> None:\n        assert self.radius > 0\n\n    @classmethod\n    def from_proto(cls, proto: raw_pb2.PowerSource) -> PowerSource:\n        return PowerSource(Point2.from_proto(proto.pos), proto.radius, proto.tag)\n\n    def covers(self, position: Point2) -> bool:\n        return self.position.distance_to(position) <= self.radius\n\n    def __repr__(self) -> str:\n        return f\"PowerSource({self.position}, {self.radius})\"\n\n\n@dataclass\nclass PsionicMatrix:\n    sources: list[PowerSource]\n\n    @classmethod\n    def from_proto(cls, proto: list[raw_pb2.PowerSource]) -> PsionicMatrix:\n        return PsionicMatrix([PowerSource.from_proto(p) for p in proto])\n\n    def covers(self, position: Point2) -> bool:\n        return any(source.covers(position) for source in self.sources)\n"
  },
  {
    "path": "sc2/protocol.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport sys\nfrom contextlib import suppress\nfrom typing import overload\n\nfrom aiohttp.client_ws import ClientWebSocketResponse\nfrom loguru import logger\n\n# pyre-fixme[21]\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom s2clientprotocol.query_pb2 import RequestQuery\nfrom sc2.data import Status\n\n\nclass ProtocolError(Exception):\n    @property\n    def is_game_over_error(self) -> bool:\n        return self.args[0] in [\"['Game has already ended']\", \"['Not supported if game has already ended']\"]\n\n\nclass ConnectionAlreadyClosedError(ProtocolError):\n    pass\n\n\nclass Protocol:\n    def __init__(self, ws: ClientWebSocketResponse) -> None:\n        \"\"\"\n        A class for communicating with an SCII application.\n        :param ws: the websocket (type: aiohttp.ClientWebSocketResponse) used to communicate with a specific SCII app\n        \"\"\"\n        assert ws\n        self._ws: ClientWebSocketResponse = ws\n        # pyre-fixme[11]\n        self._status: Status | None = None\n\n    async def __request(self, request: sc_pb.Request) -> sc_pb.Response:\n        logger.debug(f\"Sending request: {request!r}\")\n        try:\n            await self._ws.send_bytes(request.SerializeToString())\n        except TypeError as exc:\n            logger.exception(\"Cannot send: Connection already closed.\")\n            raise ConnectionAlreadyClosedError(\"Connection already closed.\") from exc\n        logger.debug(\"Request sent\")\n\n        response = sc_pb.Response()\n        try:\n            response_bytes = await self._ws.receive_bytes()\n        except TypeError as exc:\n            if self._status == Status.ended:\n                logger.info(\"Cannot receive: Game has already ended.\")\n                raise ConnectionAlreadyClosedError(\"Game has already ended\") from exc\n            logger.error(\"Cannot receive: Connection already closed.\")\n            raise ConnectionAlreadyClosedError(\"Connection already closed.\") from exc\n        except asyncio.CancelledError:\n            # If request is sent, the response must be received before reraising cancel\n            try:\n                await self._ws.receive_bytes()\n            except asyncio.CancelledError:\n                logger.critical(\"Requests must not be cancelled multiple times\")\n                sys.exit(2)\n            raise\n\n        response.ParseFromString(response_bytes)\n        logger.debug(\"Response received\")\n        return response\n\n    @overload\n    async def _execute(self, create_game: sc_pb.RequestCreateGame) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, join_game: sc_pb.RequestJoinGame) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, restart_game: sc_pb.RequestRestartGame) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, start_replay: sc_pb.RequestStartReplay) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, leave_game: sc_pb.RequestLeaveGame) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, quick_save: sc_pb.RequestQuickSave) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, quick_load: sc_pb.RequestQuickLoad) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, quit: sc_pb.RequestQuit) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, game_info: sc_pb.RequestGameInfo) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, action: sc_pb.RequestAction) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, observation: sc_pb.RequestObservation) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, obs_action: sc_pb.RequestObserverAction) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, step: sc_pb.RequestStep) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, data: sc_pb.RequestData) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, query: RequestQuery) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, save_replay: sc_pb.RequestSaveReplay) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, map_command: sc_pb.RequestMapCommand) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, replay_info: sc_pb.RequestReplayInfo) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, available_maps: sc_pb.RequestAvailableMaps) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, save_map: sc_pb.RequestSaveMap) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, ping: sc_pb.RequestPing) -> sc_pb.Response: ...\n    @overload\n    async def _execute(self, debug: sc_pb.RequestDebug) -> sc_pb.Response: ...\n    async def _execute(self, **kwargs) -> sc_pb.Response:\n        assert len(kwargs) == 1, \"Only one request allowed by the API\"\n\n        response: sc_pb.Response = await self.__request(sc_pb.Request(**kwargs))\n\n        new_status = Status(response.status)\n        if new_status != self._status:\n            logger.info(f\"Client status changed to {new_status} (was {self._status})\")\n        self._status = new_status\n\n        if response.error:\n            logger.debug(f\"Response contained an error: {response.error}\")\n            raise ProtocolError(f\"{response.error}\")\n\n        return response\n\n    async def ping(self):\n        result = await self._execute(ping=sc_pb.RequestPing())\n        return result\n\n    async def quit(self) -> None:\n        with suppress(ConnectionAlreadyClosedError, ConnectionResetError):\n            await self._execute(quit=sc_pb.RequestQuit())\n"
  },
  {
    "path": "sc2/proxy.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport os\nimport platform\nimport subprocess\nimport sys\nimport time\nimport traceback\nfrom pathlib import Path\n\nfrom aiohttp import WSMsgType, web\nfrom aiohttp.web_ws import WebSocketResponse\nfrom loguru import logger\n\n# pyre-fixme[21]\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2.controller import Controller\nfrom sc2.data import Result, Status\nfrom sc2.player import BotProcess\n\n\nclass Proxy:\n    \"\"\"\n    Class for handling communication between sc2 and an external bot.\n    This \"middleman\" is needed for enforcing time limits, collecting results, and closing things properly.\n    \"\"\"\n\n    def __init__(\n        self,\n        controller: Controller,\n        player: BotProcess,\n        proxyport: int,\n        game_time_limit: int | None = None,\n        realtime: bool = False,\n    ) -> None:\n        self.controller = controller\n        self.player = player\n        self.port = proxyport\n        self.timeout_loop = game_time_limit * 22.4 if game_time_limit else None\n        self.realtime = realtime\n        logger.debug(\n            f\"Proxy Inited with ctrl {controller}({controller._process._port}), player {player}, proxyport {proxyport}, lim {game_time_limit}\"\n        )\n\n        self.result = None\n        self.player_id: int | None = None\n        self.done = False\n\n    async def parse_request(self, msg) -> None:\n        request = sc_pb.Request()\n        request.ParseFromString(msg.data)\n        if request.HasField(\"quit\"):\n            request = sc_pb.Request(leave_game=sc_pb.RequestLeaveGame())\n        if request.HasField(\"leave_game\"):\n            if self.controller._status == Status.in_game:\n                logger.info(f\"Proxy: player {self.player.name}({self.player_id}) surrenders\")\n                self.result = {self.player_id: Result.Defeat}\n            elif self.controller._status == Status.ended:\n                await self.get_response()\n        elif request.HasField(\"join_game\") and not request.join_game.HasField(\"player_name\"):\n            if self.player.name is not None:\n                request.join_game.player_name = self.player.name\n        await self.controller._ws.send_bytes(request.SerializeToString())\n\n    # TODO Catching too general exception Exception (broad-except)\n\n    async def get_response(self):\n        response_bytes = None\n        try:\n            response_bytes = await self.controller._ws.receive_bytes()\n        except TypeError as e:\n            logger.exception(\"Cannot receive: SC2 Connection already closed.\")\n            tb = traceback.format_exc()\n            logger.error(f\"Exception {e}: {tb}\")\n        except asyncio.CancelledError:\n            logger.info(f\"Proxy({self.player.name}), caught receive from sc2\")\n            try:\n                x = await self.controller._ws.receive_bytes()\n                if response_bytes is None:\n                    response_bytes = x\n            except (asyncio.CancelledError, asyncio.TimeoutError, Exception) as e:\n                logger.exception(f\"Exception {e}\")\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n        return response_bytes\n\n    async def parse_response(self, response_bytes):\n        response = sc_pb.Response()\n        response.ParseFromString(response_bytes)\n\n        if not response.HasField(\"status\"):\n            logger.critical(\"Proxy: RESPONSE HAS NO STATUS {response}\")\n        else:\n            new_status = Status(response.status)\n            if new_status != self.controller._status:\n                logger.info(f\"Controller({self.player.name}): {self.controller._status}->{new_status}\")\n                self.controller._status = new_status\n\n        if self.player_id is None and response.HasField(\"join_game\"):\n            self.player_id = response.join_game.player_id\n            logger.info(f\"Proxy({self.player.name}): got join_game for {self.player_id}\")\n\n        if self.result is None and response.HasField(\"observation\"):\n            obs: sc_pb.ResponseObservation = response.observation\n            if obs.player_result:\n                self.result = {pr.player_id: Result(pr.result) for pr in obs.player_result}\n            elif self.timeout_loop and obs.HasField(\"observation\") and obs.observation.game_loop > self.timeout_loop:\n                self.result = {i: Result.Tie for i in range(1, 3)}  # noqa: C420\n                logger.info(f\"Proxy({self.player.name}) timing out\")\n                act = [sc_pb.Action(action_chat=sc_pb.ActionChat(message=\"Proxy: Timing out\"))]\n                await self.controller._execute(action=sc_pb.RequestAction(actions=act))\n        return response\n\n    async def get_result(self) -> None:\n        try:\n            res = await self.controller.ping()\n            if res.status in {Status.in_game, Status.in_replay, Status.ended}:\n                res = await self.controller._execute(observation=sc_pb.RequestObservation())\n                if res.HasField(\"observation\") and res.observation.player_result:\n                    self.result = {pr.player_id: Result(pr.result) for pr in res.observation.player_result}\n\n        # TODO Catching too general exception Exception (broad-except)\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n\n    async def proxy_handler(self, request) -> WebSocketResponse:\n        bot_ws = web.WebSocketResponse(receive_timeout=30)\n        await bot_ws.prepare(request)\n        try:\n            async for msg in bot_ws:\n                if msg.data is None:\n                    raise TypeError(f\"data is None, {msg}\")\n                if msg.data and msg.type == WSMsgType.BINARY:\n                    await self.parse_request(msg)\n\n                    response_bytes = await self.get_response()\n                    if response_bytes is None:\n                        raise ConnectionError(\"Could not get response_bytes\")\n\n                    new_response = await self.parse_response(response_bytes)\n                    await bot_ws.send_bytes(new_response.SerializeToString())\n\n                elif msg.type == WSMsgType.CLOSED:\n                    logger.error(\"Client shutdown\")\n                else:\n                    logger.error(\"Incorrect message type\")\n\n        # TODO Catching too general exception Exception (broad-except)\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n            ignored_errors = {ConnectionError, asyncio.CancelledError}\n            if not any(isinstance(e, E) for E in ignored_errors):\n                tb = traceback.format_exc()\n                logger.info(f\"Proxy({self.player.name}): Caught {e} traceback: {tb}\")\n        finally:\n            try:\n                if self.controller._status in {Status.in_game, Status.in_replay}:\n                    await self.controller._execute(leave_game=sc_pb.RequestLeaveGame())\n                await bot_ws.close()\n\n            # TODO Catching too general exception Exception (broad-except)\n            except Exception as e:\n                logger.exception(f\"Caught unknown exception during surrender: {e}\")\n            self.done = True\n        return bot_ws\n\n    async def play_with_proxy(self, startport):\n        logger.info(f\"Proxy({self.port}): Starting app\")\n        app = web.Application()\n        app.router.add_route(\"GET\", \"/sc2api\", self.proxy_handler)\n        apprunner = web.AppRunner(app, access_log=None)\n        await apprunner.setup()\n        appsite = web.TCPSite(apprunner, self.controller._process._host, self.port)\n        await appsite.start()\n\n        subproc_args = {\"cwd\": str(self.player.path), \"stderr\": subprocess.STDOUT}\n        if platform.system() == \"Linux\":\n            # pyrefly: ignore\n            subproc_args[\"preexec_fn\"] = os.setpgrp\n        elif platform.system() == \"Windows\" and sys.platform == \"win32\":\n            subproc_args[\"creationflags\"] = subprocess.CREATE_NEW_PROCESS_GROUP\n\n        player_command_line = self.player.cmd_line(self.port, startport, self.controller._process._host, self.realtime)\n        logger.info(f\"Starting bot with command: {' '.join(player_command_line)}\")\n        if self.player.stdout is None:\n            bot_process = subprocess.Popen(player_command_line, stdout=subprocess.DEVNULL, **subproc_args)\n        else:\n            with Path(self.player.stdout).open(\"w+\") as out:\n                bot_process = subprocess.Popen(player_command_line, stdout=out, **subproc_args)\n\n        while self.result is None:\n            bot_alive = bot_process and bot_process.poll() is None\n            sc2_alive = self.controller.running\n            if self.done or not (bot_alive and sc2_alive):\n                logger.info(\n                    f\"Proxy({self.port}): {self.player.name} died, \"\n                    f\"bot{(not bot_alive) * ' not'} alive, sc2{(not sc2_alive) * ' not'} alive\"\n                )\n                # Maybe its still possible to retrieve a result\n                if sc2_alive and not self.done:\n                    await self.get_response()\n                logger.info(f\"Proxy({self.port}): breaking, result {self.result}\")\n                break\n            await asyncio.sleep(5)\n\n        # cleanup\n        logger.info(f\"({self.port}): cleaning up {self.player!r}\")\n        for _i in range(3):\n            if isinstance(bot_process, subprocess.Popen):\n                if bot_process.stdout and not bot_process.stdout.closed:  # should not run anymore\n                    logger.info(f\"==================output for player {self.player.name}\")\n                    for line in bot_process.stdout.readlines():\n                        logger.opt(raw=True).info(line.decode(\"utf-8\"))\n                    bot_process.stdout.close()\n                    logger.info(\"==================\")\n                bot_process.terminate()\n                bot_process.wait()\n            time.sleep(0.5)\n            if not bot_process or bot_process.poll() is not None:\n                break\n        else:\n            bot_process.terminate()\n            bot_process.wait()\n        try:\n            await apprunner.cleanup()\n\n        # TODO Catching too general exception Exception (broad-except)\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception during cleaning: {e}\")\n        if isinstance(self.result, dict):\n            self.result[None] = None\n            return self.result[self.player_id]\n        return self.result\n"
  },
  {
    "path": "sc2/py.typed",
    "content": "# Required by https://peps.python.org/pep-0561/#packaging-type-information\n"
  },
  {
    "path": "sc2/renderer.py",
    "content": "from __future__ import annotations\n\n\nimport datetime\nfrom typing import TYPE_CHECKING\n\nfrom s2clientprotocol import score_pb2 as score_pb\nfrom s2clientprotocol.sc2api_pb2 import ResponseObservation\nfrom sc2.position import Point2\n\nif TYPE_CHECKING:\n    from sc2.client import Client\n    from pyglet.image import ImageData\n    from pyglet.text import Label\n    from pyglet.window import Window\n\n\nclass Renderer:\n    def __init__(self, client: Client, map_size: tuple[float, float], minimap_size: tuple[float, float]) -> None:\n        self._client = client\n\n        self._window: Window = None  # pyrefly: ignore\n        self._map_size = map_size\n        self._map_image: ImageData = None  # pyrefly: ignore\n        self._minimap_size = minimap_size\n        self._minimap_image: ImageData = None  # pyrefly: ignore\n        self._mouse_x, self._mouse_y = None, None\n        self._text_supply: Label = None  # pyrefly: ignore\n        self._text_vespene: Label = None  # pyrefly: ignore\n        self._text_minerals: Label = None  # pyrefly: ignore\n        self._text_score: Label = None  # pyrefly: ignore\n        self._text_time: Label = None  # pyrefly: ignore\n\n    async def render(self, observation: ResponseObservation) -> None:\n        render_data = observation.observation.render_data\n\n        map_size = render_data.map.size\n        map_data = render_data.map.data\n        minimap_size = render_data.minimap.size\n        minimap_data = render_data.minimap.data\n\n        map_width, map_height = map_size.x, map_size.y\n        map_pitch = -map_width * 3\n\n        minimap_width, minimap_height = minimap_size.x, minimap_size.y\n        minimap_pitch = -minimap_width * 3\n\n        if not self._window:\n            from pyglet.image import ImageData\n            from pyglet.text import Label\n            from pyglet.window import Window\n\n            self._window = Window(width=map_width, height=map_height)\n            # pyrefly: ignore\n            self._window.on_mouse_press = self._on_mouse_press\n            # pyrefly: ignore\n            self._window.on_mouse_release = self._on_mouse_release\n            # pyrefly: ignore\n            self._window.on_mouse_drag = self._on_mouse_drag\n            self._map_image = ImageData(map_width, map_height, \"RGB\", map_data, map_pitch)\n            self._minimap_image = ImageData(minimap_width, minimap_height, \"RGB\", minimap_data, minimap_pitch)\n            self._text_supply = Label(\n                \"\",\n                font_name=\"Arial\",\n                font_size=16,\n                anchor_x=\"right\",\n                anchor_y=\"top\",\n                x=self._map_size[0] - 10,\n                y=self._map_size[1] - 10,\n                color=(200, 200, 200, 255),\n            )\n            self._text_vespene = Label(\n                \"\",\n                font_name=\"Arial\",\n                font_size=16,\n                anchor_x=\"right\",\n                anchor_y=\"top\",\n                x=self._map_size[0] - 130,\n                y=self._map_size[1] - 10,\n                color=(28, 160, 16, 255),\n            )\n            self._text_minerals = Label(\n                \"\",\n                font_name=\"Arial\",\n                font_size=16,\n                anchor_x=\"right\",\n                anchor_y=\"top\",\n                x=self._map_size[0] - 200,\n                y=self._map_size[1] - 10,\n                color=(68, 140, 255, 255),\n            )\n            self._text_score = Label(\n                \"\",\n                font_name=\"Arial\",\n                font_size=16,\n                anchor_x=\"left\",\n                anchor_y=\"top\",\n                x=10,\n                y=self._map_size[1] - 10,\n                color=(219, 30, 30, 255),\n            )\n            self._text_time = Label(\n                \"\",\n                font_name=\"Arial\",\n                font_size=16,\n                anchor_x=\"right\",\n                anchor_y=\"bottom\",\n                x=self._minimap_size[0] - 10,\n                y=self._minimap_size[1] + 10,\n                color=(255, 255, 255, 255),\n            )\n        else:\n            self._map_image.set_data(\"RGB\", map_pitch, map_data)\n            self._minimap_image.set_data(\"RGB\", minimap_pitch, minimap_data)\n            self._text_time.text = str(datetime.timedelta(seconds=(observation.observation.game_loop * 0.725) // 16))\n            if observation.observation.HasField(\"player_common\"):\n                self._text_supply.text = f\"{observation.observation.player_common.food_used} / {observation.observation.player_common.food_cap}\"\n                self._text_vespene.text = str(observation.observation.player_common.vespene)\n                self._text_minerals.text = str(observation.observation.player_common.minerals)\n            if observation.observation.HasField(\"score\"):\n                # pyrefly: ignore\n                self._text_score.text = f\"{score_pb._SCORE_SCORETYPE.values_by_number[observation.observation.score.score_type].name} score: {observation.observation.score.score}\"\n\n        await self._update_window()\n\n        if self._client.in_game and (not observation.player_result) and self._mouse_x and self._mouse_y:\n            await self._client.move_camera_spatial(Point2((self._mouse_x, self._minimap_size[0] - self._mouse_y)))\n            self._mouse_x, self._mouse_y = None, None\n\n    async def _update_window(self) -> None:\n        self._window.switch_to()\n        self._window.dispatch_events()\n\n        self._window.clear()\n\n        self._map_image.blit(0, 0)\n        self._minimap_image.blit(0, 0)\n        self._text_time.draw()\n        self._text_score.draw()\n        self._text_minerals.draw()\n        self._text_vespene.draw()\n        self._text_supply.draw()\n\n        self._window.flip()\n\n    def _on_mouse_press(self, x, y, button, _modifiers) -> None:\n        if button != 1:  # 1: mouse.LEFT\n            return\n        if x > self._minimap_size[0] or y > self._minimap_size[1]:\n            return\n        self._mouse_x, self._mouse_y = x, y\n\n    def _on_mouse_release(self, x, y, button, _modifiers) -> None:\n        if button != 1:  # 1: mouse.LEFT\n            return\n        if x > self._minimap_size[0] or y > self._minimap_size[1]:\n            return\n        self._mouse_x, self._mouse_y = x, y\n\n    def _on_mouse_drag(self, x, y, _dx, _dy, buttons, _modifiers) -> None:\n        if not buttons & 1:  # 1: mouse.LEFT\n            return\n        if x > self._minimap_size[0] or y > self._minimap_size[1]:\n            return\n        self._mouse_x, self._mouse_y = x, y\n"
  },
  {
    "path": "sc2/sc2process.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport os\nimport shutil\nimport signal\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom typing import Any\n\nimport aiohttp\nimport portpicker\nfrom aiohttp.client_ws import ClientWebSocketResponse\nfrom loguru import logger\n\nfrom sc2 import paths, wsl\nfrom sc2.controller import Controller\nfrom sc2.paths import Paths\nfrom sc2.versions import VERSIONS\n\n\nclass KillSwitch:\n    _to_kill: list[Any] = []\n\n    @classmethod\n    def add(cls, value) -> None:\n        logger.debug(\"kill_switch: Add switch\")\n        cls._to_kill.append(value)\n\n    @classmethod\n    def kill_all(cls) -> None:\n        logger.info(f\"kill_switch: Process cleanup for {len(cls._to_kill)} processes\")\n        for p in cls._to_kill:\n            p._clean(verbose=False)\n\n\nclass SC2Process:\n    \"\"\"\n    A class for handling SCII applications.\n\n    :param host: hostname for the url the SCII application will listen to\n    :param port: the websocket port the SCII application will listen to\n    :param fullscreen: whether to launch the SCII application in fullscreen or not, defaults to False\n    :param resolution: (window width, window height) in pixels, defaults to (1024, 768)\n    :param placement: (x, y) the distances of the SCII app's top left corner from the top left corner of the screen\n                       e.g. (20, 30) is 20 to the right of the screen's left border, and 30 below the top border\n    :param render:\n    :param sc2_version:\n    :param base_build:\n    :param data_hash:\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str | None = None,\n        port: int | None = None,\n        fullscreen: bool = False,\n        resolution: list[int] | tuple[int, int] | None = None,\n        placement: list[int] | tuple[int, int] | None = None,\n        render: bool = False,\n        sc2_version: str | None = None,\n        base_build: str | None = None,\n        data_hash: str | None = None,\n    ) -> None:\n        assert isinstance(host, str) or host is None\n        assert isinstance(port, int) or port is None\n\n        self._render = render\n        self._arguments: dict[str, str] = {\"-displayMode\": str(int(fullscreen))}\n        if not fullscreen:\n            if resolution and len(resolution) == 2:\n                self._arguments[\"-windowwidth\"] = str(resolution[0])\n                self._arguments[\"-windowheight\"] = str(resolution[1])\n            if placement and len(placement) == 2:\n                self._arguments[\"-windowx\"] = str(placement[0])\n                self._arguments[\"-windowy\"] = str(placement[1])\n\n        self._host = host or os.environ.get(\"SC2CLIENTHOST\", \"127.0.0.1\")\n        self._serverhost = os.environ.get(\"SC2SERVERHOST\", self._host)\n\n        if port is None:\n            self._port = portpicker.pick_unused_port()\n        else:\n            self._port = port\n        self._used_portpicker = bool(port is None)\n        self._tmp_dir = tempfile.mkdtemp(prefix=\"SC2_\")\n        self._process: subprocess.Popen | None = None\n        self._session = None\n        self._ws = None\n        self._sc2_version = sc2_version\n        self._base_build = base_build\n        self._data_hash = data_hash\n\n    async def __aenter__(self) -> Controller:\n        KillSwitch.add(self)\n\n        def signal_handler(*_args):\n            # unused arguments: signal handling library expects all signal\n            # callback handlers to accept two positional arguments\n            KillSwitch.kill_all()\n\n        signal.signal(signal.SIGINT, signal_handler)\n\n        try:\n            self._process = self._launch()\n            self._ws = await self._connect()\n        except:\n            await self._close_connection()\n            self._clean()\n            raise\n\n        return Controller(self._ws, self)\n\n    async def __aexit__(self, *args) -> None:\n        await self._close_connection()\n        KillSwitch.kill_all()\n        signal.signal(signal.SIGINT, signal.SIG_DFL)\n\n    @property\n    def ws_url(self) -> str:\n        return f\"ws://{self._host}:{self._port}/sc2api\"\n\n    @property\n    def versions(self):\n        \"\"\"Opens the versions.json file which origins from\n        https://github.com/Blizzard/s2client-proto/blob/master/buildinfo/versions.json\"\"\"\n        return VERSIONS\n\n    def find_data_hash(self, target_sc2_version: str) -> str | None:\n        \"\"\"Returns the data hash from the matching version string.\"\"\"\n        for version in self.versions:\n            if version[\"label\"] == target_sc2_version:\n                # pyrefly: ignore\n                return version[\"data-hash\"]\n        return None\n\n    def find_base_dir(self, target_sc2_version: str) -> str | None:\n        \"\"\"Returns the base directory from the matching version string.\"\"\"\n        for version in self.versions:\n            if version[\"label\"] == target_sc2_version:\n                return \"Base\" + str(version[\"base-version\"])\n        return None\n\n    def _launch(self):\n        if self._sc2_version and not self._base_build:\n            self._base_build = self.find_base_dir(self._sc2_version)\n\n        if self._base_build:\n            executable = str(paths.latest_executeble(Paths.BASE / \"Versions\", self._base_build))\n        else:\n            executable = str(Paths.EXECUTABLE)\n\n        if self._port == -1:\n            self._port = portpicker.pick_unused_port()\n            self._used_portpicker = True\n        args = paths.get_runner_args(Paths.CWD) + [\n            executable,\n            \"-listen\",\n            self._serverhost,\n            \"-port\",\n            str(self._port),\n            \"-dataDir\",\n            str(Paths.BASE),\n            \"-tempDir\",\n            self._tmp_dir,\n        ]\n        for arg, value in self._arguments.items():\n            args.append(arg)\n            args.append(value)\n        if self._sc2_version:\n\n            def special_match(strg: str):\n                \"\"\"Tests if the specified version is in the versions.py dict.\"\"\"\n                return any(version[\"label\"] == strg for version in self.versions)\n\n            valid_version_string = special_match(self._sc2_version)\n            if valid_version_string:\n                self._data_hash = self.find_data_hash(self._sc2_version)\n                assert self._data_hash is not None, (\n                    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.\"\n                )\n\n            else:\n                logger.warning(\n                    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.'\n                )\n\n        if self._data_hash:\n            args.extend([\"-dataVersion\", self._data_hash])\n\n        if self._render:\n            args.extend([\"-eglpath\", \"libEGL.so\"])\n\n        # if logger.getEffectiveLevel() <= logging.DEBUG:\n        args.append(\"-verbose\")\n\n        sc2_cwd = str(Paths.CWD) if Paths.CWD else None\n\n        if paths.PF in {\"WSL1\", \"WSL2\"}:\n            return wsl.run(args, sc2_cwd)\n\n        return subprocess.Popen(\n            args,\n            cwd=sc2_cwd,\n            # Suppress Wine error messages\n            stderr=subprocess.DEVNULL,\n            # , env=run_config.env\n        )\n\n    async def _connect(self) -> ClientWebSocketResponse:\n        # How long it waits for SC2 to start (in seconds)\n        for i in range(180):\n            if self._process is None:\n                # The ._clean() was called, clearing the process\n                logger.debug(\"Process cleanup complete, exit\")\n                sys.exit()\n\n            await asyncio.sleep(1)\n            try:\n                self._session = aiohttp.ClientSession()\n                # pyrefly: ignore\n                ws = await self._session.ws_connect(self.ws_url, timeout=120)\n                # FIXME fix deprecation warning in for future aiohttp version\n                # ws = await self._session.ws_connect(\n                #     self.ws_url, timeout=aiohttp.client_ws.ClientWSTimeout(ws_close=120)\n                # )\n                logger.debug(\"Websocket connection ready\")\n                return ws\n            except aiohttp.ClientConnectorError:\n                if self._session is not None:\n                    await self._session.close()\n                if i > 15:\n                    logger.debug(\"Connection refused (startup not complete (yet))\")\n\n        logger.debug(\"Websocket connection to SC2 process timed out\")\n        raise TimeoutError(\"Websocket\")\n\n    async def _close_connection(self) -> None:\n        logger.info(f\"Closing connection at {self._port}...\")\n\n        if self._ws is not None:\n            await self._ws.close()\n\n        if self._session is not None:\n            await self._session.close()\n\n    def _clean(self, verbose: bool = True) -> None:\n        if verbose:\n            logger.info(\"Cleaning up...\")\n\n        if self._process is not None:\n            assert isinstance(self._process, subprocess.Popen)\n            if paths.PF in {\"WSL1\", \"WSL2\"}:\n                if wsl.kill(self._process):\n                    logger.error(\"KILLED\")\n            elif self._process.poll() is None:\n                for _ in range(3):\n                    self._process.terminate()\n                    time.sleep(0.5)\n                    if not self._process or self._process.poll() is not None:\n                        break\n            else:\n                self._process.kill()\n                self._process.wait()\n                logger.error(\"KILLED\")\n            # Try to kill wineserver on linux\n            # pyrefly: ignore\n            if paths.PF in {\"Linux\", \"WineLinux\"}:\n                # Command wineserver not detected\n                with suppress(FileNotFoundError), subprocess.Popen([\"wineserver\", \"-k\"]) as p:\n                    p.wait()\n\n        if Path(self._tmp_dir).exists():\n            shutil.rmtree(self._tmp_dir)\n\n        self._process = None\n        self._ws = None\n        if self._used_portpicker and self._port is not None:\n            portpicker.return_port(self._port)\n            self._port = -1\n        if verbose:\n            logger.info(\"Cleanup complete\")\n"
  },
  {
    "path": "sc2/score.py",
    "content": "from __future__ import annotations\n\nfrom s2clientprotocol import score_pb2\n\n\nclass ScoreDetails:\n    \"\"\"Accessable in self.state.score during step function\n    For more information, see https://github.com/Blizzard/s2client-proto/blob/master/s2clientprotocol/score.proto\n    \"\"\"\n\n    def __init__(self, proto: score_pb2.Score) -> None:\n        self._data = proto\n        self._proto = proto.score_details\n\n    @property\n    def summary(self) -> list[list[float]]:\n        \"\"\"\n        TODO this is super ugly, how can we improve this summary?\n        Print summary to file with:\n        In on_step:\n\n        with open(\"stats.txt\", \"w+\") as file:\n            for stat in self.state.score.summary:\n                file.write(f\"{stat[0]:<35} {float(stat[1]):>35.3f}\\n\")\n        \"\"\"\n        values = [\n            \"score_type\",\n            \"score\",\n            \"idle_production_time\",\n            \"idle_worker_time\",\n            \"total_value_units\",\n            \"total_value_structures\",\n            \"killed_value_units\",\n            \"killed_value_structures\",\n            \"collected_minerals\",\n            \"collected_vespene\",\n            \"collection_rate_minerals\",\n            \"collection_rate_vespene\",\n            \"spent_minerals\",\n            \"spent_vespene\",\n            \"food_used_none\",\n            \"food_used_army\",\n            \"food_used_economy\",\n            \"food_used_technology\",\n            \"food_used_upgrade\",\n            \"killed_minerals_none\",\n            \"killed_minerals_army\",\n            \"killed_minerals_economy\",\n            \"killed_minerals_technology\",\n            \"killed_minerals_upgrade\",\n            \"killed_vespene_none\",\n            \"killed_vespene_army\",\n            \"killed_vespene_economy\",\n            \"killed_vespene_technology\",\n            \"killed_vespene_upgrade\",\n            \"lost_minerals_none\",\n            \"lost_minerals_army\",\n            \"lost_minerals_economy\",\n            \"lost_minerals_technology\",\n            \"lost_minerals_upgrade\",\n            \"lost_vespene_none\",\n            \"lost_vespene_army\",\n            \"lost_vespene_economy\",\n            \"lost_vespene_technology\",\n            \"lost_vespene_upgrade\",\n            \"friendly_fire_minerals_none\",\n            \"friendly_fire_minerals_army\",\n            \"friendly_fire_minerals_economy\",\n            \"friendly_fire_minerals_technology\",\n            \"friendly_fire_minerals_upgrade\",\n            \"friendly_fire_vespene_none\",\n            \"friendly_fire_vespene_army\",\n            \"friendly_fire_vespene_economy\",\n            \"friendly_fire_vespene_technology\",\n            \"friendly_fire_vespene_upgrade\",\n            \"used_minerals_none\",\n            \"used_minerals_army\",\n            \"used_minerals_economy\",\n            \"used_minerals_technology\",\n            \"used_minerals_upgrade\",\n            \"used_vespene_none\",\n            \"used_vespene_army\",\n            \"used_vespene_economy\",\n            \"used_vespene_technology\",\n            \"used_vespene_upgrade\",\n            \"total_used_minerals_none\",\n            \"total_used_minerals_army\",\n            \"total_used_minerals_economy\",\n            \"total_used_minerals_technology\",\n            \"total_used_minerals_upgrade\",\n            \"total_used_vespene_none\",\n            \"total_used_vespene_army\",\n            \"total_used_vespene_economy\",\n            \"total_used_vespene_technology\",\n            \"total_used_vespene_upgrade\",\n            \"total_damage_dealt_life\",\n            \"total_damage_dealt_shields\",\n            \"total_damage_dealt_energy\",\n            \"total_damage_taken_life\",\n            \"total_damage_taken_shields\",\n            \"total_damage_taken_energy\",\n            \"total_healed_life\",\n            \"total_healed_shields\",\n            \"total_healed_energy\",\n            \"current_apm\",\n            \"current_effective_apm\",\n        ]\n        # pyrefly: ignore\n        return [[value, getattr(self, value)] for value in values]\n\n    @property\n    def score_type(self):\n        return self._data.score_type\n\n    @property\n    def score(self):\n        return self._data.score\n\n    @property\n    def idle_production_time(self):\n        return self._proto.idle_production_time\n\n    @property\n    def idle_worker_time(self):\n        return self._proto.idle_worker_time\n\n    @property\n    def total_value_units(self):\n        return self._proto.total_value_units\n\n    @property\n    def total_value_structures(self):\n        return self._proto.total_value_structures\n\n    @property\n    def killed_value_units(self):\n        return self._proto.killed_value_units\n\n    @property\n    def killed_value_structures(self):\n        return self._proto.killed_value_structures\n\n    @property\n    def collected_minerals(self):\n        return self._proto.collected_minerals\n\n    @property\n    def collected_vespene(self):\n        return self._proto.collected_vespene\n\n    @property\n    def collection_rate_minerals(self):\n        return self._proto.collection_rate_minerals\n\n    @property\n    def collection_rate_vespene(self):\n        return self._proto.collection_rate_vespene\n\n    @property\n    def spent_minerals(self):\n        return self._proto.spent_minerals\n\n    @property\n    def spent_vespene(self):\n        return self._proto.spent_vespene\n\n    @property\n    def food_used_none(self):\n        return self._proto.food_used.none\n\n    @property\n    def food_used_army(self):\n        return self._proto.food_used.army\n\n    @property\n    def food_used_economy(self):\n        return self._proto.food_used.economy\n\n    @property\n    def food_used_technology(self):\n        return self._proto.food_used.technology\n\n    @property\n    def food_used_upgrade(self):\n        return self._proto.food_used.upgrade\n\n    @property\n    def killed_minerals_none(self):\n        return self._proto.killed_minerals.none\n\n    @property\n    def killed_minerals_army(self):\n        return self._proto.killed_minerals.army\n\n    @property\n    def killed_minerals_economy(self):\n        return self._proto.killed_minerals.economy\n\n    @property\n    def killed_minerals_technology(self):\n        return self._proto.killed_minerals.technology\n\n    @property\n    def killed_minerals_upgrade(self):\n        return self._proto.killed_minerals.upgrade\n\n    @property\n    def killed_vespene_none(self):\n        return self._proto.killed_vespene.none\n\n    @property\n    def killed_vespene_army(self):\n        return self._proto.killed_vespene.army\n\n    @property\n    def killed_vespene_economy(self):\n        return self._proto.killed_vespene.economy\n\n    @property\n    def killed_vespene_technology(self):\n        return self._proto.killed_vespene.technology\n\n    @property\n    def killed_vespene_upgrade(self):\n        return self._proto.killed_vespene.upgrade\n\n    @property\n    def lost_minerals_none(self):\n        return self._proto.lost_minerals.none\n\n    @property\n    def lost_minerals_army(self):\n        return self._proto.lost_minerals.army\n\n    @property\n    def lost_minerals_economy(self):\n        return self._proto.lost_minerals.economy\n\n    @property\n    def lost_minerals_technology(self):\n        return self._proto.lost_minerals.technology\n\n    @property\n    def lost_minerals_upgrade(self):\n        return self._proto.lost_minerals.upgrade\n\n    @property\n    def lost_vespene_none(self):\n        return self._proto.lost_vespene.none\n\n    @property\n    def lost_vespene_army(self):\n        return self._proto.lost_vespene.army\n\n    @property\n    def lost_vespene_economy(self):\n        return self._proto.lost_vespene.economy\n\n    @property\n    def lost_vespene_technology(self):\n        return self._proto.lost_vespene.technology\n\n    @property\n    def lost_vespene_upgrade(self):\n        return self._proto.lost_vespene.upgrade\n\n    @property\n    def friendly_fire_minerals_none(self):\n        return self._proto.friendly_fire_minerals.none\n\n    @property\n    def friendly_fire_minerals_army(self):\n        return self._proto.friendly_fire_minerals.army\n\n    @property\n    def friendly_fire_minerals_economy(self):\n        return self._proto.friendly_fire_minerals.economy\n\n    @property\n    def friendly_fire_minerals_technology(self):\n        return self._proto.friendly_fire_minerals.technology\n\n    @property\n    def friendly_fire_minerals_upgrade(self):\n        return self._proto.friendly_fire_minerals.upgrade\n\n    @property\n    def friendly_fire_vespene_none(self):\n        return self._proto.friendly_fire_vespene.none\n\n    @property\n    def friendly_fire_vespene_army(self):\n        return self._proto.friendly_fire_vespene.army\n\n    @property\n    def friendly_fire_vespene_economy(self):\n        return self._proto.friendly_fire_vespene.economy\n\n    @property\n    def friendly_fire_vespene_technology(self):\n        return self._proto.friendly_fire_vespene.technology\n\n    @property\n    def friendly_fire_vespene_upgrade(self):\n        return self._proto.friendly_fire_vespene.upgrade\n\n    @property\n    def used_minerals_none(self):\n        return self._proto.used_minerals.none\n\n    @property\n    def used_minerals_army(self):\n        return self._proto.used_minerals.army\n\n    @property\n    def used_minerals_economy(self):\n        return self._proto.used_minerals.economy\n\n    @property\n    def used_minerals_technology(self):\n        return self._proto.used_minerals.technology\n\n    @property\n    def used_minerals_upgrade(self):\n        return self._proto.used_minerals.upgrade\n\n    @property\n    def used_vespene_none(self):\n        return self._proto.used_vespene.none\n\n    @property\n    def used_vespene_army(self):\n        return self._proto.used_vespene.army\n\n    @property\n    def used_vespene_economy(self):\n        return self._proto.used_vespene.economy\n\n    @property\n    def used_vespene_technology(self):\n        return self._proto.used_vespene.technology\n\n    @property\n    def used_vespene_upgrade(self):\n        return self._proto.used_vespene.upgrade\n\n    @property\n    def total_used_minerals_none(self):\n        return self._proto.total_used_minerals.none\n\n    @property\n    def total_used_minerals_army(self):\n        return self._proto.total_used_minerals.army\n\n    @property\n    def total_used_minerals_economy(self):\n        return self._proto.total_used_minerals.economy\n\n    @property\n    def total_used_minerals_technology(self):\n        return self._proto.total_used_minerals.technology\n\n    @property\n    def total_used_minerals_upgrade(self):\n        return self._proto.total_used_minerals.upgrade\n\n    @property\n    def total_used_vespene_none(self):\n        return self._proto.total_used_vespene.none\n\n    @property\n    def total_used_vespene_army(self):\n        return self._proto.total_used_vespene.army\n\n    @property\n    def total_used_vespene_economy(self):\n        return self._proto.total_used_vespene.economy\n\n    @property\n    def total_used_vespene_technology(self):\n        return self._proto.total_used_vespene.technology\n\n    @property\n    def total_used_vespene_upgrade(self):\n        return self._proto.total_used_vespene.upgrade\n\n    @property\n    def total_damage_dealt_life(self):\n        return self._proto.total_damage_dealt.life\n\n    @property\n    def total_damage_dealt_shields(self):\n        return self._proto.total_damage_dealt.shields\n\n    @property\n    def total_damage_dealt_energy(self):\n        return self._proto.total_damage_dealt.energy\n\n    @property\n    def total_damage_taken_life(self):\n        return self._proto.total_damage_taken.life\n\n    @property\n    def total_damage_taken_shields(self):\n        return self._proto.total_damage_taken.shields\n\n    @property\n    def total_damage_taken_energy(self):\n        return self._proto.total_damage_taken.energy\n\n    @property\n    def total_healed_life(self):\n        return self._proto.total_healed.life\n\n    @property\n    def total_healed_shields(self):\n        return self._proto.total_healed.shields\n\n    @property\n    def total_healed_energy(self):\n        return self._proto.total_healed.energy\n\n    @property\n    def current_apm(self):\n        return self._proto.current_apm\n\n    @property\n    def current_effective_apm(self):\n        return self._proto.current_effective_apm\n"
  },
  {
    "path": "sc2/unit.py",
    "content": "from __future__ import annotations\n\nimport math\nimport warnings\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Any\n\nfrom s2clientprotocol import raw_pb2\nfrom sc2.cache import CacheDict\nfrom sc2.constants import (\n    CAN_BE_ATTACKED,\n    DAMAGE_BONUS_PER_UPGRADE,\n    IS_ARMORED,\n    IS_ATTACKING,\n    IS_BIOLOGICAL,\n    IS_CARRYING_MINERALS,\n    IS_CARRYING_RESOURCES,\n    IS_CARRYING_VESPENE,\n    IS_CLOAKED,\n    IS_COLLECTING,\n    IS_CONSTRUCTING_SCV,\n    IS_DETECTOR,\n    IS_ENEMY,\n    IS_GATHERING,\n    IS_LIGHT,\n    IS_MASSIVE,\n    IS_MECHANICAL,\n    IS_MINE,\n    IS_PATROLLING,\n    IS_PLACEHOLDER,\n    IS_PSIONIC,\n    IS_REPAIRING,\n    IS_RETURNING,\n    IS_REVEALED,\n    IS_SNAPSHOT,\n    IS_STRUCTURE,\n    IS_VISIBLE,\n    OFF_CREEP_SPEED_INCREASE_DICT,\n    OFF_CREEP_SPEED_UPGRADE_DICT,\n    SPEED_ALTERING_BUFFS,\n    SPEED_INCREASE_DICT,\n    SPEED_INCREASE_ON_CREEP_DICT,\n    SPEED_UPGRADE_DICT,\n    TARGET_AIR,\n    TARGET_BOTH,\n    TARGET_GROUND,\n    TARGET_HELPER,\n    UNIT_BATTLECRUISER,\n    UNIT_COLOSSUS,\n    UNIT_ORACLE,\n    UNIT_PHOTONCANNON,\n    transforming,\n)\nfrom sc2.data import Attribute, CloakState, Race, Target, race_gas, warpgate_abilities\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.position import HasPosition2D, Point2, Point3, _PointLike\nfrom sc2.unit_command import UnitCommand\n\nif TYPE_CHECKING:\n    from sc2.bot_ai import BotAI\n    from sc2.game_data import AbilityData, UnitTypeData\n\n\n@dataclass\nclass RallyTarget:\n    point: Point2\n    tag: int | None = None\n\n    @classmethod\n    def from_proto(cls, proto: raw_pb2.RallyTarget) -> RallyTarget:\n        return cls(\n            Point2.from_proto(proto.point),\n            proto.tag if proto.HasField(\"tag\") else None,\n        )\n\n\n@dataclass\nclass UnitOrder:\n    ability: AbilityData  # TODO: Should this be AbilityId instead?\n    target: int | Point2 | None = None\n    progress: float = 0\n\n    @classmethod\n    def from_proto(cls, proto: raw_pb2.UnitOrder, bot_object: BotAI) -> UnitOrder:\n        target: int | Point2 | None = proto.target_unit_tag\n        if proto.HasField(\"target_world_space_pos\"):\n            target = Point2.from_proto(proto.target_world_space_pos)\n        elif proto.HasField(\"target_unit_tag\"):\n            target = proto.target_unit_tag\n        return cls(\n            ability=bot_object.game_data.abilities[proto.ability_id],\n            target=target,\n            progress=proto.progress,\n        )\n\n    def __repr__(self) -> str:\n        return f\"UnitOrder({self.ability}, {self.target}, {self.progress})\"\n\n\nclass Unit(HasPosition2D):\n    class_cache = CacheDict()\n\n    def __init__(\n        self,\n        proto_data: raw_pb2.Unit,\n        bot_object: BotAI,\n        distance_calculation_index: int = -1,\n        base_build: int = -1,\n    ) -> None:\n        \"\"\"\n        :param proto_data:\n        :param bot_object:\n        :param distance_calculation_index:\n        :param base_build:\n        \"\"\"\n        self._proto = proto_data\n        self._bot_object = bot_object\n        self.game_loop: int = bot_object.state.game_loop\n        self.base_build = base_build\n        # Index used in the 2D numpy array to access the 2D distance between two units\n        self.distance_calculation_index: int = distance_calculation_index\n\n    def __repr__(self) -> str:\n        \"\"\"Returns string of this form: Unit(name='SCV', tag=4396941328).\"\"\"\n        return f\"Unit(name={self.name!r}, tag={self.tag})\"\n\n    @property\n    def type_id(self) -> UnitTypeId:\n        \"\"\"UnitTypeId found in sc2/ids/unit_typeid.\"\"\"\n        unit_type: int = self._proto.unit_type\n        return self.class_cache.retrieve_and_set(unit_type, lambda: UnitTypeId(unit_type))\n\n    @cached_property\n    def _type_data(self) -> UnitTypeData:\n        \"\"\"Provides the unit type data.\"\"\"\n        return self._bot_object.game_data.units[self._proto.unit_type]\n\n    @cached_property\n    def _creation_ability(self) -> AbilityData | None:\n        \"\"\"Provides the AbilityData of the creation ability of this unit.\"\"\"\n        return self._type_data.creation_ability\n\n    @property\n    def name(self) -> str:\n        \"\"\"Returns the name of the unit.\"\"\"\n        return self._type_data.name\n\n    @cached_property\n    def race(self) -> Race:\n        \"\"\"Returns the race of the unit\"\"\"\n        return Race(self._type_data._proto.race)\n\n    @cached_property\n    def tag(self) -> int:\n        \"\"\"Returns the unique tag of the unit.\"\"\"\n        return self._proto.tag\n\n    @property\n    def is_structure(self) -> bool:\n        \"\"\"Checks if the unit is a structure.\"\"\"\n        return IS_STRUCTURE in self._type_data._proto.attributes\n\n    @property\n    def is_light(self) -> bool:\n        \"\"\"Checks if the unit has the 'light' attribute.\"\"\"\n        return IS_LIGHT in self._type_data._proto.attributes\n\n    @property\n    def is_armored(self) -> bool:\n        \"\"\"Checks if the unit has the 'armored' attribute.\"\"\"\n        return IS_ARMORED in self._type_data._proto.attributes\n\n    @property\n    def is_biological(self) -> bool:\n        \"\"\"Checks if the unit has the 'biological' attribute.\"\"\"\n        return IS_BIOLOGICAL in self._type_data._proto.attributes\n\n    @property\n    def is_mechanical(self) -> bool:\n        \"\"\"Checks if the unit has the 'mechanical' attribute.\"\"\"\n        return IS_MECHANICAL in self._type_data._proto.attributes\n\n    @property\n    def is_massive(self) -> bool:\n        \"\"\"Checks if the unit has the 'massive' attribute.\"\"\"\n        return IS_MASSIVE in self._type_data._proto.attributes\n\n    @property\n    def is_psionic(self) -> bool:\n        \"\"\"Checks if the unit has the 'psionic' attribute.\"\"\"\n        return IS_PSIONIC in self._type_data._proto.attributes\n\n    @cached_property\n    def tech_alias(self) -> list[UnitTypeId] | None:\n        \"\"\"Building tech equality, e.g. OrbitalCommand is the same as CommandCenter\n        For Hive, this returns [UnitTypeId.Hatchery, UnitTypeId.Lair]\n        For SCV, this returns None\"\"\"\n        return self._type_data.tech_alias\n\n    @cached_property\n    def unit_alias(self) -> UnitTypeId | None:\n        \"\"\"Building type equality, e.g. FlyingOrbitalCommand is the same as OrbitalCommand\n        For flying OrbitalCommand, this returns UnitTypeId.OrbitalCommand\n        For SCV, this returns None\"\"\"\n        return self._type_data.unit_alias\n\n    @cached_property\n    def _weapons(self):\n        \"\"\"Returns the weapons of the unit.\"\"\"\n        return self._type_data._proto.weapons\n\n    @cached_property\n    def can_attack(self) -> bool:\n        \"\"\"Checks if the unit can attack at all.\"\"\"\n        # TODO BATTLECRUISER doesnt have weapons in proto?!\n        return bool(self._weapons) or self.type_id in {UNIT_BATTLECRUISER, UNIT_ORACLE}\n\n    @property\n    def can_attack_both(self) -> bool:\n        \"\"\"Checks if the unit can attack both ground and air units.\"\"\"\n        return self.can_attack_ground and self.can_attack_air\n\n    @cached_property\n    def can_attack_ground(self) -> bool:\n        \"\"\"Checks if the unit can attack ground units.\"\"\"\n        if self.type_id in {UNIT_BATTLECRUISER, UNIT_ORACLE}:\n            return True\n        if self._weapons:\n            return any(weapon.type in TARGET_GROUND for weapon in self._weapons)\n        return False\n\n    @cached_property\n    def ground_dps(self) -> float:\n        \"\"\"Returns the dps against ground units. Does not include upgrades.\"\"\"\n        if self.can_attack_ground:\n            weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_GROUND), None)\n            if weapon:\n                return (weapon.damage * weapon.attacks) / weapon.speed\n        return 0\n\n    @cached_property\n    def ground_range(self) -> float:\n        \"\"\"Returns the range against ground units. Does not include upgrades.\"\"\"\n        if self.type_id == UNIT_ORACLE:\n            return 4\n        if self.type_id == UNIT_BATTLECRUISER:\n            return 6\n        if self.can_attack_ground:\n            weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_GROUND), None)\n            if weapon:\n                return weapon.range\n        return 0\n\n    @cached_property\n    def can_attack_air(self) -> bool:\n        \"\"\"Checks if the unit can air attack at all. Does not include upgrades.\"\"\"\n        if self.type_id == UNIT_BATTLECRUISER:\n            return True\n        if self._weapons:\n            return any(weapon.type in TARGET_AIR for weapon in self._weapons)\n        return False\n\n    @cached_property\n    def air_dps(self) -> float:\n        \"\"\"Returns the dps against air units. Does not include upgrades.\"\"\"\n        if self.can_attack_air:\n            weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_AIR), None)\n            if weapon:\n                return (weapon.damage * weapon.attacks) / weapon.speed\n        return 0\n\n    @cached_property\n    def air_range(self) -> float:\n        \"\"\"Returns the range against air units. Does not include upgrades.\"\"\"\n        if self.type_id == UNIT_BATTLECRUISER:\n            return 6\n        if self.can_attack_air:\n            weapon = next((weapon for weapon in self._weapons if weapon.type in TARGET_AIR), None)\n            if weapon:\n                return weapon.range\n        return 0\n\n    @cached_property\n    def bonus_damage(self) -> tuple[float, str] | None:\n        \"\"\"Returns a tuple of form '(bonus damage, armor type)' if unit does 'bonus damage' against 'armor type'.\n        Possible armor typs are: 'Light', 'Armored', 'Biological', 'Mechanical', 'Psionic', 'Massive', 'Structure'.\"\"\"\n        # TODO: Consider units with ability attacks (Oracle, Baneling) or multiple attacks (Thor).\n        if self._weapons:\n            for weapon in self._weapons:\n                if weapon.damage_bonus:\n                    b = weapon.damage_bonus[0]\n                    return b.bonus, Attribute(b.attribute).name\n        return None\n\n    @property\n    def armor(self) -> float:\n        \"\"\"Returns the armor of the unit. Does not include upgrades\"\"\"\n        return self._type_data._proto.armor\n\n    @property\n    def sight_range(self) -> float:\n        \"\"\"Returns the sight range of the unit.\"\"\"\n        return self._type_data._proto.sight_range\n\n    @property\n    def movement_speed(self) -> float:\n        \"\"\"Returns the movement speed of the unit.\n        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.\n        Does not include upgrades or buffs.\"\"\"\n        return self._type_data._proto.movement_speed\n\n    @cached_property\n    def real_speed(self) -> float:\n        \"\"\"See 'calculate_speed'.\"\"\"\n        return self.calculate_speed()\n\n    def calculate_speed(self, upgrades: set[UpgradeId] | None = None) -> float:\n        \"\"\"Calculates the movement speed of the unit including buffs and upgrades.\n        Note: Upgrades only work with own units. Use \"upgrades\" param to set expected enemy upgrades.\n\n        :param upgrades:\n        \"\"\"\n        speed: float = self.movement_speed\n        unit_type: UnitTypeId = self.type_id\n\n        # ---- Upgrades ----\n        if upgrades is None and self.is_mine:\n            upgrades = self._bot_object.state.upgrades\n\n        if upgrades and unit_type in SPEED_UPGRADE_DICT:\n            upgrade_id: UpgradeId | None = SPEED_UPGRADE_DICT.get(unit_type, None)\n            if upgrade_id and upgrade_id in upgrades:\n                speed *= SPEED_INCREASE_DICT.get(unit_type, 1)\n\n        # ---- Creep ----\n        if unit_type in SPEED_INCREASE_ON_CREEP_DICT or unit_type in OFF_CREEP_SPEED_UPGRADE_DICT:\n            # On creep\n            x, y = self.position_tuple\n            if self._bot_object.state.creep[(int(x), int(y))]:\n                speed *= SPEED_INCREASE_ON_CREEP_DICT.get(unit_type, 1)\n\n            # Off creep upgrades\n            elif upgrades:\n                upgrade_id2: UpgradeId | None = OFF_CREEP_SPEED_UPGRADE_DICT.get(unit_type, None)\n                if upgrade_id2:\n                    speed *= OFF_CREEP_SPEED_INCREASE_DICT[unit_type]\n\n            # Ultralisk has passive ability \"Frenzied\" which makes it immune to speed altering buffs\n            if unit_type == UnitTypeId.ULTRALISK:\n                return speed\n\n        # ---- Buffs ----\n        # Hard reset movement speed: medivac boost, void ray charge\n        if self.buffs and unit_type in {UnitTypeId.MEDIVAC, UnitTypeId.VOIDRAY}:\n            if BuffId.MEDIVACSPEEDBOOST in self.buffs:\n                speed = self.movement_speed * 1.7\n            elif BuffId.VOIDRAYSWARMDAMAGEBOOST in self.buffs:\n                speed = self.movement_speed * 0.75\n\n        # Speed altering buffs, e.g. stimpack, zealot charge, concussive shell, time warp, fungal growth, inhibitor zone\n        for buff in self.buffs:\n            speed *= SPEED_ALTERING_BUFFS.get(buff, 1)\n        return speed\n\n    @property\n    def distance_per_step(self) -> float:\n        \"\"\"The distance a unit can move in one step. This does not take acceleration into account.\n        Useful for micro-retreat/pathfinding\"\"\"\n        return (self.real_speed / 22.4) * self._bot_object.client.game_step\n\n    @property\n    def distance_to_weapon_ready(self) -> float:\n        \"\"\"Distance a unit can travel before it's weapon is ready to be fired again.\"\"\"\n        return (self.real_speed / 22.4) * self.weapon_cooldown\n\n    @property\n    def is_mineral_field(self) -> bool:\n        \"\"\"Checks if the unit is a mineral field.\"\"\"\n        return self._type_data.has_minerals\n\n    @property\n    def is_vespene_geyser(self) -> bool:\n        \"\"\"Checks if the unit is a non-empty vespene geyser or gas extraction building.\"\"\"\n        return self._type_data.has_vespene\n\n    @property\n    def health(self) -> float:\n        \"\"\"Returns the health of the unit. Does not include shields.\"\"\"\n        return self._proto.health\n\n    @property\n    def health_max(self) -> float:\n        \"\"\"Returns the maximum health of the unit. Does not include shields.\"\"\"\n        return self._proto.health_max\n\n    @cached_property\n    def health_percentage(self) -> float:\n        \"\"\"Returns the percentage of health the unit has. Does not include shields.\"\"\"\n        if not self._proto.health_max:\n            return 0\n        return self._proto.health / self._proto.health_max\n\n    @property\n    def shield(self) -> float:\n        \"\"\"Returns the shield points the unit has. Returns 0 for non-protoss units.\"\"\"\n        return self._proto.shield\n\n    @property\n    def shield_max(self) -> float:\n        \"\"\"Returns the maximum shield points the unit can have. Returns 0 for non-protoss units.\"\"\"\n        return self._proto.shield_max\n\n    @cached_property\n    def shield_percentage(self) -> float:\n        \"\"\"Returns the percentage of shield points the unit has. Returns 0 for non-protoss units.\"\"\"\n        if not self._proto.shield_max:\n            return 0\n        return self._proto.shield / self._proto.shield_max\n\n    @cached_property\n    def shield_health_percentage(self) -> float:\n        \"\"\"Returns the percentage of combined shield + hp points the unit has.\n        Also takes build progress into account.\"\"\"\n        max_ = (self._proto.shield_max + self._proto.health_max) * self.build_progress\n        if max_ == 0:\n            return 0\n        return (self._proto.shield + self._proto.health) / max_\n\n    @property\n    def energy(self) -> float:\n        \"\"\"Returns the amount of energy the unit has. Returns 0 for units without energy.\"\"\"\n        return self._proto.energy\n\n    @property\n    def energy_max(self) -> float:\n        \"\"\"Returns the maximum amount of energy the unit can have. Returns 0 for units without energy.\"\"\"\n        return self._proto.energy_max\n\n    @cached_property\n    def energy_percentage(self) -> float:\n        \"\"\"Returns the percentage of amount of energy the unit has. Returns 0 for units without energy.\"\"\"\n        if not self._proto.energy_max:\n            return 0\n        return self._proto.energy / self._proto.energy_max\n\n    @property\n    def age_in_frames(self) -> int:\n        \"\"\"Returns how old the unit object data is (in game frames). This age does not reflect the unit was created / trained / morphed!\"\"\"\n        return self._bot_object.state.game_loop - self.game_loop\n\n    @property\n    def age(self) -> float:\n        \"\"\"Returns how old the unit object data is (in game seconds). This age does not reflect when the unit was created / trained / morphed!\"\"\"\n        return (self._bot_object.state.game_loop - self.game_loop) / 22.4\n\n    @property\n    def is_memory(self) -> bool:\n        \"\"\"Returns True if this Unit object is referenced from the future and is outdated.\"\"\"\n        return self.game_loop != self._bot_object.state.game_loop\n\n    @cached_property\n    def is_snapshot(self) -> bool:\n        \"\"\"Checks if the unit is only available as a snapshot for the bot.\n        Enemy buildings that have been scouted and are in the fog of war or\n        attacking enemy units on higher, not visible ground appear this way.\"\"\"\n        if self.base_build >= 82457:\n            return self._proto.display_type == IS_SNAPSHOT\n        # TODO: Fixed in version 5.0.4, remove if a new linux binary is released: https://github.com/Blizzard/s2client-proto/issues/167\n        # pyrefly: ignore\n        position: tuple[int, int] = self.position.rounded\n        return self._bot_object.state.visibility.data_numpy[position[1], position[0]] != 2\n\n    @cached_property\n    def is_visible(self) -> bool:\n        \"\"\"Checks if the unit is visible for the bot.\n        NOTE: This means the bot has vision of the position of the unit!\n        It does not give any information about the cloak status of the unit.\"\"\"\n        if self.base_build >= 82457:\n            return self._proto.display_type == IS_VISIBLE\n        # TODO: Remove when a new linux binary (5.0.4 or newer) is released\n        return self._proto.display_type == IS_VISIBLE and not self.is_snapshot\n\n    @property\n    def is_placeholder(self) -> bool:\n        \"\"\"Checks if the unit is a placerholder for the bot.\n        Raw information about placeholders:\n            display_type: Placeholder\n            alliance: Self\n            unit_type: 86\n            owner: 1\n            pos {\n              x: 29.5\n              y: 53.5\n              z: 7.98828125\n            }\n            radius: 2.75\n            is_on_screen: false\n        \"\"\"\n        return self._proto.display_type == IS_PLACEHOLDER\n\n    @property\n    def alliance(self) -> int:\n        \"\"\"Returns the team the unit belongs to.\"\"\"\n        return self._proto.alliance\n\n    @property\n    def is_mine(self) -> bool:\n        \"\"\"Checks if the unit is controlled by the bot.\"\"\"\n        return self._proto.alliance == IS_MINE\n\n    @property\n    def is_enemy(self) -> bool:\n        \"\"\"Checks if the unit is hostile.\"\"\"\n        return self._proto.alliance == IS_ENEMY\n\n    @property\n    def owner_id(self) -> int:\n        \"\"\"Returns the owner of the unit. This is a value of 1 or 2 in a two player game.\"\"\"\n        return self._proto.owner\n\n    @property\n    def position_tuple(self) -> tuple[float, float]:\n        \"\"\"Returns the 2d position of the unit as tuple without conversion to Point2.\"\"\"\n        return self._proto.pos.x, self._proto.pos.y\n\n    @cached_property\n    def position(self) -> Point2:\n        \"\"\"Returns the 2d position of the unit.\"\"\"\n        return Point2.from_proto(self._proto.pos)\n\n    @cached_property\n    def position3d(self) -> Point3:\n        \"\"\"Returns the 3d position of the unit.\"\"\"\n        return Point3.from_proto(self._proto.pos)\n\n    def distance_to(self, p: Unit | _PointLike) -> float:\n        \"\"\"Using the 2d distance between self and p.\n        To calculate the 3d distance, use unit.position3d.distance_to(p)\n\n        :param p:\n        \"\"\"\n        if isinstance(p, Unit):\n            return self._bot_object._distance_squared_unit_to_unit(self, p) ** 0.5\n        return self._bot_object.distance_math_hypot(self.position_tuple, p)\n\n    def distance_to_squared(self, p: Unit | _PointLike) -> float:\n        \"\"\"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.\n        To calculate the 3d distance, use unit.position3d.distance_to(p)\n\n        :param p:\n        \"\"\"\n        if isinstance(p, Unit):\n            return self._bot_object._distance_squared_unit_to_unit(self, p)\n        return self._bot_object.distance_math_hypot_squared(self.position_tuple, p)\n\n    def target_in_range(self, target: Unit, bonus_distance: float = 0) -> bool:\n        \"\"\"Checks if the target is in range.\n        Includes the target's radius when calculating distance to target.\n\n        :param target:\n        :param bonus_distance:\n        \"\"\"\n        # TODO: Fix this because immovable units (sieged tank, planetary fortress etc.) have a little lower range than this formula\n        if self.can_attack_ground and not target.is_flying:\n            unit_attack_range = self.ground_range\n        elif self.can_attack_air and (target.is_flying or target.type_id == UNIT_COLOSSUS):\n            unit_attack_range = self.air_range\n        else:\n            return False\n        return (\n            self._bot_object._distance_squared_unit_to_unit(self, target)\n            <= (self.radius + target.radius + unit_attack_range + bonus_distance) ** 2\n        )\n\n    def in_ability_cast_range(self, ability_id: AbilityId, target: Unit | Point2, bonus_distance: float = 0) -> bool:\n        \"\"\"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).\n\n        :param ability_id:\n        :param target:\n        :param bonus_distance:\n        \"\"\"\n        cast_range = self._bot_object.game_data.abilities[ability_id.value]._proto.cast_range\n        assert cast_range > 0, f\"Checking for an ability ({ability_id}) that has no cast range\"\n        ability_target_type = self._bot_object.game_data.abilities[ability_id.value]._proto.target\n        # For casting abilities that target other units, like transfuse, feedback, snipe, yamato\n        if ability_target_type in {Target.Unit.value, Target.PointOrUnit.value} and isinstance(target, Unit):\n            return (\n                self._bot_object._distance_squared_unit_to_unit(self, target)\n                <= (cast_range + self.radius + target.radius + bonus_distance) ** 2\n            )\n        # For casting abilities on the ground, like queen creep tumor, ravager bile, HT storm\n        if ability_target_type in {Target.Point.value, Target.PointOrUnit.value} and isinstance(target, Point2):\n            return (\n                self._bot_object._distance_pos_to_pos(self.position_tuple, target)\n                <= cast_range + self.radius + bonus_distance\n            )\n        return False\n\n    def calculate_damage_vs_target(\n        self,\n        target: Unit,\n        ignore_armor: bool = False,\n        include_overkill_damage: bool = True,\n    ) -> tuple[float, float, float]:\n        \"\"\"Returns a tuple of: [potential damage against target, attack speed, attack range]\n        Returns the properly calculated damage per full-attack against the target unit.\n        Returns (0, 0, 0) if this unit can't attack the target unit.\n\n        If 'include_overkill_damage=True' and the unit deals 10 damage, the target unit has 5 hp and 0 armor,\n        the target unit would result in -5hp, so the returning damage would be 10.\n        For 'include_overkill_damage=False' this function would return 5.\n\n        If 'ignore_armor=False' and the unit deals 10 damage, the target unit has 20 hp and 5 armor,\n        the target unit would result in 15hp, so the returning damage would be 5.\n        For 'ignore_armor=True' this function would return 10.\n\n        :param target:\n        :param ignore_armor:\n        :param include_overkill_damage:\n        \"\"\"\n        if self.type_id not in {UnitTypeId.BATTLECRUISER, UnitTypeId.BUNKER}:\n            if not self.can_attack:\n                return 0, 0, 0\n            if target.type_id != UnitTypeId.COLOSSUS:\n                if not self.can_attack_ground and not target.is_flying:\n                    return 0, 0, 0\n                if not self.can_attack_air and target.is_flying:\n                    return 0, 0, 0\n        # Structures that are not completed can't attack\n        if not self.is_ready:\n            return 0, 0, 0\n        target_has_guardian_shield: bool = False\n        if ignore_armor:\n            enemy_armor: float = 0\n            enemy_shield_armor: float = 0\n        else:\n            # TODO: enemy is under influence of anti armor missile -> reduce armor and shield armor\n            enemy_armor = target.armor + target.armor_upgrade_level\n            enemy_shield_armor = target.shield_upgrade_level\n            # Ultralisk armor upgrade, only works if target belongs to the bot calling this function\n            if (\n                target.type_id in {UnitTypeId.ULTRALISK, UnitTypeId.ULTRALISKBURROWED}\n                and target.is_mine\n                and UpgradeId.CHITINOUSPLATING in target._bot_object.state.upgrades\n            ):\n                enemy_armor += 2\n            # Guardian shield adds 2 armor\n            if BuffId.GUARDIANSHIELD in target.buffs:\n                target_has_guardian_shield = True\n            # Anti armor missile of raven\n            if BuffId.RAVENSHREDDERMISSILETINT in target.buffs:\n                enemy_armor -= 2\n                enemy_shield_armor -= 2\n\n        # Hard coded return for battlecruiser because they have no weapon in the API\n        if self.type_id == UnitTypeId.BATTLECRUISER:\n            if target_has_guardian_shield:\n                enemy_armor += 2\n                enemy_shield_armor += 2\n            weapon_damage: float = (5 if target.is_flying else 8) + self.attack_upgrade_level\n            weapon_damage = weapon_damage - enemy_shield_armor if target.shield else weapon_damage - enemy_armor\n            return weapon_damage, 0.224, 6\n\n        # Fast return for bunkers, since they don't have a weapon similar to BCs\n        if self.type_id == UnitTypeId.BUNKER and self.is_enemy:\n            if self.is_active:\n                # Expect fully loaded bunker with marines\n                return (24, 0.854, 6)\n            return (0, 0, 0)\n            # TODO if bunker belongs to us, use passengers and upgrade level to calculate damage\n\n        required_target_type: set[int] = (\n            TARGET_BOTH\n            if target.type_id == UnitTypeId.COLOSSUS\n            else TARGET_GROUND\n            if not target.is_flying\n            else TARGET_AIR\n        )\n        # Contains total damage, attack speed and attack range\n        damages: list[tuple[float, float, float]] = []\n        for weapon in self._weapons:\n            if weapon.type not in required_target_type:\n                continue\n            enemy_health: float = target.health\n            enemy_shield: float = target.shield\n            total_attacks: int = weapon.attacks\n            weapon_speed: float = weapon.speed\n            weapon_range: float = weapon.range\n            bonus_damage_per_upgrade = (\n                0\n                if not self.attack_upgrade_level\n                else DAMAGE_BONUS_PER_UPGRADE.get(self.type_id, {}).get(weapon.type, {}).get(None, 1)\n            )\n            damage_per_attack: float = weapon.damage + self.attack_upgrade_level * bonus_damage_per_upgrade\n            # Remaining damage after all damage is dealt to shield\n            remaining_damage: float = 0\n\n            # Calculate bonus damage against target\n            boni: list[float] = []\n            # TODO: hardcode hellbats when they have blueflame or attack upgrades\n            for bonus in weapon.damage_bonus:\n                # More about damage bonus https://github.com/Blizzard/s2client-proto/blob/b73eb59ac7f2c52b2ca585db4399f2d3202e102a/s2clientprotocol/data.proto#L55\n                if bonus.attribute in target._type_data._proto.attributes:\n                    bonus_damage_per_upgrade = (\n                        0\n                        if not self.attack_upgrade_level\n                        else DAMAGE_BONUS_PER_UPGRADE.get(self.type_id, {}).get(weapon.type, {}).get(bonus.attribute, 0)\n                    )\n                    # Hardcode blueflame damage bonus from hellions\n                    if (\n                        bonus.attribute == IS_LIGHT\n                        and self.type_id == UnitTypeId.HELLION\n                        and UpgradeId.HIGHCAPACITYBARRELS in self._bot_object.state.upgrades\n                    ):\n                        bonus_damage_per_upgrade += 5\n                    # TODO buffs e.g. void ray charge beam vs armored\n                    boni.append(bonus.bonus + self.attack_upgrade_level * bonus_damage_per_upgrade)\n            if boni:\n                damage_per_attack += max(boni)\n\n            # Subtract enemy unit's shield\n            if target.shield > 0:\n                # Fix for ranged units + guardian shield\n                enemy_shield_armor_temp = (\n                    enemy_shield_armor + 2 if target_has_guardian_shield and weapon_range >= 2 else enemy_shield_armor\n                )\n                # Shield-armor has to be applied\n                while total_attacks > 0 and enemy_shield > 0:\n                    # Guardian shield correction\n                    enemy_shield -= max(0.5, damage_per_attack - enemy_shield_armor_temp)\n                    total_attacks -= 1\n                if enemy_shield < 0:\n                    remaining_damage = -enemy_shield\n                    enemy_shield = 0\n\n            # TODO roach and hydra in melee range are not affected by guardian shield\n            # Fix for ranged units if enemy has guardian shield buff\n            enemy_armor_temp = enemy_armor + 2 if target_has_guardian_shield and weapon_range >= 2 else enemy_armor\n            # Subtract enemy unit's HP\n            if remaining_damage > 0:\n                enemy_health -= max(0.5, remaining_damage - enemy_armor_temp)\n            while total_attacks > 0 and (include_overkill_damage or enemy_health > 0):\n                # Guardian shield correction\n                enemy_health -= max(0.5, damage_per_attack - enemy_armor_temp)\n                total_attacks -= 1\n\n            # Calculate the final damage\n            if not include_overkill_damage:\n                enemy_health = max(0, enemy_health)\n                enemy_shield = max(0, enemy_shield)\n            total_damage_dealt = target.health + target.shield - enemy_health - enemy_shield\n            # Unit modifiers: buffs and upgrades that affect weapon speed and weapon range\n            if self.type_id in {\n                UnitTypeId.ZERGLING,\n                UnitTypeId.MARINE,\n                UnitTypeId.MARAUDER,\n                UnitTypeId.ADEPT,\n                UnitTypeId.HYDRALISK,\n                UnitTypeId.PHOENIX,\n                UnitTypeId.PLANETARYFORTRESS,\n                UnitTypeId.MISSILETURRET,\n                UnitTypeId.AUTOTURRET,\n            }:\n                upgrades: set[UpgradeId] = self._bot_object.state.upgrades\n                if (\n                    self.type_id == UnitTypeId.ZERGLING\n                    # Attack speed calculation only works for our unit\n                    and self.is_mine\n                    and UpgradeId.ZERGLINGATTACKSPEED in upgrades\n                ):\n                    # 0.696044921875 for zerglings divided through 1.4 equals (+40% attack speed bonus from the upgrade):\n                    weapon_speed /= 1.4\n                elif (\n                    # Adept ereceive 45% attack speed bonus from glaives\n                    self.type_id == UnitTypeId.ADEPT and self.is_mine and UpgradeId.ADEPTPIERCINGATTACK in upgrades\n                ):\n                    # TODO next patch: if self.type_id is adept: check if attack speed buff is active, instead of upgrade\n                    weapon_speed /= 1.45\n                elif self.type_id == UnitTypeId.MARINE and BuffId.STIMPACK in self.buffs:\n                    # Marine and marauder receive 50% attack speed bonus from stim\n                    weapon_speed /= 1.5\n                elif self.type_id == UnitTypeId.MARAUDER and BuffId.STIMPACKMARAUDER in self.buffs:\n                    weapon_speed /= 1.5\n                elif (\n                    # TODO always assume that the enemy has the range upgrade researched\n                    self.type_id == UnitTypeId.HYDRALISK and self.is_mine and UpgradeId.EVOLVEGROOVEDSPINES in upgrades\n                ):\n                    weapon_range += 1\n                elif self.type_id == UnitTypeId.PHOENIX and self.is_mine and UpgradeId.PHOENIXRANGEUPGRADE in upgrades:\n                    weapon_range += 2\n                elif (\n                    self.type_id in {UnitTypeId.PLANETARYFORTRESS, UnitTypeId.MISSILETURRET, UnitTypeId.AUTOTURRET}\n                    and self.is_mine\n                    and UpgradeId.HISECAUTOTRACKING in upgrades\n                ):\n                    weapon_range += 1\n\n            # Append it to the list of damages, e.g. both thor and queen attacks work on colossus\n            damages.append((total_damage_dealt, weapon_speed, weapon_range))\n\n        # If no attack was found, return (0, 0, 0)\n        if not damages:\n            return 0, 0, 0\n        # Returns: total potential damage, attack speed, attack range\n        return max(damages, key=lambda damage_tuple: damage_tuple[0])\n\n    def calculate_dps_vs_target(\n        self,\n        target: Unit,\n        ignore_armor: bool = False,\n        include_overkill_damage: bool = True,\n    ) -> float:\n        \"\"\"Returns the DPS against the given target.\n\n        :param target:\n        :param ignore_armor:\n        :param include_overkill_damage:\n        \"\"\"\n        calc_tuple: tuple[float, float, float] = self.calculate_damage_vs_target(\n            target, ignore_armor, include_overkill_damage\n        )\n        # TODO fix for real time? The result may have to be multiplied by 1.4 because of game_speed=normal\n        if calc_tuple[1] == 0:\n            return 0\n        return calc_tuple[0] / calc_tuple[1]\n\n    @property\n    def facing(self) -> float:\n        \"\"\"Returns direction the unit is facing as a float in range [0,2π). 0 is in direction of x axis.\"\"\"\n        return self._proto.facing\n\n    def is_facing(self, other_unit: Unit, angle_error: float = 0.05) -> bool:\n        \"\"\"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.\n\n        :param other_unit:\n        :param angle_error:\n        \"\"\"\n        # 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\n        angle = math.atan2(\n            other_unit.position_tuple[1] - self.position_tuple[1], other_unit.position_tuple[0] - self.position_tuple[0]\n        )\n        if angle < 0:\n            angle += math.pi * 2\n        angle_difference = math.fabs(angle - self.facing)\n        return angle_difference < angle_error\n\n    @property\n    def footprint_radius(self) -> float | None:\n        \"\"\"For structures only.\n        For townhalls this returns 2.5\n        For barracks, spawning pool, gateway, this returns 1.5\n        For supply depot, this returns 1\n        For sensor tower, creep tumor, this return 0.5\n\n        NOTE: This can be None if a building doesn't have a creation ability.\n        For rich vespene buildings, flying terran buildings, this returns None\"\"\"\n        return self._type_data.footprint_radius\n\n    @property\n    def radius(self) -> float:\n        \"\"\"Half of unit size. See https://liquipedia.net/starcraft2/Unit_Statistics_(Legacy_of_the_Void)\"\"\"\n        return self._proto.radius\n\n    @property\n    def build_progress(self) -> float:\n        \"\"\"Returns completion in range [0,1].\"\"\"\n        return self._proto.build_progress\n\n    @property\n    def is_ready(self) -> bool:\n        \"\"\"Checks if the unit is completed.\"\"\"\n        return self.build_progress == 1\n\n    @property\n    def cloak(self) -> CloakState:\n        \"\"\"Returns cloak state.\n        See https://github.com/Blizzard/s2client-api/blob/d9ba0a33d6ce9d233c2a4ee988360c188fbe9dbf/include/sc2api/sc2_unit.h#L95\n        \"\"\"\n        return CloakState(self._proto.cloak)\n\n    @property\n    def is_cloaked(self) -> bool:\n        \"\"\"Checks if the unit is cloaked.\"\"\"\n        return self._proto.cloak in IS_CLOAKED\n\n    @property\n    def is_revealed(self) -> bool:\n        \"\"\"Checks if the unit is revealed.\"\"\"\n        return self._proto.cloak == IS_REVEALED\n\n    @property\n    def can_be_attacked(self) -> bool:\n        \"\"\"Checks if the unit is revealed or not cloaked and therefore can be attacked.\"\"\"\n        return self._proto.cloak in CAN_BE_ATTACKED\n\n    @cached_property\n    def buffs(self) -> frozenset[BuffId]:\n        \"\"\"Returns the set of current buffs the unit has.\"\"\"\n        return frozenset(BuffId(buff_id) for buff_id in self._proto.buff_ids)\n\n    @cached_property\n    def is_carrying_minerals(self) -> bool:\n        \"\"\"Checks if a worker or MULE is carrying (gold-)minerals.\"\"\"\n        return not IS_CARRYING_MINERALS.isdisjoint(self.buffs)\n\n    @cached_property\n    def is_carrying_vespene(self) -> bool:\n        \"\"\"Checks if a worker is carrying vespene gas.\"\"\"\n        return not IS_CARRYING_VESPENE.isdisjoint(self.buffs)\n\n    @cached_property\n    def is_carrying_resource(self) -> bool:\n        \"\"\"Checks if a worker is carrying a resource.\"\"\"\n        return not IS_CARRYING_RESOURCES.isdisjoint(self.buffs)\n\n    @property\n    def detect_range(self) -> float:\n        \"\"\"Returns the detection distance of the unit.\"\"\"\n        return self._proto.detect_range\n\n    @cached_property\n    def is_detector(self) -> bool:\n        \"\"\"Checks if the unit is a detector. Has to be completed\n        in order to detect and Photoncannons also need to be powered.\"\"\"\n        return self.is_ready and (self.type_id in IS_DETECTOR or self.type_id == UNIT_PHOTONCANNON and self.is_powered)\n\n    @property\n    def radar_range(self) -> float:\n        return self._proto.radar_range\n\n    @property\n    def is_selected(self) -> bool:\n        \"\"\"Checks if the unit is currently selected.\"\"\"\n        return self._proto.is_selected\n\n    @property\n    def is_on_screen(self) -> bool:\n        \"\"\"Checks if the unit is on the screen.\"\"\"\n        return self._proto.is_on_screen\n\n    @property\n    def is_blip(self) -> bool:\n        \"\"\"Checks if the unit is detected by a sensor tower.\"\"\"\n        return self._proto.is_blip\n\n    @property\n    def is_powered(self) -> bool:\n        \"\"\"Checks if the unit is powered by a pylon or warppism.\"\"\"\n        return self._proto.is_powered\n\n    @property\n    def is_active(self) -> bool:\n        \"\"\"Checks if the unit has an order (e.g. unit is currently moving or attacking, structure is currently training or researching).\"\"\"\n        return self._proto.is_active\n\n    # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR SNAPSHOTS\n\n    @property\n    def mineral_contents(self) -> int:\n        \"\"\"Returns the amount of minerals remaining in a mineral field.\"\"\"\n        return self._proto.mineral_contents\n\n    @property\n    def vespene_contents(self) -> int:\n        \"\"\"Returns the amount of gas remaining in a geyser.\"\"\"\n        return self._proto.vespene_contents\n\n    @property\n    def has_vespene(self) -> bool:\n        \"\"\"Checks if a geyser has any gas remaining.\n        You can't build extractors on empty geysers.\"\"\"\n        return bool(self._proto.vespene_contents)\n\n    @property\n    def is_flying(self) -> bool:\n        \"\"\"Checks if the unit is flying.\"\"\"\n        return self._proto.is_flying or self.has_buff(BuffId.GRAVITONBEAM)\n\n    @property\n    def is_burrowed(self) -> bool:\n        \"\"\"Checks if the unit is burrowed.\"\"\"\n        return self._proto.is_burrowed\n\n    @property\n    def is_hallucination(self) -> bool:\n        \"\"\"Returns True if the unit is your own hallucination or detected.\"\"\"\n        return self._proto.is_hallucination\n\n    @property\n    def attack_upgrade_level(self) -> int:\n        \"\"\"Returns the upgrade level of the units attack.\n        # NOTE: Returns 0 for units without a weapon.\"\"\"\n        return self._proto.attack_upgrade_level\n\n    @property\n    def armor_upgrade_level(self) -> int:\n        \"\"\"Returns the upgrade level of the units armor.\"\"\"\n        return self._proto.armor_upgrade_level\n\n    @property\n    def shield_upgrade_level(self) -> int:\n        \"\"\"Returns the upgrade level of the units shield.\n        # NOTE: Returns 0 for units without a shield.\"\"\"\n        return self._proto.shield_upgrade_level\n\n    @property\n    def buff_duration_remain(self) -> int:\n        \"\"\"Returns the amount of remaining frames of the visible timer bar.\n        # NOTE: Returns 0 for units without a timer bar.\"\"\"\n        return self._proto.buff_duration_remain\n\n    @property\n    def buff_duration_max(self) -> int:\n        \"\"\"Returns the maximum amount of frames of the visible timer bar.\n        # NOTE: Returns 0 for units without a timer bar.\"\"\"\n        return self._proto.buff_duration_max\n\n    # PROPERTIES BELOW THIS COMMENT ARE NOT POPULATED FOR ENEMIES\n\n    @cached_property\n    def orders(self) -> list[UnitOrder]:\n        \"\"\"Returns the a list of the current orders.\"\"\"\n        # TODO: add examples on how to use unit orders\n        # pyrefly: ignore\n        return [UnitOrder.from_proto(order, self._bot_object) for order in self._proto.orders]\n\n    @cached_property\n    def order_target(self) -> int | Point2 | None:\n        \"\"\"Returns the target tag (if it is a Unit) or Point2 (if it is a Position)\n        from the first order, returns None if the unit is idle\"\"\"\n        if self.orders:\n            target = self.orders[0].target\n            if target is None or isinstance(target, int):\n                return target\n            return Point2.from_proto(target)\n        return None\n\n    @property\n    def is_idle(self) -> bool:\n        \"\"\"Checks if unit is idle.\"\"\"\n        return not self._proto.orders\n\n    def is_using_ability(self, abilities: AbilityId | set[AbilityId]) -> bool:\n        \"\"\"Check if the unit is using one of the given abilities.\n        Only works for own units.\"\"\"\n        if not self.orders:\n            return False\n        if isinstance(abilities, AbilityId):\n            abilities = {abilities}\n        return self.orders[0].ability.id in abilities\n\n    @cached_property\n    def is_moving(self) -> bool:\n        \"\"\"Checks if the unit is moving.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(AbilityId.MOVE)\n\n    @cached_property\n    def is_attacking(self) -> bool:\n        \"\"\"Checks if the unit is attacking.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_ATTACKING)\n\n    @cached_property\n    def is_patrolling(self) -> bool:\n        \"\"\"Checks if a unit is patrolling.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_PATROLLING)\n\n    @cached_property\n    def is_gathering(self) -> bool:\n        \"\"\"Checks if a unit is on its way to a mineral field or vespene geyser to mine.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_GATHERING)\n\n    @cached_property\n    def is_returning(self) -> bool:\n        \"\"\"Checks if a unit is returning from mineral field or vespene geyser to deliver resources to townhall.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_RETURNING)\n\n    @cached_property\n    def is_collecting(self) -> bool:\n        \"\"\"Checks if a unit is gathering or returning.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_COLLECTING)\n\n    @cached_property\n    def is_constructing_scv(self) -> bool:\n        \"\"\"Checks if the unit is an SCV that is currently building.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_CONSTRUCTING_SCV)\n\n    @cached_property\n    def is_transforming(self) -> bool:\n        \"\"\"Checks if the unit transforming.\n        Only works for own units.\"\"\"\n        return self.type_id in transforming and self.is_using_ability(transforming[self.type_id])\n\n    @cached_property\n    def is_repairing(self) -> bool:\n        \"\"\"Checks if the unit is an SCV or MULE that is currently repairing.\n        Only works for own units.\"\"\"\n        return self.is_using_ability(IS_REPAIRING)\n\n    @property\n    def add_on_tag(self) -> int:\n        \"\"\"Returns the tag of the addon of unit. If the unit has no addon, returns 0.\"\"\"\n        return self._proto.add_on_tag\n\n    @property\n    def has_add_on(self) -> bool:\n        \"\"\"Checks if unit has an addon attached.\"\"\"\n        return bool(self._proto.add_on_tag)\n\n    @cached_property\n    def has_techlab(self) -> bool:\n        \"\"\"Check if a structure is connected to a techlab addon. This should only ever return True for BARRACKS, FACTORY, STARPORT.\"\"\"\n        return self.add_on_tag in self._bot_object.techlab_tags\n\n    @cached_property\n    def has_reactor(self) -> bool:\n        \"\"\"Check if a structure is connected to a reactor addon. This should only ever return True for BARRACKS, FACTORY, STARPORT.\"\"\"\n        return self.add_on_tag in self._bot_object.reactor_tags\n\n    @cached_property\n    def add_on_land_position(self) -> Point2:\n        \"\"\"If this unit is an addon (techlab, reactor), returns the position\n        where a terran building (BARRACKS, FACTORY, STARPORT) has to land to connect to this addon.\n\n        Why offset (-2.5, 0.5)? See description in 'add_on_position'\n        \"\"\"\n        return self.position.offset(Point2((-2.5, 0.5)))\n\n    @cached_property\n    def add_on_position(self) -> Point2:\n        \"\"\"If this unit is a terran production building (BARRACKS, FACTORY, STARPORT),\n        this property returns the position of where the addon should be, if it should build one or has one attached.\n\n        Why offset (2.5, -0.5)?\n        A barracks is of size 3x3. The distance from the center to the edge is 1.5.\n        An addon is 2x2 and the distance from the edge to center is 1.\n        The total distance from center to center on the x-axis is 2.5.\n        The distance from center to center on the y-axis is -0.5.\n        \"\"\"\n        return self.position.offset(Point2((2.5, -0.5)))\n\n    @cached_property\n    def passengers(self) -> set[Unit]:\n        \"\"\"Returns the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism.\"\"\"\n        # pyrefly: ignore\n        return {Unit(unit, self._bot_object) for unit in self._proto.passengers}\n\n    @cached_property\n    def passengers_tags(self) -> set[int]:\n        \"\"\"Returns the tags of the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism.\"\"\"\n        return {unit.tag for unit in self._proto.passengers}\n\n    @property\n    def cargo_used(self) -> int:\n        \"\"\"Returns how much cargo space is currently used in the unit.\n        Note that some units take up more than one space.\"\"\"\n        return self._proto.cargo_space_taken\n\n    @property\n    def has_cargo(self) -> bool:\n        \"\"\"Checks if this unit has any units loaded.\"\"\"\n        return bool(self._proto.cargo_space_taken)\n\n    @property\n    def cargo_size(self) -> int:\n        \"\"\"Returns the amount of cargo space the unit needs.\"\"\"\n        return self._type_data.cargo_size\n\n    @property\n    def cargo_max(self) -> int:\n        \"\"\"How much cargo space is available at maximum.\"\"\"\n        return self._proto.cargo_space_max\n\n    @property\n    def cargo_left(self) -> int:\n        \"\"\"Returns how much cargo space is currently left in the unit.\"\"\"\n        return self._proto.cargo_space_max - self._proto.cargo_space_taken\n\n    @property\n    def assigned_harvesters(self) -> int:\n        \"\"\"Returns the number of workers currently gathering resources at a geyser or mining base.\"\"\"\n        return self._proto.assigned_harvesters\n\n    @property\n    def ideal_harvesters(self) -> int:\n        \"\"\"Returns the ideal harverster count for unit.\n        3 for gas buildings, 2*n for n mineral patches on that base.\"\"\"\n        return self._proto.ideal_harvesters\n\n    @property\n    def surplus_harvesters(self) -> int:\n        \"\"\"Returns a positive int if unit has too many harvesters mining,\n        a negative int if it has too few mining.\n        Will only works on townhalls, and gas buildings.\n        \"\"\"\n        return self._proto.assigned_harvesters - self._proto.ideal_harvesters\n\n    @property\n    def weapon_cooldown(self) -> float:\n        \"\"\"Returns the time until the unit can fire again,\n        returns -1 for units that can't attack.\n        Usage:\n        if unit.weapon_cooldown == 0:\n            unit.attack(target)\n        elif unit.weapon_cooldown < 0:\n            unit.move(closest_allied_unit_because_cant_attack)\n        else:\n            unit.move(retreatPosition)\"\"\"\n        if self.can_attack:\n            return self._proto.weapon_cooldown\n        return -1\n\n    @property\n    def weapon_ready(self) -> bool:\n        \"\"\"Checks if the weapon is ready to be fired.\"\"\"\n        return self.weapon_cooldown == 0\n\n    @property\n    def engaged_target_tag(self) -> int:\n        # TODO What does this do?\n        return self._proto.engaged_target_tag\n\n    @cached_property\n    def rally_targets(self) -> list[RallyTarget]:\n        \"\"\"Returns the queue of rallytargets of the structure.\"\"\"\n        return [RallyTarget.from_proto(rally_target) for rally_target in self._proto.rally_targets]\n\n    # Unit functions\n\n    def has_buff(self, buff: BuffId) -> bool:\n        \"\"\"Checks if unit has buff 'buff'.\n\n        :param buff:\n        \"\"\"\n        assert isinstance(buff, BuffId), f\"{buff} is no BuffId\"\n        return buff in self.buffs\n\n    def train(\n        self,\n        unit: UnitTypeId,\n        queue: bool = False,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Orders unit to train another 'unit'.\n        Usage: COMMANDCENTER.train(SCV)\n\n        :param unit:\n        :param queue:\n        :param can_afford_check:\n        \"\"\"\n        creation_ability = self._bot_object.game_data.units[unit.value].creation_ability\n        if creation_ability is None:\n            return False\n\n        return self(\n            creation_ability.id,\n            queue=queue,\n            subtract_cost=True,\n            can_afford_check=can_afford_check,\n        )\n\n    def build(\n        self,\n        unit: UnitTypeId,\n        position: Point2 | Unit | None = None,\n        queue: bool = False,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Orders unit to build another 'unit' at 'position'.\n        Usage::\n\n            SCV.build(COMMANDCENTER, position)\n            hatchery.build(UnitTypeId.LAIR)\n            # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position\n            SCV.build(REFINERY, target_vespene_geyser)\n\n        :param unit:\n        :param position:\n        :param queue:\n        :param can_afford_check:\n        \"\"\"\n        if unit in {UnitTypeId.EXTRACTOR, UnitTypeId.ASSIMILATOR, UnitTypeId.REFINERY}:\n            assert isinstance(position, Unit), (\n                \"When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir.\"\n            )\n        creation_ability = self._bot_object.game_data.units[unit.value].creation_ability\n        if creation_ability is None:\n            return False\n        return self(\n            creation_ability.id,\n            target=position,\n            queue=queue,\n            subtract_cost=True,\n            can_afford_check=can_afford_check,\n        )\n\n    def build_gas(\n        self,\n        target_geysir: Unit,\n        queue: bool = False,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Orders unit to build another 'unit' at 'position'.\n        Usage::\n\n            # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position\n            SCV.build_gas(target_vespene_geyser)\n\n        :param target_geysir:\n        :param queue:\n        :param can_afford_check:\n        \"\"\"\n        gas_structure_type_id: UnitTypeId = race_gas[self._bot_object.race]\n        assert isinstance(target_geysir, Unit), (\n            \"When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir.\"\n        )\n        creation_ability = self._bot_object.game_data.units[gas_structure_type_id.value].creation_ability\n        if creation_ability is None:\n            return False\n        return self(\n            creation_ability.id,\n            target=target_geysir,\n            queue=queue,\n            subtract_cost=True,\n            can_afford_check=can_afford_check,\n        )\n\n    def research(\n        self,\n        upgrade: UpgradeId,\n        queue: bool = False,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Orders unit to research 'upgrade'.\n        Requires UpgradeId to be passed instead of AbilityId.\n\n        :param upgrade:\n        :param queue:\n        :param can_afford_check:\n        \"\"\"\n        research_ability = self._bot_object.game_data.upgrades[upgrade.value].research_ability\n        if research_ability is None:\n            return False\n        return self(\n            research_ability.exact_id,\n            queue=queue,\n            subtract_cost=True,\n            can_afford_check=can_afford_check,\n        )\n\n    def warp_in(\n        self,\n        unit: UnitTypeId,\n        position: Point2,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Orders Warpgate to warp in 'unit' at 'position'.\n\n        :param unit:\n        :param queue:\n        :param can_afford_check:\n        \"\"\"\n        creation_ability = self._bot_object.game_data.units[unit.value].creation_ability\n        if creation_ability is None:\n            return False\n        return self(\n            warpgate_abilities[creation_ability.id],\n            target=position,\n            subtract_cost=True,\n            subtract_supply=True,\n            can_afford_check=can_afford_check,\n        )\n\n    def attack(self, target: Unit | Point2, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders unit to attack. Target can be a Unit or Point2.\n        Attacking a position will make the unit move there and attack everything on its way.\n\n        :param target:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.ATTACK, target=target, queue=queue)\n\n    def smart(self, target: Unit | Point2, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders the smart command. Equivalent to a right-click order.\n\n        :param target:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.SMART, target=target, queue=queue)\n\n    def gather(self, target: Unit, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders a unit to gather minerals or gas.\n        'Target' must be a mineral patch or a gas extraction building.\n\n        :param target:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.HARVEST_GATHER, target=target, queue=queue)\n\n    def return_resource(self, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders the unit to return resource to the nearest townhall.\n\n        :param queue:\n        \"\"\"\n        return self(AbilityId.HARVEST_RETURN, target=None, queue=queue)\n\n    def move(self, position: Unit | Point2, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders the unit to move to 'position'.\n        Target can be a Unit (to follow that unit) or Point2.\n\n        :param position:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.MOVE_MOVE, target=position, queue=queue)\n\n    def hold_position(self, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders a unit to stop moving. It will not move until it gets new orders.\n\n        :param queue:\n        \"\"\"\n        return self(AbilityId.HOLDPOSITION, queue=queue)\n\n    def stop(self, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders a unit to stop, but can start to move on its own\n        if it is attacked, enemy unit is in range or other friendly\n        units need the space.\n\n        :param queue:\n        \"\"\"\n        return self(AbilityId.STOP, queue=queue)\n\n    def patrol(self, position: Point2, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Orders a unit to patrol between position it has when the command starts and the target position.\n        Can be queued up to seven patrol points. If the last point is the same as the starting\n        point, the unit will patrol in a circle.\n\n        :param position:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.PATROL, target=position, queue=queue)\n\n    def repair(self, repair_target: Unit, queue: bool = False) -> UnitCommand | bool:\n        \"\"\"Order an SCV or MULE to repair.\n\n        :param repair_target:\n        :param queue:\n        \"\"\"\n        return self(AbilityId.EFFECT_REPAIR, target=repair_target, queue=queue)\n\n    def __hash__(self) -> int:\n        return self.tag\n\n    def __eq__(self, other: Unit | Any) -> bool:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return self.tag == getattr(other, \"tag\", -1)\n\n    def __call__(\n        self,\n        ability: AbilityId,\n        target: Point2 | Unit | None = None,\n        queue: bool = False,\n        subtract_cost: bool = False,\n        subtract_supply: bool = False,\n        can_afford_check: bool = False,\n    ) -> UnitCommand | bool:\n        \"\"\"Deprecated: Stop using self.do() - This may be removed in the future.\n\n        :param ability:\n        :param target:\n        :param queue:\n        :param subtract_cost:\n        :param subtract_supply:\n        :param can_afford_check:\n        \"\"\"\n        if self._bot_object.unit_command_uses_self_do:\n            return UnitCommand(ability, self, target=target, queue=queue)\n        expected_target: int = self._bot_object.game_data.abilities[ability.value]._proto.target\n        # 1: None, 2: Point, 3: Unit, 4: PointOrUnit, 5: PointOrNone\n        if target is None and expected_target not in {1, 5}:\n            warnings.warn(\n                f\"{self} got {ability} with no target but expected {TARGET_HELPER[expected_target]}\",\n                RuntimeWarning,\n                stacklevel=2,\n            )\n        elif isinstance(target, Point2) and expected_target not in {2, 4, 5}:\n            warnings.warn(\n                f\"{self} got {ability} with Point2 as target but expected {TARGET_HELPER[expected_target]}\",\n                RuntimeWarning,\n                stacklevel=2,\n            )\n        elif isinstance(target, Unit) and expected_target not in {3, 4}:\n            warnings.warn(\n                f\"{self} got {ability} with Unit as target but expected {TARGET_HELPER[expected_target]}\",\n                RuntimeWarning,\n                stacklevel=2,\n            )\n        return self._bot_object.do(\n            UnitCommand(ability, self, target=target, queue=queue),\n            subtract_cost=subtract_cost,\n            subtract_supply=subtract_supply,\n            can_afford_check=can_afford_check,\n        )\n"
  },
  {
    "path": "sc2/unit_command.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom sc2.constants import COMBINEABLE_ABILITIES\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.position import Point2\n\nif TYPE_CHECKING:\n    from sc2.unit import Unit\n\n\nclass UnitCommand:\n    def __init__(\n        self, ability: AbilityId, unit: Unit, target: Unit | Point2 | None = None, queue: bool = False\n    ) -> None:\n        \"\"\"\n        :param ability:\n        :param unit:\n        :param target:\n        :param queue:\n        \"\"\"\n        assert ability in AbilityId, f\"ability {ability} is not in AbilityId\"\n        assert unit.__class__.__name__ == \"Unit\", f\"unit {unit} is of type {type(unit)}\"\n        assert any(\n            [\n                target is None,\n                isinstance(target, Point2),\n                unit.__class__.__name__ == \"Unit\",\n            ]\n        ), f\"target {target} is of type {type(target)}\"\n        assert isinstance(queue, bool), f\"queue flag {queue} is of type {type(queue)}\"\n        self.ability = ability\n        self.unit = unit\n        self.target = target\n        self.queue = queue\n\n    @property\n    def combining_tuple(self) -> tuple[AbilityId, Unit | Point2 | None, bool, bool]:\n        return self.ability, self.target, self.queue, self.ability in COMBINEABLE_ABILITIES\n\n    def __repr__(self) -> str:\n        return f\"UnitCommand({self.ability}, {self.unit}, {self.target}, {self.queue})\"\n"
  },
  {
    "path": "sc2/units.py",
    "content": "from __future__ import annotations\n\nimport random\nfrom collections.abc import Callable, Generator, Iterable\nfrom itertools import chain\nfrom typing import TYPE_CHECKING, Any\n\nfrom s2clientprotocol import raw_pb2\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\n\nif TYPE_CHECKING:\n    from sc2.bot_ai import BotAI\n\n\nclass Units(list[Unit]):\n    \"\"\"A collection of Unit objects. Makes it easy to select units by selectors.\"\"\"\n\n    @classmethod\n    def from_proto(cls, units: list[raw_pb2.Unit], bot_object: BotAI) -> Units:\n        return cls((Unit(raw_unit, bot_object=bot_object) for raw_unit in units), bot_object)\n\n    def __init__(self, units: Iterable[Unit], bot_object: BotAI) -> None:\n        \"\"\"\n        :param units:\n        :param bot_object:\n        \"\"\"\n        super().__init__(units)\n        self._bot_object = bot_object\n\n    def __call__(self, unit_types: UnitTypeId | Iterable[UnitTypeId]) -> Units:\n        \"\"\"Creates a new mutable Units object from Units or list object.\n\n        :param unit_types:\n        \"\"\"\n        return self.of_type(unit_types)\n\n    def __iter__(self) -> Generator[Unit, None, None]:\n        return (item for item in super().__iter__())\n\n    def copy(self) -> Units:\n        \"\"\"Creates a new mutable Units object from Units or list object.\n\n        :param units:\n        \"\"\"\n        return Units(self, self._bot_object)\n\n    def __or__(self, other: Units) -> Units:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return Units(\n            chain(\n                iter(self),\n                (other_unit for other_unit in other if other_unit.tag not in (self_unit.tag for self_unit in self)),\n            ),\n            self._bot_object,\n        )\n\n    def __add__(self, other: Units) -> Units:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return Units(\n            chain(\n                iter(self),\n                (other_unit for other_unit in other if other_unit.tag not in (self_unit.tag for self_unit in self)),\n            ),\n            self._bot_object,\n        )\n\n    def __and__(self, other: Units) -> Units:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return Units(\n            (other_unit for other_unit in other if other_unit.tag in (self_unit.tag for self_unit in self)),\n            self._bot_object,\n        )\n\n    def __sub__(self, other: Units) -> Units:\n        \"\"\"\n        :param other:\n        \"\"\"\n        return Units(\n            (self_unit for self_unit in self if self_unit.tag not in (other_unit.tag for other_unit in other)),\n            self._bot_object,\n        )\n\n    def __hash__(self) -> int:\n        return hash(unit.tag for unit in self)\n\n    @property\n    def amount(self) -> int:\n        return len(self)\n\n    @property\n    def empty(self) -> bool:\n        return not bool(self)\n\n    @property\n    def exists(self) -> bool:\n        return bool(self)\n\n    def find_by_tag(self, tag: int) -> Unit | None:\n        \"\"\"\n        :param tag:\n        \"\"\"\n        for unit in self:\n            if unit.tag == tag:\n                return unit\n        return None\n\n    def by_tag(self, tag: int) -> Unit:\n        \"\"\"\n        :param tag:\n        \"\"\"\n        unit = self.find_by_tag(tag)\n        if unit is None:\n            raise KeyError(\"Unit not found\")\n        return unit\n\n    @property\n    def first(self) -> Unit:\n        assert self, \"Units object is empty\"\n        return self[0]\n\n    def take(self, n: int) -> Units:\n        \"\"\"\n        :param n:\n        \"\"\"\n        if n >= self.amount:\n            return self\n        return self.subgroup(self[:n])\n\n    @property\n    def random(self) -> Unit:\n        assert self, \"Units object is empty\"\n        return random.choice(self)\n\n    def random_or(self, other: Any) -> Unit:\n        return random.choice(self) if self else other\n\n    def random_group_of(self, n: int) -> Units:\n        \"\"\"Returns self if n >= self.amount.\"\"\"\n        if n < 1:\n            return Units([], self._bot_object)\n        if n >= self.amount:\n            return self\n        return self.subgroup(random.sample(self, n))\n\n    def in_attack_range_of(self, unit: Unit, bonus_distance: float = 0) -> Units:\n        \"\"\"Filters units that are in attack range of the given unit.\n        This uses the unit and target unit.radius when calculating the distance, so it should be accurate.\n        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.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                all_zerglings_my_marine_can_attack = enemy_zerglings.in_attack_range_of(my_marine)\n\n        Example::\n\n            enemy_mutalisks = self.enemy_units(UnitTypeId.MUTALISK)\n            my_marauder = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARAUDER), None)\n            if my_marauder:\n                all_mutalisks_my_marauder_can_attack = enemy_mutaliskss.in_attack_range_of(my_marauder)\n                # Is empty because mutalisk are flying and marauder cannot attack air\n\n        :param unit:\n        :param bonus_distance:\n        \"\"\"\n        return self.filter(lambda x: unit.target_in_range(x, bonus_distance=bonus_distance))\n\n    def closest_distance_to(self, position: Unit | Point2) -> float:\n        \"\"\"Returns the distance between the closest unit from this group to the target unit.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                closest_zergling_distance = enemy_zerglings.closest_distance_to(my_marine)\n            # Contains the distance between the marine and the closest zergling\n\n        :param position:\n        \"\"\"\n        assert self, \"Units object is empty\"\n        if isinstance(position, Unit):\n            return min(self._bot_object._distance_squared_unit_to_unit(unit, position) for unit in self) ** 0.5\n        return min(self._bot_object._distance_units_to_pos(self, position))\n\n    def furthest_distance_to(self, position: Unit | Point2) -> float:\n        \"\"\"Returns the distance between the furthest unit from this group to the target unit\n\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                furthest_zergling_distance = enemy_zerglings.furthest_distance_to(my_marine)\n                # Contains the distance between the marine and the furthest away zergling\n\n        :param position:\n        \"\"\"\n        assert self, \"Units object is empty\"\n        if isinstance(position, Unit):\n            return max(self._bot_object._distance_squared_unit_to_unit(unit, position) for unit in self) ** 0.5\n        return max(self._bot_object._distance_units_to_pos(self, position))\n\n    def closest_to(self, position: Unit | Point2) -> Unit:\n        \"\"\"Returns the closest unit (from this Units object) to the target unit or position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                closest_zergling = enemy_zerglings.closest_to(my_marine)\n                # Contains the zergling that is closest to the target marine\n\n        :param position:\n        \"\"\"\n        assert self, \"Units object is empty\"\n        if isinstance(position, Unit):\n            return min(\n                (unit1 for unit1 in self),\n                key=lambda unit2: self._bot_object._distance_squared_unit_to_unit(unit2, position),\n            )\n\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        return min(((unit, dist) for unit, dist in zip(self, distances)), key=lambda my_tuple: my_tuple[1])[0]\n\n    def furthest_to(self, position: Unit | Point2) -> Unit:\n        \"\"\"Returns the furhest unit (from this Units object) to the target unit or position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                furthest_zergling = enemy_zerglings.furthest_to(my_marine)\n                # Contains the zergling that is furthest away to the target marine\n\n        :param position:\n        \"\"\"\n        assert self, \"Units object is empty\"\n        if isinstance(position, Unit):\n            return max(\n                (unit1 for unit1 in self),\n                key=lambda unit2: self._bot_object._distance_squared_unit_to_unit(unit2, position),\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        return max(((unit, dist) for unit, dist in zip(self, distances)), key=lambda my_tuple: my_tuple[1])[0]\n\n    def closer_than(self, distance: float, position: Unit | Point2) -> Units:\n        \"\"\"Returns all units (from this Units object) that are closer than 'distance' away from target unit or position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                close_zerglings = enemy_zerglings.closer_than(3, my_marine)\n                # Contains all zerglings that are distance 3 or less away from the marine (does not include unit radius in calculation)\n\n        :param distance:\n        :param position:\n        \"\"\"\n        if not self:\n            return self\n        if isinstance(position, Unit):\n            distance_squared = distance**2\n            return self.subgroup(\n                unit\n                for unit in self\n                if self._bot_object._distance_squared_unit_to_unit(unit, position) < distance_squared\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        return self.subgroup(unit for unit, dist in zip(self, distances) if dist < distance)\n\n    def further_than(self, distance: float, position: Unit | Point2) -> Units:\n        \"\"\"Returns all units (from this Units object) that are further than 'distance' away from target unit or position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                far_zerglings = enemy_zerglings.further_than(3, my_marine)\n                # Contains all zerglings that are distance 3 or more away from the marine (does not include unit radius in calculation)\n\n        :param distance:\n        :param position:\n        \"\"\"\n        if not self:\n            return self\n        if isinstance(position, Unit):\n            distance_squared = distance**2\n            return self.subgroup(\n                unit\n                for unit in self\n                if distance_squared < self._bot_object._distance_squared_unit_to_unit(unit, position)\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        return self.subgroup(unit for unit, dist in zip(self, distances) if distance < dist)\n\n    def in_distance_between(\n        self, position: Unit | Point2 | tuple[float, float], distance1: float, distance2: float\n    ) -> Units:\n        \"\"\"Returns units that are further than distance1 and closer than distance2 to unit or position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                zerglings_filtered = enemy_zerglings.in_distance_between(my_marine, 3, 5)\n                # Contains all zerglings that are between distance 3 and 5 away from the marine (does not include unit radius in calculation)\n\n        :param position:\n        :param distance1:\n        :param distance2:\n        \"\"\"\n        if not self:\n            return self\n        if isinstance(position, Unit):\n            distance1_squared = distance1**2\n            distance2_squared = distance2**2\n            return self.subgroup(\n                unit\n                for unit in self\n                if distance1_squared\n                < self._bot_object._distance_squared_unit_to_unit(unit, position)\n                < distance2_squared\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        return self.subgroup(unit for unit, dist in zip(self, distances) if distance1 < dist < distance2)\n\n    def closest_n_units(self, position: Unit | Point2, n: int) -> Units:\n        \"\"\"Returns the n closest units in distance to position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                zerglings_filtered = enemy_zerglings.closest_n_units(my_marine, 5)\n                # Contains 5 zerglings that are the closest to the marine\n\n        :param position:\n        :param n:\n        \"\"\"\n        if not self:\n            return self\n        return self.subgroup(self._list_sorted_by_distance_to(position)[:n])\n\n    def furthest_n_units(self, position: Unit | Point2, n: int) -> Units:\n        \"\"\"Returns the n furhest units in distance to position.\n\n        Example::\n\n            enemy_zerglings = self.enemy_units(UnitTypeId.ZERGLING)\n            my_marine = next((unit for unit in self.units if unit.type_id == UnitTypeId.MARINE), None)\n            if my_marine:\n                zerglings_filtered = enemy_zerglings.furthest_n_units(my_marine, 5)\n                # Contains 5 zerglings that are the furthest to the marine\n\n        :param position:\n        :param n:\n        \"\"\"\n        if not self:\n            return self\n        return self.subgroup(self._list_sorted_by_distance_to(position)[-n:])\n\n    def in_distance_of_group(self, other_units: Units, distance: float) -> Units:\n        \"\"\"Returns units that are closer than distance from any unit in the other units object.\n\n        :param other_units:\n        :param distance:\n        \"\"\"\n        assert other_units, \"Other units object is empty\"\n        # Return self because there are no enemies\n        if not self:\n            return self\n        distance_squared = distance**2\n        if len(self) == 1:\n            if any(\n                self._bot_object._distance_squared_unit_to_unit(self[0], target) < distance_squared\n                for target in other_units\n            ):\n                return self\n            return self.subgroup([])\n\n        return self.subgroup(\n            self_unit\n            for self_unit in self\n            if any(\n                self._bot_object._distance_squared_unit_to_unit(self_unit, other_unit) < distance_squared\n                for other_unit in other_units\n            )\n        )\n\n    def in_closest_distance_to_group(self, other_units: Units) -> Unit:\n        \"\"\"Returns unit in shortest distance from any unit in self to any unit in group.\n\n        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'.\n\n        :param other_units:\n        \"\"\"\n        assert self, \"Units object is empty\"\n        assert other_units, \"Given units object is empty\"\n        return min(\n            self,\n            key=lambda self_unit: min(\n                self._bot_object._distance_squared_unit_to_unit(self_unit, other_unit) for other_unit in other_units\n            ),\n        )\n\n    def _list_sorted_closest_to_distance(self, position: Unit | Point2, distance: float) -> list[Unit]:\n        \"\"\"This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position))\n\n        :param position:\n        :param distance:\n        \"\"\"\n        if isinstance(position, Unit):\n            return sorted(\n                self,\n                key=lambda unit: abs(self._bot_object._distance_squared_unit_to_unit(unit, position) - distance),\n                reverse=True,\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        unit_dist_dict = {unit.tag: dist for unit, dist in zip(self, distances)}\n        return sorted(self, key=lambda unit2: abs(unit_dist_dict[unit2.tag] - distance), reverse=True)\n\n    def n_closest_to_distance(self, position: Point2, distance: float, n: int) -> Units:\n        \"\"\"Returns n units that are the closest to distance away.\n        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,\n        the units with distance [4, 5, 6] will be returned\n\n        :param position:\n        :param distance:\n        \"\"\"\n        return self.subgroup(self._list_sorted_closest_to_distance(position=position, distance=distance)[:n])\n\n    def n_furthest_to_distance(self, position: Point2, distance: float, n: int) -> Units:\n        \"\"\"Inverse of the function 'n_closest_to_distance', returns the furthest units instead\n\n        :param position:\n        :param distance:\n        \"\"\"\n        return self.subgroup(self._list_sorted_closest_to_distance(position=position, distance=distance)[-n:])\n\n    def subgroup(self, units: Iterable[Unit]) -> Units:\n        \"\"\"Creates a new mutable Units object from Units or list object.\n\n        :param units:\n        \"\"\"\n        return Units(units, self._bot_object)\n\n    def filter(self, pred: Callable[[Unit], Any]) -> Units:\n        \"\"\"Filters the current Units object and returns a new Units object.\n\n        Example::\n\n            from sc2.ids.unit_typeid import UnitTypeId\n            my_marines = self.units.filter(lambda unit: unit.type_id == UnitTypeId.MARINE)\n\n            completed_structures = self.structures.filter(lambda structure: structure.is_ready)\n\n            queens_with_energy_to_inject = self.units.filter(lambda unit: unit.type_id == UnitTypeId.QUEEN and unit.energy >= 25)\n\n            orbitals_with_energy_to_mule = self.structures.filter(lambda structure: structure.type_id == UnitTypeId.ORBITALCOMMAND and structure.energy >= 50)\n\n            my_units_that_can_shoot_up = self.units.filter(lambda unit: unit.can_attack_air)\n\n        See more unit properties in unit.py\n\n        :param pred:\n        \"\"\"\n        assert callable(pred), \"Function is not callable\"\n        return self.subgroup(filter(pred, self))\n\n    def sorted(self, key: Callable[[Unit], Any], reverse: bool = False) -> Units:\n        return self.subgroup(sorted(self, key=key, reverse=reverse))\n\n    def _list_sorted_by_distance_to(self, position: Unit | Point2, reverse: bool = False) -> list[Unit]:\n        \"\"\"This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position))\n\n        :param position:\n        :param reverse:\n        \"\"\"\n        if isinstance(position, Unit):\n            return sorted(\n                self, key=lambda unit: self._bot_object._distance_squared_unit_to_unit(unit, position), reverse=reverse\n            )\n        distances = self._bot_object._distance_units_to_pos(self, position)\n        unit_dist_dict = {unit.tag: dist for unit, dist in zip(self, distances)}\n        return sorted(self, key=lambda unit2: unit_dist_dict[unit2.tag], reverse=reverse)\n\n    def sorted_by_distance_to(self, position: Unit | Point2, reverse: bool = False) -> Units:\n        \"\"\"This function should be a bit faster than using units.sorted(key=lambda u: u.distance_to(position))\n\n        :param position:\n        :param reverse:\n        \"\"\"\n        return self.subgroup(self._list_sorted_by_distance_to(position, reverse=reverse))\n\n    def tags_in(self, other: Iterable[int]) -> Units:\n        \"\"\"Filters all units that have their tags in the 'other' set/list/dict\n\n        Example::\n\n            my_inject_queens = self.units.tags_in(self.queen_tags_assigned_to_do_injects)\n\n            # 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\n            my_inject_queens_slow = self.units(QUEEN).tags_in(self.queen_tags_assigned_to_do_injects)\n\n        :param other:\n        \"\"\"\n        return self.filter(lambda unit: unit.tag in other)\n\n    def tags_not_in(self, other: Iterable[int]) -> Units:\n        \"\"\"Filters all units that have their tags not in the 'other' set/list/dict\n\n        Example::\n\n            my_non_inject_queens = self.units.tags_not_in(self.queen_tags_assigned_to_do_injects)\n\n            # 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\n            my_non_inject_queens_slow = self.units(QUEEN).tags_not_in(self.queen_tags_assigned_to_do_injects)\n\n        :param other:\n        \"\"\"\n        return self.filter(lambda unit: unit.tag not in other)\n\n    def of_type(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units:\n        \"\"\"Filters all units that are of a specific type\n\n        Example::\n\n            # Use a set instead of lists in the argument\n            some_attack_units = self.units.of_type({ZERGLING, ROACH, HYDRALISK, BROODLORD})\n\n        :param other:\n        \"\"\"\n        if isinstance(other, UnitTypeId):\n            other = {other}\n        elif isinstance(other, list):\n            other = set(other)\n        return self.filter(lambda unit: unit.type_id in other)\n\n    def exclude_type(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units:\n        \"\"\"Filters all units that are not of a specific type\n\n        Example::\n\n            # Use a set instead of lists in the argument\n            ignore_units = self.enemy_units.exclude_type({LARVA, EGG, OVERLORD})\n\n        :param other:\n        \"\"\"\n        if isinstance(other, UnitTypeId):\n            other = {other}\n        elif isinstance(other, list):\n            other = set(other)\n        return self.filter(lambda unit: unit.type_id not in other)\n\n    def same_tech(self, other: set[UnitTypeId]) -> Units:\n        \"\"\"Returns all structures that have the same base structure.\n\n        Untested: This should return the equivalents for WarpPrism, Observer, Overseer, SupplyDepot and others\n\n        Example::\n\n            # All command centers, flying command centers, orbital commands, flying orbital commands, planetary fortress\n            terran_townhalls = self.townhalls.same_tech(UnitTypeId.COMMANDCENTER)\n\n            # All hatcheries, lairs and hives\n            zerg_townhalls = self.townhalls.same_tech({UnitTypeId.HATCHERY})\n\n            # All spires and greater spires\n            spires = self.townhalls.same_tech({UnitTypeId.SPIRE})\n            # The following returns the same\n            spires = self.townhalls.same_tech({UnitTypeId.GREATERSPIRE})\n\n            # This also works with multiple unit types\n            zerg_townhalls_and_spires = self.structures.same_tech({UnitTypeId.HATCHERY, UnitTypeId.SPIRE})\n\n        :param other:\n        \"\"\"\n        assert isinstance(other, set), (\n            \"Please use a set as this filter function is already fairly slow. For example\"\n            + \" 'self.units.same_tech({UnitTypeId.LAIR})'\"\n        )\n        tech_alias_types: set[int] = {u.value for u in other}\n        unit_data = self._bot_object.game_data.units\n        for unit_type in other:\n            for same in unit_data[unit_type.value]._proto.tech_alias:\n                tech_alias_types.add(same)\n        return self.filter(\n            lambda unit: unit._proto.unit_type in tech_alias_types\n            or any(same in tech_alias_types for same in unit._type_data._proto.tech_alias)\n        )\n\n    def same_unit(self, other: UnitTypeId | Iterable[UnitTypeId]) -> Units:\n        \"\"\"Returns all units that have the same base unit while being in different modes.\n\n        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\n\n        Example::\n\n            # All command centers on the ground and flying\n            ccs = self.townhalls.same_unit(UnitTypeId.COMMANDCENTER)\n\n            # All orbital commands on the ground and flying\n            ocs = self.townhalls.same_unit(UnitTypeId.ORBITALCOMMAND)\n\n            # All roaches and burrowed roaches\n            roaches = self.units.same_unit(UnitTypeId.ROACH)\n            # This is useful because roach has a different type id when burrowed\n            burrowed_roaches = self.units(UnitTypeId.ROACHBURROWED)\n\n        :param other:\n        \"\"\"\n        if isinstance(other, UnitTypeId):\n            other = {other}\n        unit_alias_types: set[int] = {u.value for u in other}\n        unit_data = self._bot_object.game_data.units\n        for unit_type in other:\n            unit_alias_types.add(unit_data[unit_type.value]._proto.unit_alias)\n        unit_alias_types.discard(0)\n        return self.filter(\n            lambda unit: unit._proto.unit_type in unit_alias_types\n            or unit._type_data._proto.unit_alias in unit_alias_types\n        )\n\n    @property\n    def center(self) -> Point2:\n        \"\"\"Returns the central position of all units.\"\"\"\n        assert self, \"Units object is empty\"\n        return Point2(\n            (\n                sum(unit._proto.pos.x for unit in self) / self.amount,\n                sum(unit._proto.pos.y for unit in self) / self.amount,\n            )\n        )\n\n    @property\n    def selected(self) -> Units:\n        \"\"\"Returns all units that are selected by the human player.\"\"\"\n        return self.filter(lambda unit: unit.is_selected)\n\n    @property\n    def tags(self) -> set[int]:\n        \"\"\"Returns all unit tags as a set.\"\"\"\n        return {unit.tag for unit in self}\n\n    @property\n    def ready(self) -> Units:\n        \"\"\"Returns all structures that are ready (construction complete).\"\"\"\n        return self.filter(lambda unit: unit.is_ready)\n\n    @property\n    def not_ready(self) -> Units:\n        \"\"\"Returns all structures that are not ready (construction not complete).\"\"\"\n        return self.filter(lambda unit: not unit.is_ready)\n\n    @property\n    def idle(self) -> Units:\n        \"\"\"Returns all units or structures that are doing nothing (unit is standing still, structure is doing nothing).\"\"\"\n        return self.filter(lambda unit: unit.is_idle)\n\n    @property\n    def owned(self) -> Units:\n        \"\"\"Deprecated: All your units.\"\"\"\n        return self.filter(lambda unit: unit.is_mine)\n\n    @property\n    def enemy(self) -> Units:\n        \"\"\"Deprecated: All enemy units.\"\"\"\n        return self.filter(lambda unit: unit.is_enemy)\n\n    @property\n    def flying(self) -> Units:\n        \"\"\"Returns all units that are flying.\"\"\"\n        return self.filter(lambda unit: unit.is_flying)\n\n    @property\n    def not_flying(self) -> Units:\n        \"\"\"Returns all units that not are flying.\"\"\"\n        return self.filter(lambda unit: not unit.is_flying)\n\n    @property\n    def structure(self) -> Units:\n        \"\"\"Deprecated: All structures.\"\"\"\n        return self.filter(lambda unit: unit.is_structure)\n\n    @property\n    def not_structure(self) -> Units:\n        \"\"\"Deprecated: All units that are not structures.\"\"\"\n        return self.filter(lambda unit: not unit.is_structure)\n\n    @property\n    def gathering(self) -> Units:\n        \"\"\"Returns all workers that are mining minerals or vespene (gather command).\"\"\"\n        return self.filter(lambda unit: unit.is_gathering)\n\n    @property\n    def returning(self) -> Units:\n        \"\"\"Returns all workers that are carrying minerals or vespene and are returning to a townhall.\"\"\"\n        return self.filter(lambda unit: unit.is_returning)\n\n    @property\n    def collecting(self) -> Units:\n        \"\"\"Returns all workers that are mining or returning resources.\"\"\"\n        return self.filter(lambda unit: unit.is_collecting)\n\n    @property\n    def visible(self) -> Units:\n        \"\"\"Returns all units or structures that are visible.\n        TODO: add proper description on which units are exactly visible (not snapshots?)\"\"\"\n        return self.filter(lambda unit: unit.is_visible)\n\n    @property\n    def mineral_field(self) -> Units:\n        \"\"\"Returns all units that are mineral fields.\"\"\"\n        return self.filter(lambda unit: unit.is_mineral_field)\n\n    @property\n    def vespene_geyser(self) -> Units:\n        \"\"\"Returns all units that are vespene geysers.\"\"\"\n        return self.filter(lambda unit: unit.is_vespene_geyser)\n\n    @property\n    def prefer_idle(self) -> Units:\n        \"\"\"Sorts units based on if they are idle. Idle units come first.\"\"\"\n        return self.sorted(lambda unit: unit.is_idle, reverse=True)\n"
  },
  {
    "path": "sc2/versions.py",
    "content": "from __future__ import annotations\n\nVERSIONS: list[dict[str, int | str]] = [\n    {\n        \"base-version\": 52910,\n        \"data-hash\": \"8D9FEF2E1CF7C6C9CBE4FBCA830DDE1C\",\n        \"fixed-hash\": \"009BC85EF547B51EBF461C83A9CBAB30\",\n        \"label\": \"3.13\",\n        \"replay-hash\": \"47BFE9D10F26B0A8B74C637D6327BF3C\",\n        \"version\": 52910,\n    },\n    {\n        \"base-version\": 53644,\n        \"data-hash\": \"CA275C4D6E213ED30F80BACCDFEDB1F5\",\n        \"fixed-hash\": \"29198786619C9011735BCFD378E49CB6\",\n        \"label\": \"3.14\",\n        \"replay-hash\": \"5AF236FC012ADB7289DB493E63F73FD5\",\n        \"version\": 53644,\n    },\n    {\n        \"base-version\": 54518,\n        \"data-hash\": \"BBF619CCDCC80905350F34C2AF0AB4F6\",\n        \"fixed-hash\": \"D5963F25A17D9E1EA406FF6BBAA9B736\",\n        \"label\": \"3.15\",\n        \"replay-hash\": \"43530321CF29FD11482AB9CBA3EB553D\",\n        \"version\": 54518,\n    },\n    {\n        \"base-version\": 54518,\n        \"data-hash\": \"6EB25E687F8637457538F4B005950A5E\",\n        \"fixed-hash\": \"D5963F25A17D9E1EA406FF6BBAA9B736\",\n        \"label\": \"3.15.1\",\n        \"replay-hash\": \"43530321CF29FD11482AB9CBA3EB553D\",\n        \"version\": 54724,\n    },\n    {\n        \"base-version\": 55505,\n        \"data-hash\": \"60718A7CA50D0DF42987A30CF87BCB80\",\n        \"fixed-hash\": \"0189B2804E2F6BA4C4591222089E63B2\",\n        \"label\": \"3.16\",\n        \"replay-hash\": \"B11811B13F0C85C29C5D4597BD4BA5A4\",\n        \"version\": 55505,\n    },\n    {\n        \"base-version\": 55958,\n        \"data-hash\": \"5BD7C31B44525DAB46E64C4602A81DC2\",\n        \"fixed-hash\": \"717B05ACD26C108D18A219B03710D06D\",\n        \"label\": \"3.16.1\",\n        \"replay-hash\": \"21C8FA403BB1194E2B6EB7520016B958\",\n        \"version\": 55958,\n    },\n    {\n        \"base-version\": 56787,\n        \"data-hash\": \"DFD1F6607F2CF19CB4E1C996B2563D9B\",\n        \"fixed-hash\": \"4E1C17AB6A79185A0D87F68D1C673CD9\",\n        \"label\": \"3.17\",\n        \"replay-hash\": \"D0296961C9EA1356F727A2468967A1E2\",\n        \"version\": 56787,\n    },\n    {\n        \"base-version\": 56787,\n        \"data-hash\": \"3F2FCED08798D83B873B5543BEFA6C4B\",\n        \"fixed-hash\": \"4474B6B7B0D1423DAA76B9623EF2E9A9\",\n        \"label\": \"3.17.1\",\n        \"replay-hash\": \"D0296961C9EA1356F727A2468967A1E2\",\n        \"version\": 57218,\n    },\n    {\n        \"base-version\": 56787,\n        \"data-hash\": \"C690FC543082D35EA0AAA876B8362BEA\",\n        \"fixed-hash\": \"4474B6B7B0D1423DAA76B9623EF2E9A9\",\n        \"label\": \"3.17.2\",\n        \"replay-hash\": \"D0296961C9EA1356F727A2468967A1E2\",\n        \"version\": 57490,\n    },\n    {\n        \"base-version\": 57507,\n        \"data-hash\": \"1659EF34997DA3470FF84A14431E3A86\",\n        \"fixed-hash\": \"95666060F129FD267C5A8135A8920AA2\",\n        \"label\": \"3.18\",\n        \"replay-hash\": \"06D650F850FDB2A09E4B01D2DF8C433A\",\n        \"version\": 57507,\n    },\n    {\n        \"base-version\": 58400,\n        \"data-hash\": \"2B06AEE58017A7DF2A3D452D733F1019\",\n        \"fixed-hash\": \"2CFE1B8757DA80086DD6FD6ECFF21AC6\",\n        \"label\": \"3.19\",\n        \"replay-hash\": \"227B6048D55535E0FF5607746EBCC45E\",\n        \"version\": 58400,\n    },\n    {\n        \"base-version\": 58400,\n        \"data-hash\": \"D9B568472880CC4719D1B698C0D86984\",\n        \"fixed-hash\": \"CE1005E9B145BDFC8E5E40CDEB5E33BB\",\n        \"label\": \"3.19.1\",\n        \"replay-hash\": \"227B6048D55535E0FF5607746EBCC45E\",\n        \"version\": 58600,\n    },\n    {\n        \"base-version\": 59587,\n        \"data-hash\": \"9B4FD995C61664831192B7DA46F8C1A1\",\n        \"fixed-hash\": \"D5D5798A9CCD099932C8F855C8129A7C\",\n        \"label\": \"4.0\",\n        \"replay-hash\": \"BB4DA41B57D490BD13C13A594E314BA4\",\n        \"version\": 59587,\n    },\n    {\n        \"base-version\": 60196,\n        \"data-hash\": \"1B8ACAB0C663D5510941A9871B3E9FBE\",\n        \"fixed-hash\": \"9327F9AF76CF11FC43D20E3E038B1B7A\",\n        \"label\": \"4.1\",\n        \"replay-hash\": \"AEA0C2A9D56E02C6B7D21E889D6B9B2F\",\n        \"version\": 60196,\n    },\n    {\n        \"base-version\": 60321,\n        \"data-hash\": \"5C021D8A549F4A776EE9E9C1748FFBBC\",\n        \"fixed-hash\": \"C53FA3A7336EDF320DCEB0BC078AEB0A\",\n        \"label\": \"4.1.1\",\n        \"replay-hash\": \"8EE054A8D98C7B0207E709190A6F3953\",\n        \"version\": 60321,\n    },\n    {\n        \"base-version\": 60321,\n        \"data-hash\": \"33D9FE28909573253B7FC352CE7AEA40\",\n        \"fixed-hash\": \"FEE6F86A211380DF509F3BBA58A76B87\",\n        \"label\": \"4.1.2\",\n        \"replay-hash\": \"8EE054A8D98C7B0207E709190A6F3953\",\n        \"version\": 60604,\n    },\n    {\n        \"base-version\": 60321,\n        \"data-hash\": \"F486693E00B2CD305B39E0AB254623EB\",\n        \"fixed-hash\": \"AF7F5499862F497C7154CB59167FEFB3\",\n        \"label\": \"4.1.3\",\n        \"replay-hash\": \"8EE054A8D98C7B0207E709190A6F3953\",\n        \"version\": 61021,\n    },\n    {\n        \"base-version\": 60321,\n        \"data-hash\": \"2E2A3F6E0BAFE5AC659C4D39F13A938C\",\n        \"fixed-hash\": \"F9A68CF1FBBF867216FFECD9EAB72F4A\",\n        \"label\": \"4.1.4\",\n        \"replay-hash\": \"8EE054A8D98C7B0207E709190A6F3953\",\n        \"version\": 61545,\n    },\n    {\n        \"base-version\": 62347,\n        \"data-hash\": \"C0C0E9D37FCDBC437CE386C6BE2D1F93\",\n        \"fixed-hash\": \"A5C4BE991F37F1565097AAD2A707FC4C\",\n        \"label\": \"4.2\",\n        \"replay-hash\": \"2167A7733637F3AFC49B210D165219A7\",\n        \"version\": 62347,\n    },\n    {\n        \"base-version\": 62848,\n        \"data-hash\": \"29BBAC5AFF364B6101B661DB468E3A37\",\n        \"fixed-hash\": \"ABAF9318FE79E84485BEC5D79C31262C\",\n        \"label\": \"4.2.1\",\n        \"replay-hash\": \"A7ACEC5759ADB459A5CEC30A575830EC\",\n        \"version\": 62848,\n    },\n    {\n        \"base-version\": 63454,\n        \"data-hash\": \"3CB54C86777E78557C984AB1CF3494A0\",\n        \"fixed-hash\": \"A9DCDAA97F7DA07F6EF29C0BF4DFC50D\",\n        \"label\": \"4.2.2\",\n        \"replay-hash\": \"A7ACEC5759ADB459A5CEC30A575830EC\",\n        \"version\": 63454,\n    },\n    {\n        \"base-version\": 64469,\n        \"data-hash\": \"C92B3E9683D5A59E08FC011F4BE167FF\",\n        \"fixed-hash\": \"DDF3E0A6C00DC667F59BF90F793C71B8\",\n        \"label\": \"4.3\",\n        \"replay-hash\": \"6E80072968515101AF08D3953FE3EEBA\",\n        \"version\": 64469,\n    },\n    {\n        \"base-version\": 65094,\n        \"data-hash\": \"E5A21037AA7A25C03AC441515F4E0644\",\n        \"fixed-hash\": \"09EF8E9B96F14C5126F1DB5378D15F3A\",\n        \"label\": \"4.3.1\",\n        \"replay-hash\": \"DD9B57C516023B58F5B588377880D93A\",\n        \"version\": 65094,\n    },\n    {\n        \"base-version\": 65384,\n        \"data-hash\": \"B6D73C85DFB70F5D01DEABB2517BF11C\",\n        \"fixed-hash\": \"615C1705E4C7A5FD8690B3FD376C1AFE\",\n        \"label\": \"4.3.2\",\n        \"replay-hash\": \"DD9B57C516023B58F5B588377880D93A\",\n        \"version\": 65384,\n    },\n    {\n        \"base-version\": 65895,\n        \"data-hash\": \"BF41339C22AE2EDEBEEADC8C75028F7D\",\n        \"fixed-hash\": \"C622989A4C0AF7ED5715D472C953830B\",\n        \"label\": \"4.4\",\n        \"replay-hash\": \"441BBF1A222D5C0117E85B118706037F\",\n        \"version\": 65895,\n    },\n    {\n        \"base-version\": 66668,\n        \"data-hash\": \"C094081D274A39219061182DBFD7840F\",\n        \"fixed-hash\": \"1C236A42171AAC6DD1D5E50D779C522D\",\n        \"label\": \"4.4.1\",\n        \"replay-hash\": \"21D5B4B4D5175C562CF4C4A803C995C6\",\n        \"version\": 66668,\n    },\n    {\n        \"base-version\": 67188,\n        \"data-hash\": \"2ACF84A7ECBB536F51FC3F734EC3019F\",\n        \"fixed-hash\": \"2F0094C990E0D4E505570195F96C2A0C\",\n        \"label\": \"4.5\",\n        \"replay-hash\": \"E9873B3A3846F5878CEE0D1E2ADD204A\",\n        \"version\": 67188,\n    },\n    {\n        \"base-version\": 67188,\n        \"data-hash\": \"6D239173B8712461E6A7C644A5539369\",\n        \"fixed-hash\": \"A1BC35751ACC34CF887321A357B40158\",\n        \"label\": \"4.5.1\",\n        \"replay-hash\": \"E9873B3A3846F5878CEE0D1E2ADD204A\",\n        \"version\": 67344,\n    },\n    {\n        \"base-version\": 67926,\n        \"data-hash\": \"7DE59231CBF06F1ECE9A25A27964D4AE\",\n        \"fixed-hash\": \"570BEB69151F40D010E89DE1825AE680\",\n        \"label\": \"4.6\",\n        \"replay-hash\": \"DA662F9091DF6590A5E323C21127BA5A\",\n        \"version\": 67926,\n    },\n    {\n        \"base-version\": 67926,\n        \"data-hash\": \"BEA99B4A8E7B41E62ADC06D194801BAB\",\n        \"fixed-hash\": \"309E45F53690F8D1108F073ABB4D4734\",\n        \"label\": \"4.6.1\",\n        \"replay-hash\": \"DA662F9091DF6590A5E323C21127BA5A\",\n        \"version\": 68195,\n    },\n    {\n        \"base-version\": 69232,\n        \"data-hash\": \"B3E14058F1083913B80C20993AC965DB\",\n        \"fixed-hash\": \"21935E776237EF12B6CC73E387E76D6E\",\n        \"label\": \"4.6.2\",\n        \"replay-hash\": \"A230717B315D83ACC3697B6EC28C3FF6\",\n        \"version\": 69232,\n    },\n    {\n        \"base-version\": 70154,\n        \"data-hash\": \"8E216E34BC61ABDE16A59A672ACB0F3B\",\n        \"fixed-hash\": \"09CD819C667C67399F5131185334243E\",\n        \"label\": \"4.7\",\n        \"replay-hash\": \"9692B04D6E695EF08A2FB920979E776C\",\n        \"version\": 70154,\n    },\n    {\n        \"base-version\": 70154,\n        \"data-hash\": \"94596A85191583AD2EBFAE28C5D532DB\",\n        \"fixed-hash\": \"0AE50F82AC1A7C0DCB6A290D7FBA45DB\",\n        \"label\": \"4.7.1\",\n        \"replay-hash\": \"D74FBB3CB0897A3EE8F44E78119C4658\",\n        \"version\": 70326,\n    },\n    {\n        \"base-version\": 71061,\n        \"data-hash\": \"760581629FC458A1937A05ED8388725B\",\n        \"fixed-hash\": \"815C099DF1A17577FDC186FDB1381B16\",\n        \"label\": \"4.8\",\n        \"replay-hash\": \"BD692311442926E1F0B7C17E9ABDA34B\",\n        \"version\": 71061,\n    },\n    {\n        \"base-version\": 71523,\n        \"data-hash\": \"FCAF3F050B7C0CC7ADCF551B61B9B91E\",\n        \"fixed-hash\": \"4593CC331691620509983E92180A309A\",\n        \"label\": \"4.8.1\",\n        \"replay-hash\": \"BD692311442926E1F0B7C17E9ABDA34B\",\n        \"version\": 71523,\n    },\n    {\n        \"base-version\": 71663,\n        \"data-hash\": \"FE90C92716FC6F8F04B74268EC369FA5\",\n        \"fixed-hash\": \"1DBF3819F3A7367592648632CC0D5BFD\",\n        \"label\": \"4.8.2\",\n        \"replay-hash\": \"E43A9885B3EFAE3D623091485ECCCB6C\",\n        \"version\": 71663,\n    },\n    {\n        \"base-version\": 72282,\n        \"data-hash\": \"0F14399BBD0BA528355FF4A8211F845B\",\n        \"fixed-hash\": \"E9958B2CB666DCFE101D23AF87DB8140\",\n        \"label\": \"4.8.3\",\n        \"replay-hash\": \"3AF3657F55AB961477CE268F5CA33361\",\n        \"version\": 72282,\n    },\n    {\n        \"base-version\": 73286,\n        \"data-hash\": \"CD040C0675FD986ED37A4CA3C88C8EB5\",\n        \"fixed-hash\": \"62A146F7A0D19A8DD05BF011631B31B8\",\n        \"label\": \"4.8.4\",\n        \"replay-hash\": \"EE3A89F443BE868EBDA33A17C002B609\",\n        \"version\": 73286,\n    },\n    {\n        \"base-version\": 73559,\n        \"data-hash\": \"B2465E73AED597C74D0844112D582595\",\n        \"fixed-hash\": \"EF0A43C33413613BC7343B86C0A7CC92\",\n        \"label\": \"4.8.5\",\n        \"replay-hash\": \"147388D35E76861BD4F590F8CC5B7B0B\",\n        \"version\": 73559,\n    },\n    {\n        \"base-version\": 73620,\n        \"data-hash\": \"AA18FEAD6573C79EF707DF44ABF1BE61\",\n        \"fixed-hash\": \"4D76491CCAE756F0498D1C5B2973FF9C\",\n        \"label\": \"4.8.6\",\n        \"replay-hash\": \"147388D35E76861BD4F590F8CC5B7B0B\",\n        \"version\": 73620,\n    },\n    {\n        \"base-version\": 74071,\n        \"data-hash\": \"70C74A2DCA8A0D8E7AE8647CAC68ACCA\",\n        \"fixed-hash\": \"C4A3F01B4753245296DC94BC1B5E9B36\",\n        \"label\": \"4.9\",\n        \"replay-hash\": \"19D15E5391FACB379BFCA262CA8FD208\",\n        \"version\": 74071,\n    },\n    {\n        \"base-version\": 74456,\n        \"data-hash\": \"218CB2271D4E2FA083470D30B1A05F02\",\n        \"fixed-hash\": \"E82051387C591CAB1212B64073759826\",\n        \"label\": \"4.9.1\",\n        \"replay-hash\": \"1586ADF060C26219FF3404673D70245B\",\n        \"version\": 74456,\n    },\n    {\n        \"base-version\": 74741,\n        \"data-hash\": \"614480EF79264B5BD084E57F912172FF\",\n        \"fixed-hash\": \"500CC375B7031C8272546B78E9BE439F\",\n        \"label\": \"4.9.2\",\n        \"replay-hash\": \"A7FAC56F940382E05157EAB19C932E3A\",\n        \"version\": 74741,\n    },\n    {\n        \"base-version\": 75025,\n        \"data-hash\": \"C305368C63621480462F8F516FB64374\",\n        \"fixed-hash\": \"DEE7842C8BCB6874EC254AA3D45365F7\",\n        \"label\": \"4.9.3\",\n        \"replay-hash\": \"A7FAC56F940382E05157EAB19C932E3A\",\n        \"version\": 75025,\n    },\n    {\n        \"base-version\": 75689,\n        \"data-hash\": \"B89B5D6FA7CBF6452E721311BFBC6CB2\",\n        \"fixed-hash\": \"2B2097DC4AD60A2D1E1F38691A1FF111\",\n        \"label\": \"4.10\",\n        \"replay-hash\": \"6A60E59031A7DB1B272EE87E51E4C7CD\",\n        \"version\": 75689,\n    },\n    {\n        \"base-version\": 75800,\n        \"data-hash\": \"DDFFF9EC4A171459A4F371C6CC189554\",\n        \"fixed-hash\": \"1FB8FAF4A87940621B34F0B8F6FDDEA6\",\n        \"label\": \"4.10.1\",\n        \"replay-hash\": \"6A60E59031A7DB1B272EE87E51E4C7CD\",\n        \"version\": 75800,\n    },\n    {\n        \"base-version\": 76052,\n        \"data-hash\": \"D0F1A68AA88BA90369A84CD1439AA1C3\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.10.2\",\n        \"replay-hash\": \"\",\n        \"version\": 76052,\n    },\n    {\n        \"base-version\": 76114,\n        \"data-hash\": \"CDB276D311F707C29BA664B7754A7293\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.10.3\",\n        \"replay-hash\": \"\",\n        \"version\": 76114,\n    },\n    {\n        \"base-version\": 76811,\n        \"data-hash\": \"FF9FA4EACEC5F06DEB27BD297D73ED67\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.10.4\",\n        \"replay-hash\": \"\",\n        \"version\": 76811,\n    },\n    {\n        \"base-version\": 77379,\n        \"data-hash\": \"70E774E722A58287EF37D487605CD384\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.11.0\",\n        \"replay-hash\": \"\",\n        \"version\": 77379,\n    },\n    {\n        \"base-version\": 77379,\n        \"data-hash\": \"F92D1127A291722120AC816F09B2E583\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.11.1\",\n        \"replay-hash\": \"\",\n        \"version\": 77474,\n    },\n    {\n        \"base-version\": 77535,\n        \"data-hash\": \"FC43E0897FCC93E4632AC57CBC5A2137\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.11.2\",\n        \"replay-hash\": \"\",\n        \"version\": 77535,\n    },\n    {\n        \"base-version\": 77661,\n        \"data-hash\": \"A15B8E4247434B020086354F39856C51\",\n        \"fixed-hash\": \"\",\n        \"label\": \"4.11.3\",\n        \"replay-hash\": \"\",\n        \"version\": 77661,\n    },\n    {\n        \"base-version\": 78285,\n        \"data-hash\": \"69493AFAB5C7B45DDB2F3442FD60F0CF\",\n        \"fixed-hash\": \"21D2EBD5C79DECB3642214BAD4A7EF56\",\n        \"label\": \"4.11.4\",\n        \"replay-hash\": \"CAB5C056EDBDA415C552074BF363CC85\",\n        \"version\": 78285,\n    },\n    {\n        \"base-version\": 79998,\n        \"data-hash\": \"B47567DEE5DC23373BFF57194538DFD3\",\n        \"fixed-hash\": \"0A698A1B072BC4B087F44DDEF0BE361E\",\n        \"label\": \"4.12.0\",\n        \"replay-hash\": \"9E15AA09E15FE3AF3655126CEEC7FF42\",\n        \"version\": 79998,\n    },\n    {\n        \"base-version\": 80188,\n        \"data-hash\": \"44DED5AED024D23177C742FC227C615A\",\n        \"fixed-hash\": \"0A698A1B072BC4B087F44DDEF0BE361E\",\n        \"label\": \"4.12.1\",\n        \"replay-hash\": \"9E15AA09E15FE3AF3655126CEEC7FF42\",\n        \"version\": 80188,\n    },\n    {\n        \"base-version\": 80949,\n        \"data-hash\": \"9AE39C332883B8BF6AA190286183ED72\",\n        \"fixed-hash\": \"DACEAFAB8B983C08ACD31ABC085A0052\",\n        \"label\": \"5.0.0\",\n        \"replay-hash\": \"28C41277C5837AABF9838B64ACC6BDCF\",\n        \"version\": 80949,\n    },\n    {\n        \"base-version\": 81009,\n        \"data-hash\": \"0D28678BC32E7F67A238F19CD3E0A2CE\",\n        \"fixed-hash\": \"DACEAFAB8B983C08ACD31ABC085A0052\",\n        \"label\": \"5.0.1\",\n        \"replay-hash\": \"28C41277C5837AABF9838B64ACC6BDCF\",\n        \"version\": 81009,\n    },\n    {\n        \"base-version\": 81102,\n        \"data-hash\": \"DC0A1182FB4ABBE8E29E3EC13CF46F68\",\n        \"fixed-hash\": \"0C193BD5F63BBAB79D798278F8B2548E\",\n        \"label\": \"5.0.2\",\n        \"replay-hash\": \"08BB9D4CAE25B57160A6E4AD7B8E1A5A\",\n        \"version\": 81102,\n    },\n    {\n        \"base-version\": 81433,\n        \"data-hash\": \"5FD8D4B6B52723B44862DF29F232CF31\",\n        \"fixed-hash\": \"4FC35CEA63509AB06AA80AACC1B3B700\",\n        \"label\": \"5.0.3\",\n        \"replay-hash\": \"0920F1BD722655B41DA096B98CC0912D\",\n        \"version\": 81433,\n    },\n    {\n        \"base-version\": 82457,\n        \"data-hash\": \"D2707E265785612D12B381AF6ED9DBF4\",\n        \"fixed-hash\": \"ED05F0DB335D003FBC3C7DEF69911114\",\n        \"label\": \"5.0.4\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 82457,\n    },\n    {\n        \"base-version\": 82893,\n        \"data-hash\": \"D795328C01B8A711947CC62AA9750445\",\n        \"fixed-hash\": \"ED05F0DB335D003FBC3C7DEF69911114\",\n        \"label\": \"5.0.5\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 82893,\n    },\n    {\n        \"base-version\": 83830,\n        \"data-hash\": \"B4745D6A4F982A3143C183D8ACB6C3E3\",\n        \"fixed-hash\": \"ed05f0db335d003fbc3c7def69911114\",\n        \"label\": \"5.0.6\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 83830,\n    },\n    {\n        \"base-version\": 84643,\n        \"data-hash\": \"A389D1F7DF9DD792FBE980533B7119FF\",\n        \"fixed-hash\": \"368DE29820A74F5BE747543AC02DB3F8\",\n        \"label\": \"5.0.7\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 84643,\n    },\n    {\n        \"base-version\": 86383,\n        \"data-hash\": \"22EAC562CD0C6A31FB2C2C21E3AA3680\",\n        \"fixed-hash\": \"B19F4D8B87A2835F9447CA17EDD40C1E\",\n        \"label\": \"5.0.8\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 86383,\n    },\n    {\n        \"base-version\": 87702,\n        \"data-hash\": \"F799E093428D419FD634CCE9B925218C\",\n        \"fixed-hash\": \"B19F4D8B87A2835F9447CA17EDD40C1E\",\n        \"label\": \"5.0.9\",\n        \"replay-hash\": \"7D9EE968AAD81761334BD9076BFD9EFF\",\n        \"version\": 87702,\n    },\n    {\n        \"base-version\": 88500,\n        \"data-hash\": \"F38043A301B034A78AD13F558257DCF8\",\n        \"fixed-hash\": \"F3853B6E3B6013415CAC30EF3B27564B\",\n        \"label\": \"5.0.10\",\n        \"replay-hash\": \"A79CD3B6C6DADB0ECAEFA06E6D18E47B\",\n        \"version\": 88500,\n    },\n    {\n        \"base-version\": 89720,\n        \"data-hash\": \"D371D4D7D1E6C131B24A09FC0E758547\",\n        \"fixed-hash\": \"F3853B6E3B6013415CAC30EF3B27564B\",\n        \"label\": \"5.0.11\",\n        \"replay-hash\": \"A79CD3B6C6DADB0ECAEFA06E6D18E47B\",\n        \"version\": 89720,\n    },\n    {\n        \"base-version\": 91115,\n        \"data-hash\": \"7857A76754FEB47C823D18993C476BF0\",\n        \"fixed-hash\": \"99E19D19DA59112C1744A83CB49614A5\",\n        \"label\": \"5.0.12\",\n        \"replay-hash\": \"BE64E420B329BD2A7D10EEBC0039D6E5\",\n        \"version\": 89720,\n    },\n    {\n        \"base-version\": 92028,\n        \"data-hash\": \"2B7746A6706F919775EF1BADFC95EA1C\",\n        \"fixed-hash\": \"163B1CDF46F09B621F6312CD6901228E\",\n        \"label\": \"5.0.13\",\n        \"replay-hash\": \"BE64E420B329BD2A7D10EEBC0039D6E5\",\n        \"version\": 92028,\n    },\n    {\n        \"base-version\": 93333,\n        \"data-hash\": \"446907060311fb1cc29eb31e547bb9fd\",\n        \"fixed-hash\": \"BE86048D1DCE8650E1655D2FE2B665A8\",\n        \"label\": \"5.0.14.93333\",\n        \"replay-hash\": \"BE64E420B329BD2A7D10EEBC0039D6E5\",\n        \"version\": 93333,\n    },\n    {\n        \"base-version\": 94137,\n        \"data-hash\": \"519EE8D06E384469C652DD58FC6016AC\",\n        \"fixed-hash\": \"B100C340B3D0797CBE914AE091A68653\",\n        \"label\": \"5.0.14.94137\",\n        \"replay-hash\": \"BE64E420B329BD2A7D10EEBC0039D6E5\",\n        \"version\": 94137,\n    },\n]\n"
  },
  {
    "path": "sc2/wsl.py",
    "content": "from __future__ import annotations\n\nimport os\nimport re\nimport subprocess\nfrom pathlib import Path, PureWindowsPath\n\nfrom loguru import logger\n\n## This file is used for compatibility with WSL and shouldn't need to be\n## accessed directly by any bot clients\n\n\ndef win_path_to_wsl_path(path) -> Path:\n    \"\"\"Convert a path like C:\\\\foo to /mnt/c/foo\"\"\"\n    return Path(\"/mnt\") / PureWindowsPath(re.sub(\"^([A-Z]):\", lambda m: m.group(1).lower(), path))\n\n\ndef wsl_path_to_win_path(path) -> PureWindowsPath:\n    \"\"\"Convert a path like /mnt/c/foo to C:\\\\foo\"\"\"\n    return PureWindowsPath(re.sub(\"^/mnt/([a-z])\", lambda m: m.group(1).upper() + \":\", path))\n\n\ndef get_wsl_home():\n    \"\"\"Get home directory of from Windows, even if run in WSL\"\"\"\n    proc = subprocess.run([\"powershell.exe\", \"-Command\", \"Write-Host -NoNewLine $HOME\"], capture_output=True)\n\n    if proc.returncode != 0:\n        return None\n\n    return win_path_to_wsl_path(proc.stdout.decode(\"utf-8\"))\n\n\nRUN_SCRIPT = \"\"\"$proc = Start-Process -NoNewWindow -PassThru \"%s\" \"%s\"\nif ($proc) {\n    Write-Host $proc.id\n    exit $proc.ExitCode\n} else {\n    exit 1\n}\"\"\"\n\n\ndef run(popen_args, sc2_cwd) -> subprocess.Popen[str]:\n    \"\"\"Run SC2 in Windows and get the pid so that it can be killed later.\"\"\"\n    path = wsl_path_to_win_path(popen_args[0])\n    args = \" \".join(popen_args[1:])\n\n    return subprocess.Popen(\n        [\"powershell.exe\", \"-Command\", RUN_SCRIPT % (path, args)],\n        cwd=sc2_cwd,\n        stdout=subprocess.PIPE,\n        universal_newlines=True,\n        bufsize=1,\n    )\n\n\ndef kill(wsl_process) -> bool:\n    \"\"\"Needed to kill a process started with WSL. Returns true if killed successfully.\"\"\"\n    # HACK: subprocess and WSL1 appear to have a nasty interaction where\n    # any streams are never closed and the process is never considered killed,\n    # despite having an exit code (this works on WSL2 as well, but isn't\n    # necessary). As a result,\n    # 1: We need to read using readline (to make sure we block long enough to\n    #    get the exit code in the rare case where the user immediately hits ^C)\n    out = wsl_process.stdout.readline().rstrip()\n    # 2: We need to use __exit__, since kill() calls send_signal(), which thinks\n    #    the process has already exited!\n    wsl_process.__exit__(None, None, None)\n    proc = subprocess.run([\"taskkill.exe\", \"-f\", \"-pid\", out], capture_output=True)\n    return proc.returncode == 0  # Returns 128 on failure\n\n\ndef detect() -> str | None:\n    \"\"\"Detect the current running version of WSL, and bail out if it doesn't exist\"\"\"\n    # Allow disabling WSL detection with an environment variable\n    if os.getenv(\"SC2_WSL_DETECT\", \"1\") == \"0\":\n        return None\n\n    wsl_name = os.environ.get(\"WSL_DISTRO_NAME\")\n    if not wsl_name:\n        return None\n\n    try:\n        wsl_proc = subprocess.run([\"wsl.exe\", \"--list\", \"--running\", \"--verbose\"], capture_output=True)\n    except (OSError, ValueError):\n        return None\n    if wsl_proc.returncode != 0:\n        return None\n\n    # WSL.exe returns a bunch of null characters for some reason, as well as\n    # windows-style linebreaks. It's inconsistent about how many \\rs it uses\n    # and this could change in the future, so strip out all junk and split by\n    # Unix-style newlines for safety's sake.\n    lines = re.sub(r\"\\000|\\r\", \"\", wsl_proc.stdout.decode(\"utf-8\")).split(\"\\n\")\n\n    def line_has_proc(ln: str):\n        if wsl_name is not None:\n            return re.search(\"^\\\\s*[*]?\\\\s+\" + wsl_name, ln)\n\n    def line_version(ln: str):\n        return re.sub(\"^.*\\\\s+(\\\\d+)\\\\s*$\", \"\\\\1\", ln)\n\n    versions = [line_version(ln) for ln in lines if line_has_proc(ln)]\n\n    try:\n        version = versions[0]\n        if int(version) not in [1, 2]:\n            return None\n    except (ValueError, IndexError):\n        return None\n\n    logger.info(f\"WSL version {version} detected\")\n\n    if version == \"2\" and not (os.environ.get(\"SC2CLIENTHOST\") and os.environ.get(\"SC2SERVERHOST\")):\n        logger.warning(\"You appear to be running WSL2 without your hosts configured correctly.\")\n        logger.warning(\"This may result in SC2 staying on a black screen and not connecting to your bot.\")\n        logger.warning(\"Please see the python-sc2 README for WSL2 configuration instructions.\")\n\n    return \"WSL\" + version\n"
  },
  {
    "path": "test/Dockerfile",
    "content": "# Buildable via command from root folder\n# docker build -f test/Dockerfile -t test_image --build-arg PYTHON_VERSION=3.10 --build-arg SC2_VERSION=4.10 .\n\n# For more info see https://github.com/BurnySc2/python-sc2-docker\nARG PYTHON_VERSION=3.10\nARG SC2_VERSION=4.10\nARG VERSION_NUMBER=1.0.0\n\nFROM burnysc2/python-sc2-docker:py_$PYTHON_VERSION-sc2_$SC2_VERSION-v$VERSION_NUMBER\n\n# Debugging purposes\nRUN echo $PYTHON_VERSION\nRUN echo $SC2_VERSION\nRUN echo $VERSION_NUMBER\n\n# Copy files from the current commit (the python-sc2 folder) to root\nADD . /root/python-sc2\n\n# Install the python-sc2 library and its requirements (s2clientprotocol etc.) to python\nWORKDIR /root/python-sc2\nRUN pip install --no-cache-dir uv \\\n    # This will not include dev dependencies\n    && uv export --format requirements-txt --output-file requirements.txt --no-hashes \\\n    && pip install --no-cache-dir -r requirements.txt\n\n# This will be executed during the container run instead:\n# docker run test_image -c \"uv run python examples/protoss/cannon_rush.py\"\n\nENTRYPOINT [ \"/bin/bash\" ]\n"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/autotest_bot.py",
    "content": "from __future__ import annotations\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.effect_id import EffectId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass TestBot(BotAI):\n    def __init__(self):\n        BotAI.__init__(self)\n        # The time the bot has to complete all tests, here: the number of game seconds\n        self.game_time_timeout_limit = 20 * 60  # 20 minutes ingame time\n\n        # Check how many test action functions we have\n        # At least 4 tests because we test properties and variables\n        self.action_tests = [\n            getattr(self, f\"test_botai_actions{index}\")\n            for index in range(4000)\n            if hasattr(getattr(self, f\"test_botai_actions{index}\", 0), \"__call__\")\n        ]\n        self.tests_done_by_name = set()\n\n        # Keep track of the action index and when the last action was started\n        self.current_action_index = 1\n        self.iteration_last_action_started = 8\n        # There will be 20 iterations of the bot doing nothing between tests\n        self.iteration_wait_time_between_actions = 20\n\n        # Variables for test_botai_actions11\n\n    async def on_start(self):\n        self.client.game_step = 8\n        # await self.client.quick_save()\n        await self.distribute_workers()\n\n    async def on_step(self, iteration):\n        if iteration == 0:\n            await self.chat_send(\"(glhf)\")\n        # Test if chat message was sent correctly\n        if iteration == 1:\n            assert len(self.state.chat) >= 1, self.state.chat\n\n        # Tests at start\n        if iteration == 3:\n            # No need to use try except as the travis test script checks for \"Traceback\" in STDOUT\n            await self.test_botai_properties()\n            await self.test_botai_functions()\n            await self.test_game_state_static_variables()\n            await self.test_game_info_static_variables()\n\n        # Test actions\n        if iteration == 7:\n            for action_test in self.action_tests:\n                await action_test()\n\n        # Exit bot\n        if iteration > 100:\n            logger.info(f\"Tests completed after {round(self.time, 1)} seconds\")\n            exit(0)\n\n    async def clean_up_center(self):\n        map_center = self.game_info.map_center\n        # Remove everything close to map center\n        my_units = self.all_own_units\n        if my_units:\n            my_units = my_units.closer_than(20, map_center)\n        if my_units:\n            await self.client.debug_kill_unit(my_units)\n        enemy_units = self.enemy_units | self.enemy_structures\n        if enemy_units:\n            enemy_units = enemy_units.closer_than(20, map_center)\n        if enemy_units:\n            await self.client.debug_kill_unit(enemy_units)\n        await self._advance_steps(2)\n\n    # Test BotAI properties, starting conditions\n    async def test_botai_properties(self):\n        assert 1 <= self.player_id <= 2, self.player_id\n        assert self.race == Race.Terran, self.race\n        assert 0 <= self.time <= 180, self.time\n        assert self.start_location == self.townhalls.random.position, (\n            self.start_location,\n            self.townhalls.random.position,\n        )\n        for loc in self.enemy_start_locations:\n            assert isinstance(loc, Point2), loc\n            assert loc.distance_to(self.start_location) > 20, (loc, self.start_location)\n        assert self.main_base_ramp.top_center.distance_to(self.start_location) < 30, self.main_base_ramp.top_center\n        assert self.can_afford(UnitTypeId.SCV)\n        assert self.owned_expansions == {self.townhalls.first.position: self.townhalls.first}\n        # Test if bot start location is in expansion locations\n        assert self.townhalls.random.position in self.expansion_locations_list\n        # Test if enemy start locations are in expansion locations\n        for location in self.enemy_start_locations:\n            assert location in self.expansion_locations_list\n\n        self.tests_done_by_name.add(\"test_botai_properties\")\n\n    # Test BotAI functions\n    async def test_botai_functions(self):\n        for location in self.expansion_locations_list:\n            # Can't build on spawn locations, skip these\n            if location in self.enemy_start_locations or location == self.start_location:\n                continue\n            assert (await self.can_place(UnitTypeId.COMMANDCENTER, [location]))[0]\n            assert (await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [location]))[0]\n            # TODO Remove the following two lines if can_place function gets fully converted to only accept list of positions\n            assert await self.can_place(UnitTypeId.COMMANDCENTER, [location])\n            assert await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [location])\n            assert await self.can_place_single(UnitTypeId.COMMANDCENTER, location)\n            assert await self.can_place_single(AbilityId.TERRANBUILD_COMMANDCENTER, location)\n            await self.find_placement(UnitTypeId.COMMANDCENTER, location)\n        assert len(await self.get_available_abilities(self.workers)) == self.workers.amount\n        self.tests_done_by_name.add(\"test_botai_functions\")\n\n    # Test self.state variables\n    async def test_game_state_static_variables(self):\n        assert len(self.state.actions) == 0, self.state.actions\n        assert len(self.state.action_errors) == 0, self.state.action_errors\n        assert len(self.state.chat) == 0, self.state.chat\n        assert self.state.game_loop > 0, self.state.game_loop\n        assert self.state.score.collection_rate_minerals >= 0, self.state.score.collection_rate_minerals\n        assert len(self.state.upgrades) == 0, self.state.upgrades\n        self.tests_done_by_name.add(\"test_game_state_static_variables\")\n\n    # Test self._game_info variables\n    async def test_game_info_static_variables(self):\n        assert len(self.game_info.players) == 2, self.game_info.players\n        assert len(self.game_info.map_ramps) >= 2, self.game_info.map_ramps\n        assert len(self.game_info.player_races) == 2, self.game_info.player_races\n        self.tests_done_by_name.add(\"test_game_info_static_variables\")\n\n    async def test_botai_actions1(self):\n        # Test BotAI action: train SCV\n        while self.already_pending(UnitTypeId.SCV) < 1:\n            if self.can_afford(UnitTypeId.SCV):\n                self.townhalls.random.train(UnitTypeId.SCV)\n            await self._advance_steps(2)\n\n        await self._advance_steps(2)\n        logger.warning(\"Action test 01 successful.\")\n\n    # Test BotAI action: move all SCVs to center of map\n    async def test_botai_actions2(self):\n        center = self.game_info.map_center\n\n        def temp_filter(unit: Unit):\n            return (\n                unit.is_moving\n                or unit.is_patrolling\n                or unit.orders\n                and unit.orders[0] == AbilityId.HOLDPOSITION_HOLD\n                or unit.is_attacking\n            )\n\n        scv_action_list = [\"move\", \"patrol\", \"attack\", \"hold\", \"scan_move\"]\n        while self.units.filter(lambda unit: temp_filter(unit)).amount < len(scv_action_list):\n            scv: Unit\n            for index, scv in enumerate(self.workers):\n                if index > len(scv_action_list):\n                    scv.stop()\n                action = scv_action_list[index % len(scv_action_list)]\n                if action == \"move\":\n                    scv.move(center)\n                elif action == \"patrol\":\n                    scv.patrol(center)\n                elif action == \"attack\":\n                    scv.attack(center)\n                elif action == \"hold\":\n                    scv.hold_position()\n\n            await self._advance_steps(2)\n\n        await self._advance_steps(2)\n        logger.warning(\"Action test 02 successful.\")\n\n    async def test_botai_actions3(self):\n        # Test BotAI action: move some scvs to the center, some to minerals\n        center = self.game_info.map_center\n\n        while self.units.filter(lambda x: x.is_moving).amount < 6 and self.units.gathering.amount >= 6:\n            scvs = self.workers\n            scvs1 = scvs[:6]\n            scvs2 = scvs[6:]\n            for scv in scvs1:\n                scv.move(center)\n            mf = self.mineral_field.closest_to(self.townhalls.random)\n            for scv in scvs2:\n                scv.gather(mf)\n\n            await self._advance_steps(2)\n        await self._advance_steps(2)\n        logger.warning(\"Action test 03 successful.\")\n\n    async def test_botai_actions4(self):\n        # Test BotAI action: move all SCVs to mine minerals near townhall\n        while self.units.gathering.amount < 12:\n            mf = self.mineral_field.closest_to(self.townhalls.random)\n            for scv in self.workers:\n                scv.gather(mf)\n\n            await self._advance_steps(2)\n        await self._advance_steps(2)\n        logger.warning(\"Action test 04 successful.\")\n\n    async def test_botai_actions5(self):\n        # Test BotAI action: self.expand_now() which tests for get_next_expansion, select_build_worker, can_place, find_placement, build and can_afford\n        # Wait till worker has started construction of CC\n        while True:\n            if self.can_afford(UnitTypeId.COMMANDCENTER):\n                await self.get_next_expansion()\n                await self.expand_now()\n\n            await self._advance_steps(10)\n\n            assert self.structures_without_construction_SCVs(UnitTypeId.COMMANDCENTER).amount == 0\n\n            if self.townhalls(UnitTypeId.COMMANDCENTER).amount >= 2:\n                assert self.townhalls(UnitTypeId.COMMANDCENTER).not_ready.amount == 1\n                assert self.already_pending(UnitTypeId.COMMANDCENTER) == 1\n                # The CC construction has started, 'worker_en_route_to_build' should show 0\n                assert self.worker_en_route_to_build(UnitTypeId.COMMANDCENTER) == 0\n                break\n            elif self.already_pending(UnitTypeId.COMMANDCENTER) == 1:\n                assert self.worker_en_route_to_build(UnitTypeId.COMMANDCENTER) == 1\n\n        await self._advance_steps(2)\n        logger.warning(\"Action test 05 successful.\")\n\n    async def test_botai_actions6(self):\n        # Test if reaper grenade shows up in effects\n        center = self.game_info.map_center\n\n        while True:\n            if self.units(UnitTypeId.REAPER).amount < 10:\n                await self.client.debug_create_unit([(UnitTypeId.REAPER, 10, center, 1)])\n\n            for reaper in self.units(UnitTypeId.REAPER):\n                reaper(AbilityId.KD8CHARGE_KD8CHARGE, center)\n\n            # logger.info(f\"Effects: {self.state.effects}\")\n            for effect in self.state.effects:\n                # logger.info(f\"Effect: {effect}\")\n                pass\n            # Cleanup\n            await self._advance_steps(2)\n            # Check if condition is met\n            if len(self.state.effects) != 0:\n                break\n\n        await self.client.debug_kill_unit(self.units(UnitTypeId.REAPER))\n        # Wait for effectts to time out\n        await self._advance_steps(100)\n        logger.warning(\"Action test 06 successful.\")\n\n    async def test_botai_actions7(self):\n        # Test ravager effects\n        center = self.game_info.map_center\n        while True:\n            if self.units(UnitTypeId.RAVAGER).amount < 10:\n                await self.client.debug_create_unit([(UnitTypeId.RAVAGER, 10, center, 1)])\n            for ravager in self.units(UnitTypeId.RAVAGER):\n                ravager(AbilityId.EFFECT_CORROSIVEBILE, center)\n\n            # logger.info(f\"Effects: {self.state.effects}\")\n            for effect in self.state.effects:\n                # logger.info(f\"Effect: {effect}\")\n                if effect.id == EffectId.RAVAGERCORROSIVEBILECP:\n                    success = True\n            await self._advance_steps(2)\n            # Check if condition is met\n            if len(self.state.effects) != 0:\n                break\n        # Cleanup\n        await self.client.debug_kill_unit(self.units(UnitTypeId.RAVAGER))\n        # Wait for effectts to time out\n        await self._advance_steps(100)\n        logger.warning(\"Action test 07 successful.\")\n\n    async def test_botai_actions8(self):\n        # Test if train function works on hatchery, lair, hive\n        center = self.game_info.map_center\n        if not self.structures(UnitTypeId.HIVE):\n            await self.client.debug_create_unit([(UnitTypeId.HIVE, 1, center, 1)])\n        if not self.structures(UnitTypeId.LAIR):\n            await self.client.debug_create_unit([(UnitTypeId.LAIR, 1, center, 1)])\n        if not self.structures(UnitTypeId.HATCHERY):\n            await self.client.debug_create_unit([(UnitTypeId.HATCHERY, 1, center, 1)])\n        if not self.structures(UnitTypeId.SPAWNINGPOOL):\n            await self.client.debug_create_unit([(UnitTypeId.SPAWNINGPOOL, 1, center, 1)])\n\n        while True:\n            townhalls = self.structures.of_type({UnitTypeId.HIVE, UnitTypeId.LAIR, UnitTypeId.HATCHERY})\n            if townhalls.amount == 3 and self.minerals >= 450 and not self.already_pending(UnitTypeId.QUEEN):\n                self.train(UnitTypeId.QUEEN, amount=3)\n                # Equivalent to:\n                # for townhall in townhalls:\n                #     townhall.train(UnitTypeId.QUEEN)\n            await self._advance_steps(20)\n            # Check if condition is met\n            if self.already_pending(UnitTypeId.QUEEN) == 3:\n                break\n\n        # Cleanup\n        townhalls = self.structures.of_type({UnitTypeId.HIVE, UnitTypeId.LAIR, UnitTypeId.HATCHERY})\n        queens = self.units(UnitTypeId.QUEEN)\n        pool = self.structures(UnitTypeId.SPAWNINGPOOL)\n        await self.client.debug_kill_unit(townhalls | queens | pool)\n        await self._advance_steps(2)\n        logger.warning(\"Action test 08 successful.\")\n\n    async def test_botai_actions9(self):\n        # Morph an archon from 2 high templars\n        center = self.game_info.map_center\n        await self.client.debug_create_unit(\n            [\n                (UnitTypeId.HIGHTEMPLAR, 1, center, 1),\n                (UnitTypeId.DARKTEMPLAR, 1, center + Point2((5, 0)), 1),\n            ]\n        )\n        await self._advance_steps(4)\n        assert self.already_pending(UnitTypeId.ARCHON) == 0\n\n        while True:\n            for templar in self.units.of_type({UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR}):\n                templar(AbilityId.MORPH_ARCHON)\n\n            await self._advance_steps(4)\n\n            templars = self.units.of_type({UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR})\n            archons = self.units(UnitTypeId.ARCHON)\n            if templars.amount > 0:\n                # High templars are on their way to morph ot morph has started\n                assert self.already_pending(UnitTypeId.ARCHON) == 1\n            else:\n                # Morph started\n                assert self.already_pending(UnitTypeId.ARCHON) == archons.not_ready.amount\n\n            # Check if condition is met\n            if archons.ready.amount == 1:\n                assert templars.amount == 0\n                assert self.already_pending(UnitTypeId.ARCHON) == 0\n                break\n\n        # Cleanup\n        if archons:\n            await self.client.debug_kill_unit(archons)\n        if templars:\n            await self.client.debug_kill_unit(templars)\n        await self._advance_steps(2)\n        logger.warning(\"Action test 09 successful.\")\n\n    async def test_botai_actions10(self):\n        # Morph 400 banelings from 400 lings in the same frame\n        center = self.game_info.map_center\n\n        target_amount = 400\n        while True:\n            bane_nests = self.structures(UnitTypeId.BANELINGNEST)\n            lings = self.units(UnitTypeId.ZERGLING)\n            banes = self.units(UnitTypeId.BANELING)\n            bane_cocoons = self.units(UnitTypeId.BANELINGCOCOON)\n\n            # Cheat money, need 10k/10k to morph 400 lings to 400 banes\n            if not banes and not bane_cocoons and (self.minerals < 10_000 or self.vespene < 10_000):\n                await self.client.debug_all_resources()\n\n            # Spawn units\n            if not bane_nests:\n                await self.client.debug_create_unit([(UnitTypeId.BANELINGNEST, 1, center, 1)])\n            current_amount = banes.amount + bane_cocoons.amount + lings.amount\n            if current_amount < target_amount:\n                await self.client.debug_create_unit([(UnitTypeId.ZERGLING, target_amount - current_amount, center, 1)])\n\n            if lings.amount >= target_amount and self.minerals >= 10_000 and self.vespene >= 10_000:\n                for ling in lings:\n                    ling.train(UnitTypeId.BANELING)\n            await self._advance_steps(20)\n\n            # Check if condition is met\n            bane_nests = self.structures(UnitTypeId.BANELINGNEST)\n            lings = self.units(UnitTypeId.ZERGLING)\n            banes = self.units(UnitTypeId.BANELING)\n            bane_cocoons = self.units(UnitTypeId.BANELINGCOCOON)\n            if banes.amount >= target_amount:\n                break\n\n        # Cleanup\n        await self.client.debug_kill_unit(lings | banes | bane_nests | bane_cocoons)\n        await self._advance_steps(2)\n        logger.warning(\"Action test 10 successful.\")\n\n    async def test_botai_actions11(self):\n        # Trigger anti armor missile of raven against enemy unit and check if buff was received\n        await self.clean_up_center()\n        await self.clean_up_center()\n\n        map_center = self.game_info.map_center\n\n        while not self.units(UnitTypeId.RAVEN):\n            await self.client.debug_create_unit([(UnitTypeId.RAVEN, 1, map_center, 1)])\n            await self._advance_steps(2)\n\n        while not self.enemy_units(UnitTypeId.INFESTOR):\n            await self.client.debug_create_unit([(UnitTypeId.INFESTOR, 1, map_center, 2)])\n            await self._advance_steps(2)\n\n        raven = self.units(UnitTypeId.RAVEN)[0]\n        # Set raven energy to max\n        await self.client.debug_set_unit_value(raven, 1, 200)\n        await self._advance_steps(4)\n\n        enemy = self.enemy_units(UnitTypeId.INFESTOR)[0]\n        while True:\n            raven = self.units(UnitTypeId.RAVEN)[0]\n            raven(AbilityId.EFFECT_ANTIARMORMISSILE, enemy)\n            await self._advance_steps(2)\n            enemy = self.enemy_units(UnitTypeId.INFESTOR)[0]\n            if enemy.buffs:\n                # logger.info(enemy.buffs, enemy.buff_duration_remain, enemy.buff_duration_max)\n                break\n\n        logger.warning(\"Action test 11 successful.\")\n        await self.clean_up_center()\n\n    async def test_botai_actions12(self):\n        # Test if structures_without_construction_SCVs works after killing the scv\n        # Wait till can afford depot\n        while not self.can_afford(UnitTypeId.SUPPLYDEPOT):\n            await self.client.debug_all_resources()\n            await self._advance_steps(2)\n\n        while True:\n            # Once depot is under construction: debug kill scv -> advance simulation: should now match the test case\n            if self.structures(UnitTypeId.SUPPLYDEPOT).not_ready.amount == 1:\n                construction_scvs: Units = self.workers.filter(lambda worker: worker.is_constructing_scv)\n                if construction_scvs:\n                    await self.client.debug_kill_unit(construction_scvs)\n                    await self._advance_steps(8)\n                    await self._advance_steps(8)\n\n                    # Test case\n                    assert not self.workers.filter(lambda worker: worker.is_constructing_scv)\n                    assert self.structures_without_construction_SCVs.amount >= 1\n                    break\n\n            if not self.already_pending(UnitTypeId.SUPPLYDEPOT):\n                # Pick scv\n                scv: Unit = self.workers.random\n                # Pick location to build depot on\n                placement_position: Point2 | None = await self.find_placement(\n                    UnitTypeId.SUPPLYDEPOT, near=self.townhalls.random.position\n                )\n                if placement_position:\n                    scv.build(UnitTypeId.SUPPLYDEPOT, placement_position)\n            await self._advance_steps(2)\n\n        logger.warning(\"Action test 12 successful.\")\n        await self.clean_up_center()\n\n    # TODO:\n    # self.can_cast function\n    # Test client.py debug functions\n    # Test if events work (upgrade complete, unit complete, building complete, building started)\n    # Test if functions with various combinations works (e.g. already_pending)\n    # Test self.train function on: larva, hatchery + lair (queens), 2 barracks (2 marines), 2 nexus (probes) (best: every building)\n    # Test unit range and (base attack damage) and other unit stats (e.g. acceleration, deceleration, movement speed (on, off creep), turn speed\n    # Test if dicts are correct for unit_trained_from.py -> train all units once\n\n\nclass EmptyBot(BotAI):\n    async def on_start(self):\n        if self.units:\n            await self.client.debug_kill_unit(self.units)\n\n    async def on_step(self, iteration: int):\n        map_center = self.game_info.map_center\n        enemies = self.enemy_units | self.enemy_structures\n        if enemies:\n            enemies = enemies.closer_than(20, map_center)\n        if enemies:\n            # If attacker is visible: move command to attacker but try to not attack\n            for unit in self.units:\n                unit.move(enemies.closest_to(unit).position)\n        else:\n            # If attacker is invisible: dont move\n            for unit in self.units:\n                unit.hold_position()\n\n\ndef main():\n    run_game(maps.get(\"Acropolis\"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/battery_overcharge_bot.py",
    "content": "\"\"\"\nThis bot tests if battery overcharge crashes the bot.\n\"\"\"\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\n\n\nclass BatteryOverchargeBot(BotAI):\n    async def on_start(self):\n        \"\"\"Spawn requires structures.\"\"\"\n        await self.client.debug_create_unit(\n            [\n                (UnitTypeId.PYLON, 1, self.start_location.towards(self.game_info.map_center, 5), 1),\n                (UnitTypeId.SHIELDBATTERY, 1, self.start_location.towards(self.game_info.map_center, 5), 1),\n                (UnitTypeId.CYBERNETICSCORE, 1, self.start_location.towards(self.game_info.map_center, 5), 1),\n            ]\n        )\n\n    async def on_step(self, iteration):\n        if iteration > 10:\n            # Cast battery overcharge\n            nexi = self.structures(UnitTypeId.NEXUS)\n            batteries = self.structures(UnitTypeId.SHIELDBATTERY)\n            for nexus in nexi:\n                for battery in batteries:\n                    if nexus.energy >= 50:\n                        nexus(AbilityId.BATTERYOVERCHARGE_BATTERYOVERCHARGE, battery)\n\n        if iteration > 20:\n            logger.warning(\"Success, bot did not crash. Exiting bot.\")\n            await self.client.leave()\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Protoss, BatteryOverchargeBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=False,\n        disable_fog=True,\n    )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/benchmark_array_creation.py",
    "content": "\"\"\"\nTesting what the fastest way is to create a 1D Array with 2 values\n\"\"\"\n\nimport random\n\nimport numpy as np\n\nx, y = random.uniform(0, 300), random.uniform(0, 300)\n\n\ndef numpy_array(x, y):\n    # Calculate distances between each of the points\n    return np.array((x, y), dtype=float)\n\n\ndef numpy_array_tuple(my_tuple):\n    # Calculate distances between each of the points\n    return np.array(my_tuple, dtype=float)\n\n\ndef numpy_asarray(x, y):\n    # Calculate distances between each of the points\n    return np.asarray((x, y), dtype=float)\n\n\ndef numpy_asarray_tuple(my_tuple):\n    # Calculate distances between each of the points\n    return np.asarray(my_tuple, dtype=float)\n\n\ndef numpy_asanyarray(x, y):\n    # Calculate distances between each of the points\n    return np.asanyarray((x, y), dtype=float)\n\n\ndef numpy_asanyarray_tuple(my_tuple):\n    # Calculate distances between each of the points\n    return np.asanyarray(my_tuple, dtype=float)\n\n\ndef numpy_fromiter(x, y):\n    # Calculate distances between each of the points\n    return np.fromiter((x, y), dtype=float, count=2)\n\n\ndef numpy_fromiter_tuple(my_tuple):\n    # Calculate distances between each of the points\n    return np.fromiter(my_tuple, dtype=float, count=2)\n\n\ndef numpy_fromiter_np_float(x, y):\n    # Calculate distances between each of the points\n    return np.fromiter((x, y), dtype=float, count=2)\n\n\ndef numpy_fromiter_np_float_tuple(my_tuple):\n    # Calculate distances between each of the points\n    return np.fromiter(my_tuple, dtype=float, count=2)\n\n\ndef numpy_zeros(x, y):\n    # Calculate distances between each of the points\n    a = np.zeros(2, dtype=float)\n    a[0] = x\n    a[1] = y\n    return a\n\n\ndef numpy_ones(x, y):\n    # Calculate distances between each of the points\n    a = np.ones(2, dtype=float)\n    a[0] = x\n    a[1] = y\n    return a\n\n\nnumpy_array(x, y)\ncorrect_array = np.array([x, y])\n\n\ndef test_numpy_array(benchmark):\n    result = benchmark(numpy_array, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_array_tuple(benchmark):\n    result = benchmark(numpy_array_tuple, (x, y))\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_asarray(benchmark):\n    result = benchmark(numpy_asarray, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_asarray_tuple(benchmark):\n    result = benchmark(numpy_asarray_tuple, (x, y))\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_asanyarray(benchmark):\n    result = benchmark(numpy_asanyarray, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_asanyarray_tuple(benchmark):\n    result = benchmark(numpy_asanyarray_tuple, (x, y))\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_fromiter(benchmark):\n    result = benchmark(numpy_fromiter, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_fromiter_tuple(benchmark):\n    result = benchmark(numpy_fromiter_tuple, (x, y))\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_fromiter_np_float(benchmark):\n    result = benchmark(numpy_fromiter_np_float, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_fromiter_np_float_tuple(benchmark):\n    result = benchmark(numpy_fromiter_np_float_tuple, (x, y))\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_zeros(benchmark):\n    result = benchmark(numpy_zeros, x, y)\n    assert np.array_equal(result, correct_array)\n\n\ndef test_numpy_ones(benchmark):\n    result = benchmark(numpy_ones, x, y)\n    assert np.array_equal(result, correct_array)\n\n\n# Run this file using\n# uv run pytest test/test_benchmark_array_creation.py --benchmark-compare\n"
  },
  {
    "path": "test/benchmark_bot_ai_init.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom test.test_pickled_data import MAPS, build_bot_object_from_pickle_data, load_map_pickle_data\n\n\ndef _test_run_bot_ai_init_on_all_maps(pickle_data: list[tuple[Any, Any, Any]]):\n    for data in pickle_data:\n        build_bot_object_from_pickle_data(*data)\n\n\ndef test_bench_bot_ai_init(benchmark):\n    # Load pickle files outside of benchmark\n    map_pickle_data: list[tuple[Any, Any, Any]] = [load_map_pickle_data(path) for path in MAPS]\n    _result = benchmark(_test_run_bot_ai_init_on_all_maps, map_pickle_data)\n\n\n# Run this file using\n# uv run pytest test/benchmark_bot_ai_init.py --benchmark-compare --benchmark-min-rounds=5\n"
  },
  {
    "path": "test/benchmark_distance_two_points.py",
    "content": "from __future__ import annotations\n\nimport math\nimport platform\nimport random\n\nimport numpy as np\nfrom scipy.spatial import distance as scipydistance\n\n# from numba import jit, njit, vectorize, float64, int64\nfrom sc2.position import Point2\n\nPYTHON_VERSION = platform.python_version_tuple()\nUSING_PYTHON_3_8: bool = PYTHON_VERSION >= (\"3\", \"8\")\n\n\ndef distance_to_python_raw(s, p):\n    return ((s[0] - p[0]) ** 2 + (s[1] - p[1]) ** 2) ** 0.5\n\n\ndef distance_to_squared_python_raw(s, p):\n    return (s[0] - p[0]) ** 2 + (s[1] - p[1]) ** 2\n\n\nif USING_PYTHON_3_8:\n\n    def distance_to_math_dist(s, p):\n        return math.dist(s, p)\n\n\ndef distance_to_math_hypot(s, p):\n    return math.hypot((s[0] - p[0]), (s[1] - p[1]))\n\n\ndef distance_scipy_euclidean(p1, p2) -> int | float:\n    \"\"\"Distance calculation using scipy\"\"\"\n    dist = scipydistance.euclidean(p1, p2)\n    # dist = distance.cdist(p1.T, p2.T, \"euclidean\")\n    return dist\n\n\ndef distance_numpy_linalg_norm(p1, p2):\n    \"\"\"Distance calculation using numpy\"\"\"\n    return np.linalg.norm(p1 - p2)\n\n\ndef distance_sum_squared_sqrt(p1, p2) -> int | float:\n    \"\"\"Distance calculation using numpy\"\"\"\n    return np.sqrt(np.sum((p1 - p2) ** 2))  # pyrefly: ignore\n\n\ndef distance_sum_squared(p1, p2) -> int | float:\n    \"\"\"Distance calculation using numpy\"\"\"\n    return np.sum((p1 - p2) ** 2, axis=0)\n\n\n# @njit\n# def distance_python_raw_njit(p1: Point2, p2: Point2) -> int | float:\n#     \"\"\" The built in Point2 distance function rewritten differently with njit, same structure as distance02 \"\"\"\n#     return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5\n\n# @njit\n# def distance_python_raw_square_njit(p1: Point2, p2: Point2) -> int | float:\n#     \"\"\" The built in Point2 distance function rewritten differently with njit, same structure as distance02 \"\"\"\n#     return (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2\n\n# @njit(\"float64(float64[:], float64[:])\")\n# def distance_numpy_linalg_norm_njit(p1, p2):\n#     \"\"\" Distance calculation using numpy + numba, same structure as distance12 \"\"\"\n#     return np.linalg.norm(p1 - p2)\n\n# @njit(\"float64(float64[:], float64[:])\")\n# def distance_numpy_square_sum_sqrt_njit(p1, p2) -> int | float:\n#     \"\"\" Distance calculation using numpy + numba, same structure as distance13 \"\"\"\n#     return np.sqrt(np.sum((p1 - p2) ** 2))\n\n# @njit(\"float64(float64[:], float64[:])\")\n# def distance_numpy_square_sum_njit(p1, p2) -> int | float:\n#     \"\"\" Distance calculation using numpy + numba, same structure as distance13 \"\"\"\n#     return np.sum((p1 - p2) ** 2, axis=0)\n\n# Points as Point2 object\np1 = Point2((random.uniform(0, 300), random.uniform(0, 300)))\np2 = Point2((random.uniform(0, 300), random.uniform(0, 300)))\n# Points as numpy array to get most accuracy if all points do not need to be converted before calculation\np1_np = np.asarray(p1)\np2_np = np.asarray(p2)\n\n# Correct result to ensure that in the functions the correct result is calculated\ncorrect_result = distance_to_math_hypot(p1, p2)\n\n\ndef check_result(result1, result2, accuracy=1e-5):\n    return abs(result1 - result2) <= accuracy\n\n\nif USING_PYTHON_3_8:\n\n    def test_distance_to_math_dist(benchmark):\n        result = benchmark(distance_to_math_dist, p1, p2)\n        assert check_result(result, correct_result)\n\n\ndef test_distance_to_math_hypot(benchmark):\n    result = benchmark(distance_to_math_hypot, p1, p2)\n    assert check_result(result, correct_result)\n\n\ndef test_distance_to_python_raw(benchmark):\n    result = benchmark(distance_to_python_raw, p1, p2)\n    assert check_result(result, correct_result)\n\n\ndef test_distance_to_squared_python_raw(benchmark):\n    result = benchmark(distance_to_squared_python_raw, p1, p2)\n    assert check_result(result, correct_result**2)\n\n\ndef test_distance_scipy_euclidean(benchmark):\n    result = benchmark(distance_scipy_euclidean, p1_np, p2_np)\n    assert check_result(result, correct_result)\n\n\ndef test_distance_numpy_linalg_norm(benchmark):\n    result = benchmark(distance_numpy_linalg_norm, p1_np, p2_np)\n    assert check_result(result, correct_result)\n\n\ndef test_distance_sum_squared_sqrt(benchmark):\n    result = benchmark(distance_sum_squared_sqrt, p1_np, p2_np)\n    assert check_result(result, correct_result)\n\n\ndef test_distance_sum_squared(benchmark):\n    result = benchmark(distance_sum_squared, p1_np, p2_np)\n    assert check_result(result, correct_result**2)\n\n\n# def test_distance_python_raw_njit(benchmark):\n#     result = benchmark(distance_python_raw_njit, p1_np, p2_np)\n#     assert check_result(result, correct_result)\n\n# def test_distance_python_raw_square_njit(benchmark):\n#     result = benchmark(distance_python_raw_square_njit, p1_np, p2_np)\n#     assert check_result(re`sult, correct_result ** 2)\n#\n#\n# def test_distance_numpy_linalg_norm_njit(benchmark):\n#     result = benchmark(distance_numpy_linalg_norm_njit, p1_np, p2_np)\n#     assert check_result(result, correct_result)\n#\n#\n# def test_distance_numpy_square_sum_sqrt_njit(benchmark):\n#     result = benchmark(distance_numpy_square_sum_sqrt_njit, p1_np, p2_np)\n#     assert check_result(result, correct_result)\n#\n#\n# def test_distance_numpy_square_sum_njit(benchmark):\n#     result = benchmark(distance_numpy_square_sum_njit, p1_np, p2_np)\n#     assert check_result(result, correct_result ** 2)\n\n# Run this file using\n# uv run pytest test/test_benchmark_distance_two_points.py --benchmark-compare\n"
  },
  {
    "path": "test/benchmark_distances_cdist.py",
    "content": "import random\n\nimport numpy as np\nfrom scipy.spatial.distance import cdist, pdist\n\n\ndef distance_matrix_scipy_cdist_braycurtis(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"braycurtis\")\n\n\ndef distance_matrix_scipy_cdist_canberra(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"canberra\")\n\n\ndef distance_matrix_scipy_cdist_chebyshev(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"chebyshev\")\n\n\ndef distance_matrix_scipy_cdist_cityblock(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"cityblock\")\n\n\ndef distance_matrix_scipy_cdist_correlation(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"correlation\")\n\n\ndef distance_matrix_scipy_cdist_cosine(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"cosine\")\n\n\ndef distance_matrix_scipy_cdist_hamming(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"hamming\")\n\n\ndef distance_matrix_scipy_cdist_jaccard(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"jaccard\")\n\n\ndef distance_matrix_scipy_cdist_jensenshannon(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"jensenshannon\")\n\n\ndef distance_matrix_scipy_cdist_mahalanobis(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"mahalanobis\")\n\n\ndef distance_matrix_scipy_cdist_matching(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"matching\")  # pyrefly: ignore\n\n\n# def distance_matrix_scipy_cdist_minkowski(ps):\n#     # Calculate distances between each of the points\n#     return cdist(ps, ps, \"minkowski\")\n\n\ndef distance_matrix_scipy_cdist_rogerstanimoto(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"rogerstanimoto\")\n\n\ndef distance_matrix_scipy_cdist_russellrao(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"russellrao\")\n\n\ndef distance_matrix_scipy_cdist_seuclidean(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"seuclidean\")\n\n\ndef distance_matrix_scipy_cdist_sokalmichener(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"sokalmichener\")\n\n\ndef distance_matrix_scipy_cdist_sokalsneath(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"sokalsneath\")\n\n\n# def distance_matrix_scipy_cdist_wminkowski(ps):\n#     # Calculate distances between each of the points\n#     return cdist(ps, ps, \"wminkowski\")\n\n\ndef distance_matrix_scipy_cdist_yule(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"yule\")\n\n\ndef distance_matrix_scipy_cdist(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"euclidean\")\n\n\ndef distance_matrix_scipy_pdist(ps):\n    # Calculate distances between each of the points\n    return pdist(ps, \"euclidean\")\n\n\ndef distance_matrix_scipy_cdist_squared(ps):\n    # Calculate squared distances between each of the points\n    return cdist(ps, ps, \"sqeuclidean\")\n\n\ndef distance_matrix_scipy_pdist_squared(ps):\n    # Calculate squared distances between each of the points\n    return pdist(ps, \"sqeuclidean\")\n\n\n# Points as numpy arrays\namount = 200\nmin_value = 0\nmax_value = 300\npoints = np.array(\n    [np.array([random.uniform(min_value, max_value), random.uniform(min_value, max_value)]) for _ in range(amount)]\n)\n\n\ndef test_distance_matrix_scipy_cdist_braycurtis(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_braycurtis, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_canberra(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_canberra, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_chebyshev(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_chebyshev, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_cityblock(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_cityblock, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_correlation(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_correlation, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_cosine(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_cosine, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_hamming(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_hamming, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_jaccard(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_jaccard, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_jensenshannon(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_jensenshannon, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_mahalanobis(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_mahalanobis, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_matching(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_matching, points)\n    # assert check_result(result, correct_result)\n\n\n# def test_distance_matrix_scipy_cdist_minkowski(benchmark):\n#     result = benchmark(distance_matrix_scipy_cdist_minkowski, points)\n#     # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_rogerstanimoto(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_rogerstanimoto, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_russellrao(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_russellrao, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_seuclidean(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_seuclidean, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_sokalmichener(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_sokalmichener, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_sokalsneath(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_sokalsneath, points)\n    # assert check_result(result, correct_result)\n\n\n# def test_distance_matrix_scipy_cdist_wminkowski(benchmark):\n#     result = benchmark(distance_matrix_scipy_cdist_wminkowski, points)\n#     # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_yule(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_yule, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_pdist(benchmark):\n    result = benchmark(distance_matrix_scipy_pdist, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_squared(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_squared, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_pdist_squared(benchmark):\n    result = benchmark(distance_matrix_scipy_pdist_squared, points)\n    # assert check_result(result, correct_result)\n\n\n# Run this file using\n# uv run pytest test/test_benchmark_distances_cdist.py --benchmark-compare\n"
  },
  {
    "path": "test/benchmark_distances_points_to_point.py",
    "content": "from __future__ import annotations\n\nimport math\nimport random\n\nimport numpy as np\nfrom scipy.spatial.distance import cdist\n\n\ndef distance_matrix_scipy_cdist_squared(ps, p1):\n    # Calculate squared distances between multiple points and target point\n    flat_units = (item for sublist in ps for item in sublist)\n    units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2))\n    point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2))\n    return cdist(units_np, point_np, \"sqeuclidean\")\n\n\ndef distance_numpy_basic_1(ps, p1):\n    \"\"\"Distance calculation using numpy\"\"\"\n    flat_units = (item for sublist in ps for item in sublist)\n    units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2))\n    point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2))\n    # Subtract and then square the values\n    nppoints = (units_np - point_np) ** 2\n    # Calc the sum of each vector\n    nppoints = nppoints.sum(axis=1)\n    return nppoints\n\n\ndef distance_numpy_basic_2(ps, p1):\n    \"\"\"Distance calculation using numpy\"\"\"\n    flat_units = (item for sublist in ps for item in sublist)\n    units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2))\n    point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2))\n    dist_2 = np.sum((units_np - point_np) ** 2, axis=1)\n    return dist_2\n\n\ndef distance_numpy_einsum(ps, p1):\n    \"\"\"Distance calculation using numpy einstein sum\"\"\"\n    flat_units = (item for sublist in ps for item in sublist)\n    units_np = np.fromiter(flat_units, dtype=float, count=2 * len(ps)).reshape((-1, 2))\n    point_np = np.fromiter(p1, dtype=float, count=2).reshape((-1, 2))\n    deltas = units_np - point_np\n    dist_2 = np.einsum(\"ij,ij->i\", deltas, deltas)\n    return dist_2\n\n\ndef distance_numpy_einsum_pre_converted(ps, p1):\n    \"\"\"Distance calculation using numpy einstein sum\"\"\"\n    deltas = ps - p1\n    dist_2 = np.einsum(\"ij,ij->i\", deltas, deltas)\n    return dist_2\n\n\n# @njit(\"float64[:](float64[:, :], float64[:, :])\")\n# def distance_numpy_basic_1_numba(ps, p1):\n#     \"\"\" Distance calculation using numpy with njit \"\"\"\n#     # Subtract and then square the values\n#     nppoints = (ps - p1) ** 2\n#     # Calc the sum of each vector\n#     nppoints = nppoints.sum(axis=1)\n#     return nppoints\n\n# @njit(\"float64[:](float64[:, :], float64[:, :])\")\n# def distance_numpy_basic_2_numba(ps, p1):\n#     \"\"\" Distance calculation using numpy with njit \"\"\"\n#     distances = np.sum((ps - p1) ** 2, axis=1)\n#     return distances\n\n# # @njit(\"float64[:](float64[:], float64[:])\")\n# @jit(nopython=True)\n# def distance_numba(ps, p1, amount):\n#     \"\"\" Distance calculation using numpy with jit(nopython=True) \"\"\"\n#     distances = []\n#     x1 = p1[0]\n#     y1 = p1[1]\n#     for index in range(amount):\n#         x0 = ps[2 * index]\n#         y0 = ps[2 * index + 1]\n#         distance_squared = (x0 - x1) ** 2 + (y0 - y1) ** 2\n#         distances.append(distance_squared)\n#     return distances\n\n\ndef distance_pure_python(ps, p1):\n    \"\"\"Distance calculation using numpy with jit(nopython=True)\"\"\"\n    distances = []\n    x1 = p1[0]\n    y1 = p1[1]\n    for x0, y0 in ps:\n        distance_squared = (x0 - x1) ** 2 + (y0 - y1) ** 2\n        distances.append(distance_squared)\n    return distances\n\n\ndef distance_math_hypot(ps, p1):\n    \"\"\"Distance calculation using math.hypot\"\"\"\n    distances = []\n    x1 = p1[0]\n    y1 = p1[1]\n    # for x0, y0 in ps:\n    #     distance = math.hypot(x0 - x1, y0 - y1)\n    #     distances.append(distance)\n    # return distances\n    return [math.hypot(x0 - x1, y0 - y1) for x0, y0 in ps]\n\n\n# Points as numpy arrays\namount = 50\nmin_value = 0\nmax_value = 250\n\npoint: tuple[float, float] = (random.uniform(min_value, max_value), random.uniform(min_value, max_value))\nunits: list[tuple[float, float]] = [\n    (random.uniform(min_value, max_value), random.uniform(min_value, max_value)) for _ in range(amount)\n]\n\n# Pre convert points to numpy array\nflat_units = [item for sublist in units for item in sublist]\nunits_np = np.fromiter(flat_units, dtype=float, count=2 * len(units)).reshape((-1, 2))\npoint_np = np.fromiter(point, dtype=float, count=2).reshape((-1, 2))\n\nr1 = distance_matrix_scipy_cdist_squared(units, point).flatten()\nr2 = distance_numpy_basic_1(units, point)\nr3 = distance_numpy_basic_2(units, point)\nr4 = distance_numpy_einsum(units, point)\n\nassert np.array_equal(r1, r2)\nassert np.array_equal(r1, r3)\nassert np.array_equal(r1, r4)\n\n\ndef test_distance_matrix_scipy_cdist_squared(benchmark):\n    result = benchmark(distance_matrix_scipy_cdist_squared, units, point)\n\n\ndef test_distance_numpy_basic_1(benchmark):\n    result = benchmark(distance_numpy_basic_1, units, point)\n\n\ndef test_distance_numpy_basic_2(benchmark):\n    result = benchmark(distance_numpy_basic_2, units, point)\n\n\ndef test_distance_numpy_einsum(benchmark):\n    result = benchmark(distance_numpy_einsum, units, point)\n\n\ndef test_distance_numpy_einsum_pre_converted(benchmark):\n    result = benchmark(distance_numpy_einsum_pre_converted, units_np, point_np)\n\n\n# def test_distance_numpy_basic_1_numba(benchmark):\n#     result = benchmark(distance_numpy_basic_1_numba, units_np, point_np)\n\n# def test_distance_numpy_basic_2_numba(benchmark):\n#     result = benchmark(distance_numpy_basic_2_numba, units_np, point_np)\n\n# def test_distance_numba(benchmark):\n#     result = benchmark(distance_numba, flat_units, point, len(flat_units) // 2)\n\n\ndef test_distance_pure_python(benchmark):\n    result = benchmark(distance_pure_python, units, point)\n\n\ndef test_distance_math_hypot(benchmark):\n    result = benchmark(distance_math_hypot, units, point)\n\n\n# Run this file using\n# uv run pytest test/test_benchmark_distances_points_to_point.py --benchmark-compare\n"
  },
  {
    "path": "test/benchmark_distances_units.py",
    "content": "import math\nimport random\n\nimport numpy as np\nfrom scipy.spatial.distance import cdist, pdist\n\n\ndef distance_matrix_scipy_cdist(ps):\n    # Calculate distances between each of the points\n    return cdist(ps, ps, \"euclidean\")\n\n\ndef distance_matrix_scipy_pdist(ps):\n    # Calculate distances between each of the points\n    return pdist(ps, \"euclidean\")\n\n\ndef distance_matrix_scipy_cdist_squared(ps):\n    # Calculate squared distances between each of the points\n    return cdist(ps, ps, \"sqeuclidean\")\n\n\ndef distance_matrix_scipy_pdist_squared(ps):\n    # Calculate squared distances between each of the points\n    return pdist(ps, \"sqeuclidean\")\n\n\n# Points as numpy arrays\namount = 200\nmin_value = 0\nmax_value = 250\npoints = np.array(\n    [np.array([random.uniform(min_value, max_value), random.uniform(min_value, max_value)]) for _ in range(amount)]\n)\n\nm1 = distance_matrix_scipy_cdist(points)\nm2 = distance_matrix_scipy_pdist(points)\nms1 = distance_matrix_scipy_cdist_squared(points)\nms2 = distance_matrix_scipy_pdist_squared(points)\n\n\ndef calc_row_idx(k, n):\n    return int(math.ceil((1 / 2.0) * (-((-8 * k + 4 * n**2 - 4 * n - 7) ** 0.5) + 2 * n - 1) - 1))\n\n\ndef elem_in_i_rows(i, n):\n    return i * (n - 1 - i) + (i * (i + 1)) // 2\n\n\ndef calc_col_idx(k, i, n):\n    return int(n - elem_in_i_rows(i + 1, n) + k)\n\n\ndef condensed_to_square(k, n):\n    i = calc_row_idx(k, n)\n    j = calc_col_idx(k, i, n)\n    return i, j\n\n\ndef square_to_condensed(i, j, amount):\n    # Converts indices of a square matrix to condensed matrix\n    # 'amount' is the number of points that were used to calculate the distances\n    # https://stackoverflow.com/a/36867493/10882657\n    assert i != j, \"No diagonal elements in condensed matrix! Diagonal elements are zero\"\n    if i < j:\n        i, j = j, i\n    return amount * j - j * (j + 1) // 2 + i - 1 - j\n\n\n# Test if distance in cdist is same as in pdist, and that the indices function is correct\nindices = set()\nfor i1 in range(amount):\n    for i2 in range(amount):\n        if i1 == i2:\n            # Diagonal entries are zero\n            continue\n        # m1: cdist square matrix\n        v1 = m1[i1, i2]\n        # m2: pdist condensed matrix vector\n        index = square_to_condensed(i1, i2, amount)\n\n        indices.add(index)\n        v2 = m2[index]\n\n        # Test if convert indices functions work\n        j1, j2 = condensed_to_square(index, amount)\n        # Swap if first is bigger than 2nd\n        assert j1 == i1 and j2 == i2 or j2 == i1 and j2 == i1, f\"{j1} == {i1} and {j2} == {i2}\"\n\n        # Assert if the values of cdist is the same as the value of pdist\n        assert v1 == v2, f\"m1[i1, i2] is {v1}, m2[index] is {v2}\"\n# Test that all indices were generated using the for loop above\nassert max(indices) == len(m2) - 1\nassert min(indices) == 0\nassert len(indices) == len(m2), f\"{len(indices)} == {len(m2)}\"\n\n\ndef test_distance_matrix_scipy_cdist(benchmark):\n    _result = benchmark(distance_matrix_scipy_cdist, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_pdist(benchmark):\n    _result = benchmark(distance_matrix_scipy_pdist, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_cdist_squared(benchmark):\n    _result = benchmark(distance_matrix_scipy_cdist_squared, points)\n    # assert check_result(result, correct_result)\n\n\ndef test_distance_matrix_scipy_pdist_squared(benchmark):\n    _result = benchmark(distance_matrix_scipy_pdist_squared, points)\n    # assert check_result(result, correct_result)\n\n\n# Run this file using\n# uv run pytest test/test_benchmark_distances_units.py --benchmark-compare\n"
  },
  {
    "path": "test/benchmark_prepare_units.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom test.test_pickled_data import MAPS, get_map_specific_bot\n\nif TYPE_CHECKING:\n    from sc2.bot_ai import BotAI\n\n\ndef _run_prepare_units(bot_objects: list[BotAI]):\n    for bot_object in bot_objects:\n        bot_object._prepare_units()\n\n\ndef test_bench_prepare_units(benchmark):\n    bot_objects = [get_map_specific_bot(map_) for map_ in MAPS]\n    _result = benchmark(_run_prepare_units, bot_objects)\n\n\n# Run this file using\n# uv run pytest test/benchmark_prepare_units.py --benchmark-compare\n"
  },
  {
    "path": "test/damagetest_bot.py",
    "content": "from __future__ import annotations\n\nimport math\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot\nfrom sc2.unit import Unit\n\n\nclass TestBot(BotAI):\n    def __init__(self):\n        # The time the bot has to complete all tests, here: the number of game seconds\n        self.game_time_timeout_limit = 20 * 60  # 20 minutes ingame time\n\n        # Check how many test action functions we have\n        # At least 4 tests because we test properties and variables\n        self.action_tests = [\n            getattr(self, f\"test_botai_actions{index}\")\n            for index in range(4000)\n            if hasattr(getattr(self, f\"test_botai_actions{index}\", 0), \"__call__\")\n        ]\n        self.tests_target = 4\n        self.tests_done_by_name = set()\n\n        # Keep track of the action index and when the last action was started\n        self.current_action_index = 1\n        self.iteration_last_action_started = 8\n        # There will be 20 iterations of the bot doing nothing between tests\n        self.iteration_wait_time_between_actions = 20\n\n        self.scv_action_list = [\"move\", \"patrol\", \"attack\", \"hold\", \"scan_move\"]\n\n        # Variables for test_botai_actions11\n\n    async def on_start(self):\n        self.client.game_step = 8\n        # await self.client.quick_save()\n        await self.distribute_workers()\n\n    async def on_step(self, iteration):\n        # Test actions\n        if iteration == 7:\n            for action_test in self.action_tests:\n                await action_test()\n\n        # Exit bot\n        if iteration > 100:\n            logger.info(f\"Tests completed after {round(self.time, 1)} seconds\")\n            exit(0)\n\n    async def clean_up_center(self):\n        map_center = self.game_info.map_center\n        # Remove everything close to map center\n        my_units = self.units | self.structures\n        if my_units:\n            my_units = my_units.closer_than(20, map_center)\n        if my_units:\n            await self.client.debug_kill_unit(my_units)\n        enemy_units = self.enemy_units | self.enemy_structures\n        if enemy_units:\n            enemy_units = enemy_units.closer_than(20, map_center)\n        if enemy_units:\n            await self.client.debug_kill_unit(enemy_units)\n        await self._advance_steps(2)\n\n    # Create a lot of units and check if their damage calculation is correct based on Unit.calculate_damage_vs_target()\n    async def test_botai_actions1001(self):\n        upgrade_levels = {0, 1}\n        attacker_units = [\n            #\n            # Protoss\n            #\n            UnitTypeId.PROBE,\n            # UnitTypeId.ZEALOT,\n            UnitTypeId.ADEPT,\n            UnitTypeId.STALKER,\n            UnitTypeId.HIGHTEMPLAR,\n            UnitTypeId.DARKTEMPLAR,\n            UnitTypeId.ARCHON,  # Doesnt work vs workers when attacklevel > 1\n            UnitTypeId.IMMORTAL,\n            UnitTypeId.COLOSSUS,\n            UnitTypeId.PHOENIX,\n            UnitTypeId.VOIDRAY,\n            # UnitTypeId.CARRIER, # TODO\n            UnitTypeId.MOTHERSHIP,\n            UnitTypeId.TEMPEST,\n            #\n            # Terran\n            #\n            UnitTypeId.SCV,\n            UnitTypeId.MARINE,\n            UnitTypeId.MARAUDER,\n            UnitTypeId.GHOST,\n            UnitTypeId.HELLION,\n            # UnitTypeId.HELLIONTANK, # Incorrect for light targets because hellbat does not seem to have another weapon vs light specifically in the API\n            # UnitTypeId.CYCLONE, # Seems to lock on as soon as it spawns\n            UnitTypeId.SIEGETANK,\n            UnitTypeId.THOR,\n            # UnitTypeId.THORAP, # TODO uncomment when new version for linux client is released\n            UnitTypeId.BANSHEE,\n            UnitTypeId.VIKINGFIGHTER,\n            UnitTypeId.VIKINGASSAULT,\n            # UnitTypeId.BATTLECRUISER, # Does not work because weapon_cooldown is not displayed in the API\n            #\n            # Zerg\n            #\n            UnitTypeId.DRONE,\n            UnitTypeId.ZERGLING,\n            # UnitTypeId.BANELING, # TODO\n            UnitTypeId.QUEEN,\n            # UnitTypeId.ROACH, # Has bugs that I don't know how to fix\n            UnitTypeId.RAVAGER,\n            # UnitTypeId.HYDRALISK, # TODO\n            # UnitTypeId.LURKERMPBURROWED, # Somehow fails the test\n            # 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\n            UnitTypeId.CORRUPTOR,\n            # UnitTypeId.BROODLORD, # Was unreliable because the broodlings would also attack\n            UnitTypeId.ULTRALISK,\n            # Buildings\n            UnitTypeId.MISSILETURRET,\n            UnitTypeId.SPINECRAWLER,\n            UnitTypeId.SPORECRAWLER,\n            UnitTypeId.PLANETARYFORTRESS,\n        ]\n        defender_units = [\n            # Ideally one of each type: ground and air unit with each armor tage\n            # Ground, no tag\n            UnitTypeId.RAVAGER,\n            # Ground, light\n            UnitTypeId.MULE,\n            # Ground, armored\n            UnitTypeId.MARAUDER,\n            # Ground, biological\n            UnitTypeId.ROACH,\n            # Ground, psionic\n            UnitTypeId.HIGHTEMPLAR,\n            # Ground, mechanical\n            UnitTypeId.STALKER,\n            # Ground, massive\n            # UnitTypeId.ULTRALISK, # Fails vs our zergling\n            # Ground, structure\n            # UnitTypeId.PYLON, # Pylon seems to regenerate 1 shield for no reason\n            UnitTypeId.SUPPLYDEPOT,\n            UnitTypeId.BUNKER,\n            UnitTypeId.MISSILETURRET,\n            # Air, light\n            UnitTypeId.PHOENIX,\n            # Air, armored\n            UnitTypeId.VOIDRAY,\n            # Air, biological\n            UnitTypeId.CORRUPTOR,\n            # Air, psionic\n            UnitTypeId.VIPER,\n            # Air, mechanical\n            UnitTypeId.MEDIVAC,\n            # Air, massive\n            UnitTypeId.BATTLECRUISER,\n            # Air, structure\n            UnitTypeId.BARRACKSFLYING,\n            # Ground and air\n            UnitTypeId.COLOSSUS,\n        ]\n        await self._advance_steps(20)\n        map_center = self.game_info.map_center\n\n        # Show whole map\n        await self.client.debug_show_map()\n\n        def get_attacker_and_defender():\n            my_units = self.units | self.structures\n            enemy_units = self.enemy_units | self.enemy_structures\n            if not my_units or not enemy_units:\n                # logger.info(\"my units:\", my_units)\n                # logger.info(\"enemy units:\",enemy_units)\n                return None, None\n            attacker: Unit = my_units.closest_to(map_center)\n            defender: Unit = enemy_units.closest_to(map_center)\n            return attacker, defender\n\n        def do_some_unit_property_tests(attacker: Unit, defender: Unit):\n            \"\"\"Some tests that are not covered by test_pickled_data.py\"\"\"\n            # TODO move unit unrelated tests elsewhere\n            self.step_time\n            self.units_created\n\n            self.structure_type_build_progress(attacker.type_id)\n            self.structure_type_build_progress(defender.type_id)\n            self.tech_requirement_progress(attacker.type_id)\n            self.tech_requirement_progress(defender.type_id)\n            self.in_map_bounds(attacker.position)\n            self.in_map_bounds(defender.position)\n            self.get_terrain_z_height(attacker.position)\n            self.get_terrain_z_height(defender.position)\n\n            for unit in [attacker, defender]:\n                unit.shield_percentage\n                unit.shield_health_percentage\n                unit.energy_percentage\n                unit.age_in_frames\n                unit.age\n                unit.is_memory\n                unit.is_snapshot\n                unit.cloak\n                unit.is_revealed\n                unit.can_be_attacked\n                unit.buff_duration_remain\n                unit.buff_duration_max\n                unit.order_target\n                unit.is_transforming\n                unit.has_techlab\n                unit.has_reactor\n                unit.add_on_position\n                unit.health_percentage\n                unit.bonus_damage\n                unit.air_dps\n\n            attacker.target_in_range(defender)\n            defender.target_in_range(attacker)\n            attacker.calculate_dps_vs_target(defender)\n            defender.calculate_dps_vs_target(attacker)\n            attacker.is_facing(defender)\n            defender.is_facing(attacker)\n            attacker == defender\n            defender == attacker\n\n        await self.clean_up_center()\n\n        for upgrade_level in upgrade_levels:\n            if upgrade_level != 0:\n                await self.client.debug_upgrade()\n            for attacker_type in attacker_units:\n                for defender_type in defender_units:\n                    # DT, Thor, Tempest one-shots workers, so skip test\n                    if attacker_type in {\n                        UnitTypeId.DARKTEMPLAR,\n                        UnitTypeId.TEMPEST,\n                        UnitTypeId.THOR,\n                        UnitTypeId.THORAP,\n                        UnitTypeId.LIBERATORAG,\n                        UnitTypeId.PLANETARYFORTRESS,\n                        UnitTypeId.ARCHON,\n                    } and defender_type in {UnitTypeId.PROBE, UnitTypeId.DRONE, UnitTypeId.SCV, UnitTypeId.MULE}:\n                        continue\n\n                    # Spawn units\n                    await self.client.debug_create_unit(\n                        [(attacker_type, 1, map_center, 1), (defender_type, 1, map_center, 2)]\n                    )\n                    await self._advance_steps(1)\n\n                    # Wait for units to spawn\n                    attacker, defender = get_attacker_and_defender()\n                    while (\n                        attacker is None\n                        or defender is None\n                        or attacker.type_id != attacker_type\n                        or defender.type_id != defender_type\n                    ):\n                        await self._advance_steps(1)\n                        attacker, defender = get_attacker_and_defender()\n                        # TODO check if shield calculation is correct by setting shield of enemy unit\n                    # logger.info(f\"Attacker: {attacker}, defender: {defender}\")\n                    do_some_unit_property_tests(attacker, defender)\n\n                    # Units have spawned, calculate expected damage\n                    expected_damage: float = attacker.calculate_damage_vs_target(defender)[0]\n                    # If expected damage is zero, it means that the attacker cannot attack the defender: skip test\n                    if expected_damage == 0:\n                        await self.clean_up_center()\n                        continue\n                    # Thor antiground seems buggy sometimes and not reliable in tests, skip it\n                    if attacker_type in {UnitTypeId.THOR, UnitTypeId.THORAP} and not defender.is_flying:\n                        await self.clean_up_center()\n                        continue\n\n                    real_damage = 0\n                    # Limit the while loop\n                    max_steps = 100\n                    while (\n                        attacker.weapon_cooldown == 0 or attacker.weapon_cooldown > 3\n                    ) and real_damage < expected_damage:\n                        if attacker_type in {UnitTypeId.PROBE, UnitTypeId.SCV, UnitTypeId.DRONE}:\n                            attacker.attack(defender)\n                        await self._advance_steps(1)\n                        # Unsure why I have to recalculate this here again but it prevents a bug\n                        attacker, defender = get_attacker_and_defender()\n                        # pyrefly: ignore\n                        expected_damage: float = max(expected_damage, attacker.calculate_damage_vs_target(defender)[0])\n                        real_damage = math.ceil(\n                            # pyrefly: ignore\n                            defender.health_max + defender.shield_max - defender.health - defender.shield\n                        )\n                        # logger.info(\n                        #     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}\"\n                        # )\n                        max_steps -= 1\n                        assert max_steps > 0, (\n                            f\"Step limit reached. Test timed out for attacker {attacker_type} and defender {defender_type}\"\n                        )\n                    assert expected_damage == real_damage, (\n                        # pyrefly: ignore\n                        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}\"\n                    )\n\n                    await self.clean_up_center()\n\n        # Hide map again\n        await self.client.debug_show_map()\n        await self._advance_steps(2)\n        logger.warning(\"Action test 1001 successful.\")\n\n\nclass EmptyBot(BotAI):\n    async def on_start(self):\n        if self.units:\n            await self.client.debug_kill_unit(self.units)\n\n    async def on_step(self, iteration: int):\n        map_center = self.game_info.map_center\n        enemies = self.enemy_units | self.enemy_structures\n        if enemies:\n            enemies = enemies.closer_than(20, map_center)\n        if enemies:\n            # If attacker is visible: move command to attacker but try to not attack\n            for unit in self.units:\n                unit.move(enemies.closest_to(unit).position)\n        else:\n            # If attacker is invisible: dont move\n            for unit in self.units:\n                unit.hold_position()\n\n\ndef main():\n    run_game(maps.get(\"Empty128\"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/generate_pickle_files_bot.py",
    "content": "\"\"\"\nThis \"bot\" will loop over several available ladder maps and generate the pickle file in the \"/test/pickle_data/\" subfolder.\nThese will then be used to run tests from the test script \"test_pickled_data.py\"\n\"\"\"\n\nimport lzma\nimport pickle\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom s2clientprotocol import sc2api_pb2 as sc_pb\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race\nfrom sc2.game_data import GameData\nfrom sc2.game_info import GameInfo\nfrom sc2.game_state import GameState\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.protocol import ProtocolError\n\n\nclass ExporterBot(BotAI):\n    def __init__(self):\n        BotAI.__init__(self)\n        self.map_name: str = None  # pyrefly: ignore\n\n    async def on_step(self, iteration):\n        pass\n\n    def get_pickle_file_path(self) -> Path:\n        folder_path = Path(__file__).parent\n        subfolder_name = \"pickle_data\"\n        file_name = f\"{self.map_name}.xz\"\n        file_path = folder_path / subfolder_name / file_name\n        return file_path\n\n    def get_combat_file_path(self) -> Path:\n        folder_path = Path(__file__).parent\n        subfolder_name = \"combat_data\"\n        file_name = f\"{self.map_name}.xz\"\n        file_path = folder_path / subfolder_name / file_name\n        return file_path\n\n    async def store_data_to_file(self, file_path: Path):\n        # Grab all raw data from observation\n        raw_game_data = await self.client._execute(\n            data=sc_pb.RequestData(ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True)\n        )\n\n        raw_game_info = await self.client._execute(game_info=sc_pb.RequestGameInfo())\n\n        raw_observation = self.state.response_observation\n\n        # To test if this data is convertable in the first place\n        _game_data = GameData(raw_game_data.data)\n        _game_info = GameInfo(raw_game_info.game_info)\n        _game_state = GameState(raw_observation)\n\n        Path(file_path).parent.mkdir(exist_ok=True, parents=True)\n        with lzma.open(file_path, \"wb\") as f:\n            pickle.dump([raw_game_data, raw_game_info, raw_observation], f)\n\n    async def on_start(self):\n        file_path = self.get_pickle_file_path()\n        logger.info(f\"Saving pickle file to {self.map_name}.xz\")\n        await self.store_data_to_file(file_path)\n\n        # Make map visible\n        await self.client.debug_show_map()\n        await self.client.debug_control_enemy()\n        await self.client.debug_god()\n\n        # Spawn one of each unit\n        valid_units: set[UnitTypeId] = {\n            UnitTypeId(unit_id)\n            for unit_id, data in self.game_data.units.items()\n            if data._proto.race != Race.NoRace\n            and data._proto.race != Race.Random\n            and data._proto.available\n            # Dont cloak units\n            and UnitTypeId(unit_id) != UnitTypeId.MOTHERSHIP\n            and (data._proto.mineral_cost or data._proto.movement_speed or data._proto.weapons)\n        }\n\n        # Create units for self\n        await self.client.debug_create_unit([(valid_unit, 1, self.start_location, 1) for valid_unit in valid_units])\n        # Create units for enemy\n        await self.client.debug_create_unit(\n            [(valid_unit, 1, self.enemy_start_locations[0], 2) for valid_unit in valid_units]\n        )\n\n        await self._advance_steps(2)\n\n        file_path = self.get_combat_file_path()\n        await self.store_data_to_file(file_path)\n\n        await self.client.leave()\n\n\ndef main():\n    maps_ = [\n        \"16-BitLE\",\n        \"2000AtmospheresAIE\",\n        \"AbiogenesisLE\",\n        \"AbyssalReefLE\",\n        \"AcidPlantLE\",\n        \"AcolyteLE\",\n        \"AcropolisLE\",\n        \"AncientCisternAIE\",\n        \"Artana\",\n        \"AscensiontoAiurLE\",\n        \"AutomatonLE\",\n        \"BackwaterLE\",\n        \"Bandwidth\",\n        \"BattleontheBoardwalkLE\",\n        \"BelShirVestigeLE\",\n        \"BerlingradAIE\",\n        \"BlackburnAIE\",\n        \"BlackpinkLE\",\n        \"BlueshiftLE\",\n        \"CactusValleyLE\",\n        \"CatalystLE\",\n        \"CeruleanFallLE\",\n        \"CrystalCavern\",\n        \"CuriousMindsAIE\",\n        \"CyberForestLE\",\n        \"DarknessSanctuaryLE\",\n        \"DeathAura506\",\n        \"DeathAuraLE\",\n        \"DefendersLandingLE\",\n        \"DigitalFrontier\",\n        \"DiscoBloodbathLE\",\n        \"DragonScalesAIE\",\n        \"DreamcatcherLE\",\n        \"EastwatchLE\",\n        \"Ephemeron\",\n        \"EphemeronLE\",\n        \"EternalEmpire506\",\n        \"EternalEmpireLE\",\n        \"EverDream506\",\n        \"EverDreamLE\",\n        \"FractureLE\",\n        \"FrostLE\",\n        \"GlitteringAshesAIE\",\n        \"GoldenauraAIE\",\n        \"GoldenWall506\",\n        \"GoldenWallLE\",\n        \"GresvanAIE\",\n        \"HardwireAIE\",\n        \"HonorgroundsLE\",\n        \"IceandChrome506\",\n        \"IceandChromeLE\",\n        \"InfestationStationAIE\",\n        \"InsideAndOutAIE\",\n        \"InterloperLE\",\n        \"JagannathaAIE\",\n        \"KairosJunctionLE\",\n        \"KingsCoveLE\",\n        \"LostandFoundLE\",\n        \"LightshadeAIE\",\n        \"MechDepotLE\",\n        \"MoondanceAIE\",\n        \"NeonVioletSquareLE\",\n        \"NewkirkPrecinctTE\",\n        \"NewRepugnancyLE\",\n        \"NightshadeLE\",\n        \"OdysseyLE\",\n        \"OldSunshine\",\n        \"OxideAIE\",\n        \"PaladinoTerminalLE\",\n        \"ParaSiteLE\",\n        \"PersephoneAIE\",\n        \"PillarsofGold506\",\n        \"PillarsofGoldLE\",\n        \"PortAleksanderLE\",\n        \"PrimusQ9\",\n        \"ProximaStationLE\",\n        \"PylonAIE\",\n        \"RedshiftLE\",\n        \"Reminiscence\",\n        \"RomanticideAIE\",\n        \"RoyalBloodAIE\",\n        \"Sanglune\",\n        \"SequencerLE\",\n        \"SimulacrumLE\",\n        \"Submarine506\",\n        \"SubmarineLE\",\n        \"StargazersAIE\",\n        \"StasisLE\",\n        \"TheTimelessVoid\",\n        \"ThunderbirdLE\",\n        \"TorchesAIE\",\n        \"Treachery\",\n        \"Triton\",\n        \"Urzagol\",\n        \"WaterfallAIE\",\n        \"WintersGateLE\",\n        \"WorldofSleepersLE\",\n        \"YearZeroLE\",\n        \"ZenLE\",\n        \"Equilibrium513AIE\",\n        \"GoldenAura513AIE\",\n        \"HardLead513AIE\",\n        \"Oceanborn513AIE\",\n        \"SiteDelta513AIE\",\n        \"Gresvan513AIE\",\n    ]\n\n    for map_ in maps_:\n        try:\n            bot = ExporterBot()\n            bot.map_name = map_\n            file_path = bot.get_pickle_file_path()\n            if Path(file_path).is_file():\n                logger.warning(\n                    f\"Pickle file for map {map_} was already generated. Skipping. If you wish to re-generate files, please remove them first.\"\n                )\n                continue\n            logger.info(f\"Creating pickle file for map {map_} ...\")\n            run_game(maps.get(map_), [Bot(Race.Terran, bot), Computer(Race.Zerg, Difficulty.Easy)], realtime=False)\n        except ProtocolError:\n            # ProtocolError appears after a leave game request\n            pass\n        except Exception as e:\n            logger.exception(f\"Caught unknown exception: {e}\")\n            logger.error(\n                f\"Map {map_} could not be found, so pickle files for that map could not be generated. Error: {e}\"\n            )\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/queries_test_bot.py",
    "content": "\"\"\"\nThis testbot's purpose is to test the query behavior of the API.\nThese query functions are:\nself.can_place (RequestQueryBuildingPlacement)\nTODO: self.client.query_pathing (RequestQueryPathing)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot\nfrom sc2.position import Point2\n\n\nclass TestBot(BotAI):\n    def __init__(self):\n        # The time the bot has to complete all tests, here: the number of game seconds\n        self.game_time_timeout_limit = 20 * 60  # 20 minutes ingame time\n\n    async def on_start(self):\n        self.client.game_step = 16\n\n    async def on_step(self, iteration):\n        if iteration <= 7:\n            return\n\n        await self.clear_map_center()\n        await self.test_can_place_expect_true()\n        await self.test_can_place_expect_false()\n        await self.test_rally_points_with_rally_ability()\n        await self.test_rally_points_with_smart_ability()\n\n        # await self.client.leave()\n        sys.exit(0)\n\n    async def clear_map_center(self):\n        \"\"\"Spawn observer in map center, remove all enemy units, remove all own units.\"\"\"\n        map_center = self.game_info.map_center\n\n        # Spawn observer to be able to see enemy invisible units\n        await self.client.debug_create_unit([(UnitTypeId.OBSERVER, 1, map_center, 1)])\n        await self._advance_steps(10)\n\n        # Remove everything close to map center\n        enemy_units = self.enemy_units | self.enemy_structures\n        if enemy_units:\n            await self.client.debug_kill_unit(enemy_units)\n            await self._advance_steps(10)\n\n        neutral_units = self.resources\n        if neutral_units:\n            await self.client.debug_kill_unit(neutral_units)\n            await self._advance_steps(10)\n\n        my_units = self.units | self.structures\n        if my_units:\n            await self.client.debug_kill_unit(my_units)\n            await self._advance_steps(10)\n\n    async def spawn_unit(self, unit_type: UnitTypeId | list[UnitTypeId]):\n        await self._advance_steps(10)\n        if not isinstance(unit_type, list):\n            unit_type = [unit_type]\n        for i in unit_type:\n            await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 1)])\n\n    async def spawn_unit_enemy(self, unit_type: UnitTypeId | list[UnitTypeId]):\n        await self._advance_steps(10)\n        if not isinstance(unit_type, list):\n            unit_type = [unit_type]\n        for i in unit_type:\n            if i == UnitTypeId.CREEPTUMOR:\n                await self.client.debug_create_unit([(i, 1, self.game_info.map_center + Point2((5, 5)), 2)])\n            else:\n                await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 2)])\n\n    async def run_can_place(self) -> bool:\n        result = await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [self.game_info.map_center])\n        return result[0]\n\n    async def run_can_place_single(self) -> bool:\n        result = await self.can_place_single(AbilityId.TERRANBUILD_COMMANDCENTER, self.game_info.map_center)\n        return result\n\n    async def test_can_place_expect_true(self):\n        test_cases = [\n            # Invisible undetected enemy units\n            [UnitTypeId.OVERLORD, UnitTypeId.DARKTEMPLAR],\n            [UnitTypeId.OVERLORD, UnitTypeId.ROACHBURROWED],\n            [UnitTypeId.OVERLORD, UnitTypeId.ZERGLINGBURROWED],\n            [UnitTypeId.BARRACKSFLYING, UnitTypeId.WIDOWMINEBURROWED],\n            # Own units\n            [UnitTypeId.ZEALOT, None],\n            # Enemy units and structures, but without vision\n            [None, UnitTypeId.ZEALOT],\n            [None, UnitTypeId.SUPPLYDEPOT],\n            [None, UnitTypeId.DARKTEMPLAR],\n            [None, UnitTypeId.ROACHBURROWED],\n        ]\n\n        for i, (own_unit_type, enemy_unit_type) in enumerate(test_cases):\n            if enemy_unit_type:\n                await self.spawn_unit_enemy(enemy_unit_type)\n            if own_unit_type:\n                await self.spawn_unit(own_unit_type)\n\n            # Wait for creep\n            if enemy_unit_type == UnitTypeId.CREEPTUMOR:\n                await self._advance_steps(1000)\n            else:\n                await self._advance_steps(10)\n\n            result = await self.run_can_place()\n            if result:\n                logger.info(f\"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\")\n            else:\n                logger.error(\n                    f\"Expected result to be True, but was False for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\"\n                )\n            assert result, f\"Expected result to be True, but was False for test case: {i}\"\n            result2 = await self.run_can_place_single()\n            if result2:\n                logger.info(f\"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\")\n            else:\n                logger.error(\n                    f\"Expected result2 to be True, but was False for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\"\n                )\n            assert result2, f\"Expected result to be False, but was True for test case: {i}\"\n            await self.clear_map_center()\n\n    async def test_can_place_expect_false(self):\n        test_cases = [\n            # Own structures\n            [UnitTypeId.COMMANDCENTER, None],\n            # Enemy structures\n            [UnitTypeId.OVERLORD, UnitTypeId.SUPPLYDEPOT],\n            [UnitTypeId.OVERLORD, UnitTypeId.SUPPLYDEPOTLOWERED],\n            # Visible units\n            [UnitTypeId.OVERLORD, UnitTypeId.ZEALOT],\n            [UnitTypeId.OVERLORD, UnitTypeId.SIEGETANKSIEGED],\n            # Visible creep\n            [UnitTypeId.OVERLORD, UnitTypeId.CREEPTUMOR],\n            [UnitTypeId.OBSERVER, UnitTypeId.CREEPTUMOR],\n            # Invisible but detected units\n            [UnitTypeId.OBSERVER, UnitTypeId.DARKTEMPLAR],\n            [UnitTypeId.OBSERVER, UnitTypeId.ROACHBURROWED],\n            [UnitTypeId.OBSERVER, UnitTypeId.WIDOWMINEBURROWED],\n            # Special cases\n            [UnitTypeId.SIEGETANKSIEGED, None],\n            [UnitTypeId.OVERLORD, UnitTypeId.CHANGELING],\n            [UnitTypeId.OBSERVER, UnitTypeId.CHANGELING],\n            # True for linux client, False for windows client:\n            # [UnitTypeId.OVERLORD, UnitTypeId.MINERALFIELD450],\n            # [None, UnitTypeId.MINERALFIELD450],\n        ]\n\n        for i, (own_unit_type, enemy_unit_type) in enumerate(test_cases):\n            if own_unit_type:\n                await self.spawn_unit(own_unit_type)\n            if enemy_unit_type:\n                await self.spawn_unit_enemy(enemy_unit_type)\n\n            # Wait for creep\n            if enemy_unit_type == UnitTypeId.CREEPTUMOR:\n                await self._advance_steps(1000)\n            else:\n                await self._advance_steps(10)\n\n            result = await self.run_can_place()\n            if result:\n                logger.error(\n                    f\"Expected result to be False, but was True for test case: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\"\n                )\n            else:\n                logger.info(f\"Test case successful: {i}, own unit: {own_unit_type}, enemy unit: {enemy_unit_type}\")\n            assert not result, f\"Expected result to be False, but was True for test case: {i}\"\n            await self.clear_map_center()\n\n        # TODO Losing vision of a blocking enemy unit, check if can_place still returns False\n        #   for: creep, burrowed ling, burrowed roach, dark templar\n\n        # TODO Check if a moving invisible unit is blocking (patroulling dark templar, patroulling burrowed roach)\n\n    async def test_rally_points_with_rally_ability(self):\n        map_center = self.game_info.map_center\n        barracks_spawn_point = map_center.offset(Point2((10, 10)))\n        await self.client.debug_create_unit(\n            [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)]\n        )\n        await self._advance_steps(10)\n\n        for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]):\n            structure(AbilityId.RALLY_UNITS, map_center)\n        assert len(self.actions) == 4\n        filtered_actions = list(filter(self.prevent_double_actions, self.actions))\n        assert len(filtered_actions) == 4\n\n        await self._advance_steps(10)\n        for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]):\n            if not structure.rally_targets:\n                logger.error(\"Test case incomplete: Rally point command by using rally ability\")\n                return\n            rally_target_point = structure.rally_targets[0].point\n            distance = rally_target_point.distance_to_point2(map_center)\n            assert distance < 0.1\n\n        logger.info(\"Test case successful: Rally point command by using rally ability\")\n        await self.clear_map_center()\n\n    async def test_rally_points_with_smart_ability(self):\n        map_center = self.game_info.map_center\n        barracks_spawn_point = map_center.offset(Point2((10, 10)))\n        await self.client.debug_create_unit(\n            [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)]\n        )\n        await self._advance_steps(10)\n\n        for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]):\n            structure(AbilityId.SMART, map_center)\n        assert len(self.actions) == 4\n        filtered_actions = list(filter(self.prevent_double_actions, self.actions))\n        assert len(filtered_actions) == 4\n\n        await self._advance_steps(10)\n        for structure in self.structures([UnitTypeId.BARRACKS, UnitTypeId.FACTORY]):\n            if not structure.rally_targets:\n                logger.error(\"Test case incomplete: Rally point command by using smart ability\")\n                sys.exit(1)\n            rally_target_point = structure.rally_targets[0].point\n            distance = rally_target_point.distance_to_point2(map_center)\n            assert distance < 0.1\n\n        logger.info(\"Test case successful: Rally point command by using smart ability\")\n        await self.clear_map_center()\n\n    # TODO: Add more examples that use constants.py \"COMBINEABLE_ABILITIES\"\n\n    # TODO self.can_cast()\n\n\nclass EmptyBot(BotAI):\n    async def on_step(self, iteration: int):\n        for unit in self.units:\n            unit.hold_position()\n\n\ndef main():\n    run_game(maps.get(\"Empty128\"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/real_time_worker_production.py",
    "content": "\"\"\"\nThis bot tests if on 'realtime=True' any nexus has more than 1 probe in the queue.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race, Result\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot, Computer\nfrom sc2.unit import Unit\n\non_end_was_called: bool = False\n\n\nclass RealTimeTestBot(BotAI):\n    async def on_before_start(self):\n        mf = self.mineral_field\n        for w in self.workers:\n            w.gather(mf.closest_to(w))\n\n        # for nexus in self.townhalls:\n        #     nexus.train(UnitTypeId.PROBE)\n\n        await self._do_actions(self.actions)\n        self.actions.clear()\n        await asyncio.sleep(1)\n\n    async def on_start(self):\n        \"\"\"This function is run after the expansion locations and ramps are calculated.\"\"\"\n        self.client.game_step = 1\n\n    async def on_step(self, iteration):\n        # assert (\n        #     self.supply_left <= 15\n        # ), f\"Bot created 2 nexus in one step. Supply: {self.supply_used} / {self.supply_cap}\"\n\n        # Simulate that the bot takes too long in one iteration, sometimes\n        if iteration % 20 != 0:\n            await asyncio.sleep(0.1)\n\n        # Queue probes\n        for nexus in self.townhalls:\n            nexus_orders_amount = len(nexus.orders)\n            assert nexus_orders_amount <= 1, f\"{nexus_orders_amount}\"\n            # logger.info(f\"{self.time_formatted} {self.state.game_loop} {nexus} orders: {nexus_orders_amount}\")\n            if nexus.is_idle and self.can_afford(UnitTypeId.PROBE):\n                nexus.train(UnitTypeId.PROBE)\n                logger.info(\n                    f\"{self.time_formatted} {self.state.game_loop} Training probe {self.supply_used} / {self.supply_cap}\"\n                )\n            # Chrono\n            if nexus.energy >= 50:\n                nexus(AbilityId.EFFECT_CHRONOBOOSTENERGYCOST, nexus)\n\n        # Spawn nexus at expansion location that is not used\n        made_nexus = False\n        if self.supply_left == 0:\n            for expansion_location in self.expansion_locations_list:\n                if self.townhalls.closer_than(10, expansion_location):\n                    continue\n                if self.enemy_structures.closer_than(10, expansion_location):\n                    continue\n                await self.client.debug_create_unit([(UnitTypeId.NEXUS, 1, expansion_location, 1)])\n                logger.info(\n                    f\"{self.time_formatted} {self.state.game_loop} Spawning a nexus {self.supply_used} / {self.supply_cap}\"\n                )\n                made_nexus = True\n                continue\n\n        # Spawn new pylon in map center if no more expansions are available\n        if self.supply_left == 0 and not made_nexus:\n            await self.client.debug_create_unit([(UnitTypeId.PYLON, 1, self.game_info.map_center, 1)])\n\n        # Don't get disturbed during this test\n        if self.enemy_units:\n            await self.client.debug_kill_unit(self.enemy_units)\n\n        if self.supply_used >= 199 or self.time > 7 * 60:\n            logger.info(\"Test successful, bot reached 199 supply without queueing two probes at once\")\n            await self.client.leave()\n\n    async def on_building_construction_complete(self, unit: Unit):\n        # Set worker rally point\n        if unit.is_structure:\n            unit(AbilityId.RALLY_WORKERS, self.mineral_field.closest_to(unit))\n\n    async def on_end(self, game_result: Result):\n        global on_end_was_called\n        on_end_was_called = True\n        logger.info(f\"on_end() was called with result: {game_result}\")\n\n\ndef main():\n    run_game(\n        maps.get(\"AcropolisLE\"),\n        [Bot(Race.Protoss, RealTimeTestBot()), Computer(Race.Terran, Difficulty.Medium)],\n        realtime=True,\n        disable_fog=True,\n    )\n    assert on_end_was_called, f\"{on_end_was_called}\"\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "test/run_example_bots_vs_computer.py",
    "content": "\"\"\"\nThis script makes sure to run all bots in the examples folder to check if they can launch.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom importlib import import_module\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Difficulty, Race, Result\nfrom sc2.main import GameMatch, a_run_multiple_games_nokill\nfrom sc2.player import Bot, Computer\n\n# Time limit given in seconds of total in game time\ngame_time_limit_vs_computer = 240\n\nbot_infos = [\n    # Protoss\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.cannon_rush\",\n        \"bot_class_name\": \"CannonRushBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.find_adept_shades\",\n        \"bot_class_name\": \"FindAdeptShadesBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.threebase_voidray\",\n        \"bot_class_name\": \"ThreebaseVoidrayBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.warpgate_push\",\n        \"bot_class_name\": \"WarpGateBot\",\n    },\n    # Terran\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.cyclone_push\",\n        \"bot_class_name\": \"CyclonePush\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.mass_reaper\",\n        \"bot_class_name\": \"MassReaperBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.onebase_battlecruiser\",\n        \"bot_class_name\": \"BCRushBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.proxy_rax\",\n        \"bot_class_name\": \"ProxyRaxBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.ramp_wall\",\n        \"bot_class_name\": \"RampWallBot\",\n    },\n    # Zerg\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.expand_everywhere\",\n        \"bot_class_name\": \"ExpandEverywhere\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.hydralisk_push\",\n        \"bot_class_name\": \"Hydralisk\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.onebase_broodlord\",\n        \"bot_class_name\": \"BroodlordBot\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.zerg_rush\",\n        \"bot_class_name\": \"ZergRushBot\",\n    },\n    # # Other\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.worker_stack_bot\",\n        \"bot_class_name\": \"WorkerStackBot\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.worker_rush\",\n        \"bot_class_name\": \"WorkerRushBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.too_slow_bot\",\n        \"bot_class_name\": \"SlowBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.distributed_workers\",\n        \"bot_class_name\": \"TerranBot\",\n    },\n]\n\nmatches: list[GameMatch] = []\n\n# Run example bots\nfor bot_info in bot_infos:\n    bot_race: Race = bot_info[\"race\"]  # pyrefly: ignore\n    bot_path: str = bot_info[\"path\"]  # pyrefly: ignore\n    bot_class_name: str = bot_info[\"bot_class_name\"]  # pyrefly: ignore\n    module = import_module(bot_path)\n    bot_class: type[BotAI] = getattr(module, bot_class_name)\n\n    limit_match_duration = game_time_limit_vs_computer\n    if bot_class_name in {\"SlowBot\", \"RampWallBot\"}:\n        limit_match_duration = 2\n\n    matches.append(\n        GameMatch(\n            map_sc2=maps.get(\"Acropolis\"),\n            players=[Bot(bot_race, bot_class()), Computer(Race.Protoss, Difficulty.Easy)],\n            realtime=False,\n            game_time_limit=limit_match_duration,\n        )\n    )\n\n\nasync def main():\n    results = await a_run_multiple_games_nokill(matches)\n\n    # Verify results\n    for result, game_match in zip(results, matches):\n        # Zergrush bot sets variable to True when on_end was called\n        if hasattr(game_match.players[0], \"on_end_called\"):\n            assert getattr(game_match.players[0], \"on_end_called\", False) is True\n\n        assert all(v == Result.Tie for k, v in result.items()), (\n            f\"result={result} in bot vs computer: {game_match.players[0]} in realtime={game_match.realtime}\"\n        )\n    logger.info(\"Checked all results\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "test/run_example_bots_vs_each_other.py",
    "content": "\"\"\"\nThis script makes sure to run all bots in the examples folder to check if they can launch against each other.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom importlib import import_module\nfrom itertools import combinations\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race, Result\nfrom sc2.main import GameMatch, a_run_multiple_games_nokill\nfrom sc2.player import Bot\n\n# Time limit given in seconds of total in game time\ngame_time_limit_bot_vs_bot = 10\ngame_time_limit_bot_vs_bot_realtime = 2\n\nbot_infos = [\n    # Protoss\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.cannon_rush\",\n        \"bot_class_name\": \"CannonRushBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.find_adept_shades\",\n        \"bot_class_name\": \"FindAdeptShadesBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.threebase_voidray\",\n        \"bot_class_name\": \"ThreebaseVoidrayBot\",\n    },\n    {\n        \"race\": Race.Protoss,\n        \"path\": \"examples.protoss.warpgate_push\",\n        \"bot_class_name\": \"WarpGateBot\",\n    },\n    # Terran\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.cyclone_push\",\n        \"bot_class_name\": \"CyclonePush\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.mass_reaper\",\n        \"bot_class_name\": \"MassReaperBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.onebase_battlecruiser\",\n        \"bot_class_name\": \"BCRushBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.proxy_rax\",\n        \"bot_class_name\": \"ProxyRaxBot\",\n    },\n    {\n        \"race\": Race.Terran,\n        \"path\": \"examples.terran.ramp_wall\",\n        \"bot_class_name\": \"RampWallBot\",\n    },\n    # Zerg\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.expand_everywhere\",\n        \"bot_class_name\": \"ExpandEverywhere\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.hydralisk_push\",\n        \"bot_class_name\": \"Hydralisk\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.onebase_broodlord\",\n        \"bot_class_name\": \"BroodlordBot\",\n    },\n    {\n        \"race\": Race.Zerg,\n        \"path\": \"examples.zerg.zerg_rush\",\n        \"bot_class_name\": \"ZergRushBot\",\n    },\n]\n\nmatches: list[GameMatch] = []\n\n# Run bots against each other\nfor bot_info1, bot_info2 in combinations(bot_infos, 2):\n    bot_race1: Race = bot_info1[\"race\"]  # pyrefly: ignore\n    bot_path: str = bot_info1[\"path\"]  # pyrefly: ignore\n    bot_class_name: str = bot_info1[\"bot_class_name\"]  # pyrefly: ignore\n    module = import_module(bot_path)\n    bot_class1: type[BotAI] = getattr(module, bot_class_name)\n\n    bot_race2: Race = bot_info2[\"race\"]  # pyrefly: ignore\n    bot_path: str = bot_info2[\"path\"]  # pyrefly: ignore\n    bot_class_name: str = bot_info2[\"bot_class_name\"]  # pyrefly: ignore\n    module = import_module(bot_path)\n    bot_class2: type[BotAI] = getattr(module, bot_class_name)\n\n    for realtime in [True, False]:\n        matches.append(\n            GameMatch(\n                map_sc2=maps.get(\"Acropolis\"),\n                players=[\n                    Bot(bot_race1, bot_class1()),\n                    Bot(bot_race2, bot_class2()),\n                ],\n                realtime=False,\n                game_time_limit=game_time_limit_bot_vs_bot_realtime if realtime else game_time_limit_bot_vs_bot,\n            )\n        )\n\n\nasync def main():\n    results = await a_run_multiple_games_nokill(matches)\n\n    # Verify results\n    for result, game_match in zip(results, matches):\n        # Zergrush bot sets variable to True when on_end was called\n        if hasattr(game_match.players[0], \"on_end_called\"):\n            assert getattr(game_match.players[0], \"on_end_called\", False) is True\n\n        assert all(v == Result.Tie for k, v in result.items()), (\n            f\"result={result} in bot vs bot: {game_match.players[0]} vs {game_match.players[1]} in realtime={game_match.realtime}\"\n        )\n    logger.info(\"Checked all results\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "test/test_directions.py",
    "content": "import random\nfrom math import atan2, cos, pi, sin, sqrt\n\nfrom sc2.position import EPSILON, Point2\n\nP0 = Point2((0, 0))\nP1 = Point2((1, 1))\nP2 = Point2((3, 1))\nP3 = Point2((3, 3))\n\n\ndef rad_diff(a, b):\n    r1 = abs(a - b)\n    r2 = abs(b - a)\n    r3 = min(r1, r2)\n    r4 = abs(2 * pi - r3)\n    return min(r3, r4)\n\n\ndef test_test_rad_diff():\n    assert rad_diff(0, 0) == 0\n    assert rad_diff(0, 1) == 1\n    assert rad_diff(0, pi) == pi\n    assert rad_diff(pi, pi) == 0\n    assert rad_diff(2 * pi, 0) == 0\n    assert rad_diff(2 * pi + 1, 0) == 1\n    assert rad_diff(2 * pi + 1, 1) == 0\n    assert rad_diff(pi, -pi) == 0\n\n\ndef test_distance():\n    assert P0.distance_to(P1) == sqrt(2)\n    assert P1.distance_to(P2) == 2\n    assert P0.distance_to(P2) == sqrt(10)\n\n\ndef test_towards():\n    assert P0.towards(P1, 1) == Point2((sqrt(2) / 2, sqrt(2) / 2))\n\n\ndef test_random_on_distance():\n    random.seed(1)\n\n    def get_points(source, distance, n=1000):\n        return {source.random_on_distance(distance) for _ in range(n)}\n\n    def verify_distances(source, distance, n=1000):\n        for p in get_points(source, distance, n):\n            assert abs(source.distance_to(p) - distance) < 0.000001\n\n    def verify_angles(source, distance, n=1000):\n        angles_rad = {atan2(p.y - source.y, p.x - source.x) for p in get_points(source, distance, n)}\n\n        quadrants = {(cos(a) < 0, sin(a) < 0) for a in angles_rad}\n        assert len(quadrants) == 4\n\n    verify_distances(P0, 1e2)\n    verify_distances(P1, 1e3)\n    verify_distances(P2, 1e4)\n\n    verify_angles(P0, 1e2)\n    verify_angles(P1, 1e3)\n    verify_angles(P2, 1e4)\n\n\ndef test_towards_random_angle():\n    random.seed(1)\n\n    def random_points(n=1000):\n        def rs():\n            return 1 - random.random() * 2\n\n        return {Point2((rs() * 1000, rs() * 1000)) for _ in range(n)}\n\n    def verify(source, target, max_difference=(pi / 4), n=1000):\n        d = 1 + random.random() * 100\n        points = {source.towards_with_random_angle(target, distance=d, max_difference=max_difference) for _ in range(n)}\n\n        dx, dy = target.x - source.x, target.y - source.y\n        src_angle = atan2(dy, dx)\n\n        for p in points:\n            angle = atan2(p.y - source.y, p.x - source.x)\n            assert rad_diff(src_angle, angle) <= max_difference\n\n            assert abs(source.distance_to(p) - d) <= EPSILON\n\n    verify(P0, P1)\n    verify(P1, P2)\n    verify(P1, P3)\n    verify(P2, P3)\n\n    verify(P1, P0)\n    verify(P2, P1)\n    verify(P3, P1)\n    verify(P3, P2)\n\n    ps = random_points(n=50)\n    for p1 in ps:\n        for p2 in ps:\n            if p1 == p2:\n                continue\n            verify(p1, p2, n=10)\n"
  },
  {
    "path": "test/test_expiring_dict.py",
    "content": "from contextlib import suppress\n\nfrom sc2.expiring_dict import ExpiringDict\n\n\ndef test_class():\n    class State:\n        def __init__(self):\n            self.game_loop = 0\n\n    class BotAI:\n        def __init__(self):\n            self.state = State()\n\n        def increment(self, value=1):\n            self.state.game_loop += value\n\n    test_dict = {\"hello\": \"its me mario\", \"does_this_work\": \"yes it works\", \"another_test\": \"yep this one also worked\"}\n\n    bot = BotAI()\n    test = ExpiringDict(bot, max_age_frames=10)  # pyrefly: ignore\n\n    for key, value in test_dict.items():\n        test[key] = value\n    bot.increment()\n\n    # Test len\n    assert len(test) == 3\n\n    # Test contains method\n    assert \"hello\" in test\n    assert \"doesnt_exist\" not in test\n\n    # Get item\n    result = test[\"hello\"]\n    assert result == \"its me mario\"\n\n    # Get item that doesnt exist\n    with suppress(KeyError):\n        result = test[\"doesnt_exist\"]\n    assert result == test[\"hello\"]\n\n    # Set new item\n    test[\"setitem\"] = \"test\"\n\n    assert len(test) == 4\n\n    # Test iteration\n    for key, item in test.items():\n        assert key in test\n        assert test[key] == item, (key, item)\n        assert test.get(key) == item\n        assert test.get(key, with_age=True)[0] == item  # pyrefly: ignore\n        assert test.get(key, with_age=True)[1] in {0, 1}  # pyrefly: ignore\n\n    c = 0\n    for _key in test:\n        c += 1\n    assert c == 4\n\n    c = 0\n    for value in test.values():\n        c += 1\n    assert c == 4\n\n    # Update from another dict\n    updater_dict = {\"new_key\": \"my_new_value\"}\n    test.update(updater_dict)  # pyrefly: ignore\n    assert \"does_this_work\" in test\n    assert \"new_key\" in test\n\n    # Test pop method\n    new_key = test.pop(\"new_key\")\n    assert new_key == \"my_new_value\"\n\n    # Advance the frames by 10, this means all entries should now be invalid\n    bot.increment(10)\n\n    assert len(test) == 0\n\n    for _key in test:\n        assert False\n\n    for _value in test.values():\n        assert False\n\n    for _key, _value in test.items():\n        assert False\n\n    assert \"new_key\" not in test\n    assert \"setitem\" not in test\n    # len doesn't work at the moment how it should - all items in the dict are expired, so len should return 0\n    assert len(test) == 0, len(test)\n\n    # Test repr and str function\n    test[\"another_test\"] = \"yep this one also worked\"\n    test[\"setitem\"] = \"test\"\n    assert repr(test) == \"ExpiringDict('another_test': ('yep this one also worked', 11), 'setitem': ('test', 11))\"\n    assert str(test) == \"ExpiringDict('another_test': ('yep this one also worked', 11), 'setitem': ('test', 11))\"\n\n\nif __name__ == \"__main__\":\n    test_class()\n"
  },
  {
    "path": "test/test_pickled_data.py",
    "content": "\"\"\"\nYou can execute this test running the following command from the root python-sc2 folder:\nuv run pytest test/test_pickled_data.py\n\nThis 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.\n\nIt will load the pickle files, recreate the bot object from scratch and tests most of the bot properties and functions.\nAll 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.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport lzma\nimport math\nimport pickle\nimport random\nimport unittest\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom typing import Any\n\nfrom google.protobuf.internal import api_implementation\nfrom hypothesis import given, settings\nfrom hypothesis import strategies as st\nfrom loguru import logger\n\nfrom sc2.bot_ai import BotAI\nfrom sc2.client import Client\nfrom sc2.constants import ALL_GAS, CREATION_ABILITY_FIX\nfrom sc2.data import CloakState, Race\nfrom sc2.game_data import AbilityData, Cost, GameData\nfrom sc2.game_info import GameInfo\nfrom sc2.game_state import GameState\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.buff_id import BuffId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.pixel_map import PixelMap\nfrom sc2.position import Point2, Point3, Rect, Size\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\nMAPS: list[Path] = [\n    map_path for map_path in (Path(__file__).parent / \"pickle_data\").iterdir() if map_path.suffix == \".xz\"\n]\n\n\ndef load_map_pickle_data(map_path: Path) -> tuple[Any, Any, Any]:\n    with lzma.open(str(map_path.absolute()), \"rb\") as f:\n        raw_game_data, raw_game_info, raw_observation = pickle.load(f)\n        return raw_game_data, raw_game_info, raw_observation\n\n\ndef build_bot_object_from_pickle_data(raw_game_data, raw_game_info, raw_observation) -> BotAI:\n    # Build fresh bot object, and load the pickled data into the bot object\n    bot = BotAI()\n    game_data = GameData(raw_game_data.data)\n    game_info = GameInfo(raw_game_info.game_info)\n    game_state = GameState(raw_observation)\n    bot._initialize_variables()\n    client = Client(True)  # pyrefly: ignore\n    bot._prepare_start(client=client, player_id=1, game_info=game_info, game_data=game_data)\n    bot._prepare_step(state=game_state, proto_game_info=raw_game_info)\n    return bot\n\n\ndef get_map_specific_bot(map_path: Path) -> BotAI:\n    assert map_path in MAPS\n    data = load_map_pickle_data(map_path)\n    return build_bot_object_from_pickle_data(*data)\n\n\ndef test_protobuf_implementation():\n    \"\"\"Make sure that upb is used as implementation\"\"\"\n    assert api_implementation.Type() == \"upb\"\n\n\ndef test_bot_ai():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    # Test initial bot attributes at game start\n\n    # Properties from _prepare_start\n    assert 1 <= bot.player_id <= 2\n    assert isinstance(bot.race, Race)\n    assert isinstance(bot.enemy_race, Race)\n\n    # Properties from _prepare_step\n    assert bot.units.amount == bot.workers.amount\n    assert bot.structures.amount == bot.townhalls.amount\n    assert bot.workers.amount == 12\n    assert bot.townhalls.amount == 1\n    assert bot.gas_buildings.amount == 0\n    assert bot.minerals == 50\n    assert bot.vespene == 0\n    assert bot.supply_army == 0\n    assert bot.supply_workers == 12\n    assert bot.supply_cap == 15\n    assert bot.supply_used == 12\n    assert bot.supply_left == 3\n    assert bot.idle_worker_count == 0\n    assert bot.army_count == 0\n\n    # Test properties updated by \"_prepare_units\" function\n    assert not bot.blips\n    assert bot.units\n    assert bot.structures\n    assert not bot.enemy_units\n    assert not bot.enemy_structures\n    assert bot.mineral_field\n    assert bot.vespene_geyser\n    assert bot.resources\n    assert len(bot.destructables) >= 0\n    assert isinstance(bot.destructables, (list, set, dict))\n    assert len(bot.watchtowers) >= 0\n    assert bot.all_units\n    assert bot.workers\n    assert bot.townhalls\n    assert not bot.gas_buildings\n\n    # Test bot_ai functions\n    assert bot.time == 0\n    assert bot.time_formatted in {\"0:00\", \"00:00\"}\n    assert bot.start_location is None  # Is populated by main.py\n    bot.game_info.player_start_location = bot.townhalls.random.position  # pyrefly: ignore\n    assert bot.townhalls.random.position not in bot.enemy_start_locations\n    assert bot.enemy_units == Units([], bot)\n    assert bot.enemy_structures == Units([], bot)\n    bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers()\n    assert bot.main_base_ramp  # Test if any ramp was found\n\n    # 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\n    assert bot.can_feed(UnitTypeId.MARINE)\n    assert bot.can_feed(UnitTypeId.SIEGETANK)\n    assert not bot.can_feed(UnitTypeId.THOR)\n    assert not bot.can_feed(UnitTypeId.BATTLECRUISER)\n    assert not bot.can_feed(UnitTypeId.IMMORTAL)\n    assert bot.can_afford(UnitTypeId.ZERGLING)\n    assert bot.can_afford(UnitTypeId.MARINE)\n    assert bot.can_afford(UnitTypeId.SCV)\n    assert bot.can_afford(UnitTypeId.DRONE)\n    assert bot.can_afford(UnitTypeId.PROBE)\n    assert bot.can_afford(AbilityId.COMMANDCENTERTRAIN_SCV)\n    assert bot.can_afford(UnitTypeId.MARINE)\n    assert not bot.can_afford(UnitTypeId.SIEGETANK)\n    assert not bot.can_afford(UnitTypeId.BATTLECRUISER)\n    assert not bot.can_afford(UnitTypeId.MARAUDER)\n    assert not bot.can_afford(UpgradeId.WARPGATERESEARCH)\n    assert not bot.can_afford(AbilityId.RESEARCH_WARPGATE)\n\n    # Store old values for minerals, vespene\n    old_values = bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used\n    bot.vespene = 50  # pyrefly: ignore\n    assert bot.can_afford(UpgradeId.WARPGATERESEARCH)\n    assert bot.can_afford(AbilityId.RESEARCH_WARPGATE)\n    bot.minerals = 150  # pyrefly: ignore\n    bot.supply_cap = 15  # pyrefly: ignore\n    bot.supply_left = -1  # pyrefly: ignore\n    bot.supply_used = 16  # pyrefly: ignore\n    # Confirm that units that don't cost supply can be built while at negative supply using can_afford function\n    assert bot.can_afford(UnitTypeId.GATEWAY)\n    assert bot.can_afford(UnitTypeId.PYLON)\n    assert bot.can_afford(UnitTypeId.OVERLORD)\n    assert bot.can_afford(UnitTypeId.BANELING)\n    assert not bot.can_afford(UnitTypeId.ZERGLING)\n    assert not bot.can_afford(UnitTypeId.MARINE)\n    # pyrefly: ignore\n    bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used = old_values\n\n    worker = bot.workers.random\n    assert bot.select_build_worker(worker.position) == worker\n    for w in bot.workers:\n        if w == worker:\n            continue\n        assert bot.select_build_worker(w.position) != worker\n    assert bot.already_pending_upgrade(UpgradeId.STIMPACK) == 0\n    assert bot.already_pending(UpgradeId.STIMPACK) == 0\n    assert bot.already_pending(UnitTypeId.SCV) == 0\n    assert bot.get_terrain_height(worker) > 0\n    assert bot.in_placement_grid(worker)\n    assert bot.in_pathing_grid(worker)\n    # The pickle data was created by a terran bot, so there is no creep under any worker\n    assert not bot.has_creep(worker)\n    # Why did this stop working, not visible on first frame?\n    assert bot.is_visible(worker), f\"Visibility value at worker is {bot.state.visibility[worker.position.rounded]}\"\n\n    # Check price for morphing units and upgrades\n    cost_100 = [\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL1,\n        UpgradeId.TERRANSHIPWEAPONSLEVEL1,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL1,\n        UpgradeId.TERRANVEHICLEARMORSLEVEL1,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL1,\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL1,\n        UpgradeId.TERRANINFANTRYARMORSLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1,\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1,\n        UpgradeId.ZERGMELEEWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1,\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL1,\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL1,\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1,\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL1,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL1,\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL1,\n        UpgradeId.ZERGFLYERWEAPONSLEVEL1,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL1,\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL1,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL1,\n        UpgradeId.ZERGFLYERARMORSLEVEL1,\n    ]\n    cost_175 = [\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL2,\n        UpgradeId.TERRANSHIPWEAPONSLEVEL2,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL2,\n        UpgradeId.TERRANVEHICLEARMORSLEVEL2,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL2,\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL2,\n        UpgradeId.ZERGFLYERWEAPONSLEVEL2,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL2,\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL2,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL2,\n        UpgradeId.ZERGFLYERARMORSLEVEL2,\n    ]\n    cost_200 = [\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL2,\n        UpgradeId.PROTOSSSHIELDSLEVEL2,\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2,\n        UpgradeId.ZERGGROUNDARMORSLEVEL2,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3,\n        UpgradeId.ZERGMELEEWEAPONSLEVEL3,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3,\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL3,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL3,\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL3,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL3,\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL3,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL3,\n        UpgradeId.TERRANINFANTRYARMORSLEVEL3,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL3,\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL3,\n    ]\n    cost_250 = [\n        AbilityId.ARMORYRESEARCH_TERRANSHIPWEAPONSLEVEL3,\n        UpgradeId.TERRANSHIPWEAPONSLEVEL3,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEPLATINGLEVEL3,\n        UpgradeId.TERRANVEHICLEARMORSLEVEL3,\n        AbilityId.ARMORYRESEARCH_TERRANVEHICLEWEAPONSLEVEL3,\n        UpgradeId.TERRANVEHICLEWEAPONSLEVEL3,\n        AbilityId.RESEARCH_ZERGFLYERATTACKLEVEL3,\n        UpgradeId.ZERGFLYERWEAPONSLEVEL3,\n        AbilityId.CYBERNETICSCORERESEARCH_PROTOSSAIRWEAPONSLEVEL3,\n        UpgradeId.PROTOSSAIRWEAPONSLEVEL3,\n        AbilityId.RESEARCH_ZERGFLYERARMORLEVEL3,\n        UpgradeId.ZERGFLYERARMORSLEVEL3,\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3,\n        UpgradeId.ZERGGROUNDARMORSLEVEL3,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL3,\n        UpgradeId.PROTOSSSHIELDSLEVEL3,\n    ]\n\n    cost_150 = [\n        AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1,\n        UpgradeId.ZERGGROUNDARMORSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1,\n        UpgradeId.PROTOSSSHIELDSLEVEL1,\n        AbilityId.FORGERESEARCH_PROTOSSSHIELDSLEVEL1,\n        UpgradeId.PROTOSSSHIELDSLEVEL1,\n        AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2,\n        UpgradeId.ZERGMELEEWEAPONSLEVEL2,\n        AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2,\n        UpgradeId.ZERGMISSILEWEAPONSLEVEL2,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDARMORLEVEL2,\n        UpgradeId.PROTOSSGROUNDARMORSLEVEL2,\n        AbilityId.FORGERESEARCH_PROTOSSGROUNDWEAPONSLEVEL2,\n        UpgradeId.PROTOSSGROUNDWEAPONSLEVEL2,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYARMORLEVEL2,\n        UpgradeId.TERRANINFANTRYARMORSLEVEL2,\n        AbilityId.ENGINEERINGBAYRESEARCH_TERRANINFANTRYWEAPONSLEVEL2,\n        UpgradeId.TERRANINFANTRYWEAPONSLEVEL2,\n    ]\n    cost_list = [100, 175, 200, 250, 150]\n\n    def calc_cost(item_id) -> Cost:\n        if isinstance(item_id, AbilityId):\n            return bot.game_data.calculate_ability_cost(item_id)\n        elif isinstance(item_id, UpgradeId):\n            return bot.game_data.upgrades[item_id.value].cost\n        elif isinstance(item_id, UnitTypeId):\n            creation_ability = bot.game_data.units[item_id.value].creation_ability\n            if creation_ability is None:\n                return Cost(0, 0)\n            creation_ability_id = creation_ability.exact_id\n            return bot.game_data.calculate_ability_cost(creation_ability_id)\n        return Cost(0, 0)\n\n    def assert_cost(item_id, real_cost: Cost):\n        assert calc_cost(item_id) == real_cost, f\"Cost of {item_id} should be {real_cost} but is {calc_cost(item_id)}\"\n\n    for items, cost in zip([cost_100, cost_175, cost_200, cost_250, cost_150], cost_list):\n        real_cost2: Cost = Cost(cost, cost)\n        for item in items:\n            assert_cost(item, real_cost2)\n            assert bot.calculate_cost(item) == real_cost2, (\n                f\"Cost of {item} should be {real_cost2} but is {calc_cost(item)}\"\n            )\n\n    # Do not use the generic research abilities in the bot when testing if you can afford it as these are wrong\n    assert_cost(AbilityId.RESEARCH_ZERGFLYERARMOR, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_ZERGFLYERATTACK, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_ZERGGROUNDARMOR, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_ZERGMELEEWEAPONS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_ZERGMISSILEWEAPONS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_TERRANINFANTRYARMOR, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_TERRANINFANTRYWEAPONS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDARMOR, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_PROTOSSGROUNDWEAPONS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_PROTOSSSHIELDS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_TERRANSHIPWEAPONS, Cost(0, 0))\n    assert_cost(AbilityId.RESEARCH_TERRANVEHICLEWEAPONS, Cost(0, 0))\n\n    # Somehow this is 0, returned by the API\n    assert_cost(AbilityId.BUILD_REACTOR, Cost(0, 0))\n    # UnitTypeId.REACTOR has no creation ability (None)\n    # assert_cost(UnitTypeId.REACTOR, Cost(50, 50))\n\n    assert_cost(AbilityId.BUILD_REACTOR_BARRACKS, Cost(50, 50))\n    assert_cost(UnitTypeId.BARRACKSREACTOR, Cost(50, 50))\n    assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0))\n    assert_cost(UnitTypeId.ORBITALCOMMAND, Cost(150, 0))\n    assert_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND, Cost(150, 0))\n\n    assert bot.calculate_unit_value(UnitTypeId.ORBITALCOMMAND) == Cost(550, 0)\n    assert bot.calculate_unit_value(UnitTypeId.RAVAGER) == Cost(100, 100)\n    assert bot.calculate_unit_value(UnitTypeId.ARCHON) == Cost(175, 275)\n    assert bot.calculate_unit_value(UnitTypeId.ADEPTPHASESHIFT) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.AUTOTURRET) == Cost(100, 0)\n    assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRAN) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.INFESTORTERRANBURROWED) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.LARVA) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.EGG) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.LOCUSTMP) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.LOCUSTMPFLYING) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.BROODLING) == Cost(0, 0)\n    # Other and effects\n    assert bot.calculate_unit_value(UnitTypeId.KD8CHARGE) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.RAVAGERCORROSIVEBILEMISSILE) == Cost(0, 0)\n    assert bot.calculate_unit_value(UnitTypeId.VIPERACGLUESCREENDUMMY) == Cost(0, 0)\n\n    assert bot.calculate_cost(UnitTypeId.BROODLORD) == Cost(150, 150)\n    assert bot.calculate_cost(UnitTypeId.RAVAGER) == Cost(25, 75)\n    assert bot.calculate_cost(UnitTypeId.BANELING) == Cost(25, 25)\n    assert bot.calculate_cost(UnitTypeId.ORBITALCOMMAND) == Cost(150, 0)\n    assert bot.calculate_cost(AbilityId.UPGRADETOORBITAL_ORBITALCOMMAND) == Cost(150, 0)\n    assert bot.calculate_cost(UnitTypeId.REACTOR) == Cost(50, 50)\n    assert bot.calculate_cost(UnitTypeId.TECHLAB) == Cost(50, 25)\n    assert bot.calculate_cost(UnitTypeId.QUEEN) == Cost(150, 0)\n    assert bot.calculate_cost(UnitTypeId.HATCHERY) == Cost(300, 0)\n    assert bot.calculate_cost(UnitTypeId.LAIR) == Cost(150, 100)\n    assert bot.calculate_cost(UnitTypeId.HIVE) == Cost(200, 150)\n    assert bot.calculate_cost(UnitTypeId.DRONE) == Cost(50, 0)\n    assert bot.calculate_cost(UnitTypeId.SCV) == Cost(50, 0)\n    assert bot.calculate_cost(UnitTypeId.PROBE) == Cost(50, 0)\n    assert bot.calculate_cost(UnitTypeId.SPIRE) == Cost(200, 200)\n    assert bot.calculate_cost(UnitTypeId.ARCHON) == bot.calculate_unit_value(UnitTypeId.ARCHON)\n\n    assert_cost(AbilityId.MORPHTOBROODLORD_BROODLORD, Cost(150, 150))\n    assert_cost(AbilityId.MORPHTORAVAGER_RAVAGER, Cost(25, 75))\n    assert_cost(AbilityId.MORPH_LURKER, Cost(50, 100))\n    assert_cost(AbilityId.MORPHZERGLINGTOBANELING_BANELING, Cost(25, 25))\n\n    assert Cost(100, 50) == 2 * Cost(50, 25)\n    assert Cost(100, 50) == Cost(50, 25) * 2\n    assert Cost(50, 25) + Cost(50, 25) == Cost(50, 25) * 2\n    assert Cost(50, 25) + Cost(50, 25) == 2 * Cost(50, 25)\n    assert Cost(50, 25) != Cost(50, 25) * 2\n    assert Cost(100, 50) - Cost(50, 25) == Cost(50, 25)\n\n    assert bot.calculate_supply_cost(UnitTypeId.BARRACKS) == 0\n    assert bot.calculate_supply_cost(UnitTypeId.HATCHERY) == 0\n    assert bot.calculate_supply_cost(UnitTypeId.OVERLORD) == 0\n    assert bot.calculate_supply_cost(UnitTypeId.ZERGLING) == 1\n    assert bot.calculate_supply_cost(UnitTypeId.MARINE) == 1\n    assert bot.calculate_supply_cost(UnitTypeId.BANELING) == 0\n    assert bot.calculate_supply_cost(UnitTypeId.QUEEN) == 2\n    assert bot.calculate_supply_cost(UnitTypeId.ROACH) == 2\n    assert bot.calculate_supply_cost(UnitTypeId.RAVAGER) == 1\n    assert bot.calculate_supply_cost(UnitTypeId.CORRUPTOR) == 2\n    assert bot.calculate_supply_cost(UnitTypeId.BROODLORD) == 2\n    assert bot.calculate_supply_cost(UnitTypeId.HYDRALISK) == 2\n    assert bot.calculate_supply_cost(UnitTypeId.LURKERMP) == 1\n\n\ndef test_game_info():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    # Test if main base ramp works\n    bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers()\n    game_info: GameInfo = bot.game_info\n\n    bot.game_info.player_start_location = bot.townhalls.random.position\n\n    # Test game info object\n    assert len(game_info.players) == 2\n    assert game_info.map_name\n    assert game_info.local_map_path\n    assert game_info.map_size\n    assert game_info.pathing_grid\n    assert game_info.terrain_height\n    assert game_info.placement_grid\n    assert game_info.playable_area\n    assert game_info.map_center\n    assert game_info.map_ramps\n    assert game_info.player_races\n    assert game_info.start_locations\n    assert game_info.player_start_location\n\n\ndef test_game_data():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    game_data = bot.game_data\n\n    assert game_data.abilities\n    for ability_data in game_data.abilities.values():\n        assert ability_data.id\n        assert ability_data.exact_id\n        assert ability_data.friendly_name\n        # Doesnt work for all AbilityData (may return empty string or no cost)\n        assert isinstance(ability_data.link_name, str)\n        assert isinstance(ability_data.button_name, str)\n        assert isinstance(ability_data.is_free_morph, bool)\n        assert isinstance(ability_data.cost, Cost)\n\n    assert game_data.units\n    for unit_data in game_data.units.values():\n        with suppress(ValueError):\n            assert unit_data.id\n        assert unit_data.name\n        assert isinstance(unit_data.creation_ability, (AbilityData, type(None)))\n        assert isinstance(unit_data.footprint_radius, (float, type(None)))\n        # TODO Fails on newer python versions\n        # assert isinstance(unit_data.attributes, RepeatedScalarContainer)\n        assert isinstance(unit_data.has_minerals, bool)\n        assert isinstance(unit_data.has_vespene, bool)\n        assert isinstance(unit_data.cargo_size, int)\n        assert isinstance(unit_data.tech_requirement, (UnitTypeId, type(None)))\n        assert isinstance(unit_data.tech_alias, (list, type(None)))\n        assert isinstance(unit_data.unit_alias, (UnitTypeId, type(None)))\n        assert isinstance(unit_data.race, Race)\n        assert isinstance(unit_data.cost_zerg_corrected, Cost)\n        assert isinstance(unit_data.morph_cost, (Cost, type(None)))\n\n    assert game_data.upgrades\n    for upgrade_data in game_data.upgrades.values():\n        assert isinstance(upgrade_data.name, str)\n        assert isinstance(upgrade_data.research_ability, (AbilityData, type(None)))\n        assert isinstance(upgrade_data.cost, Cost)\n\n\ndef test_game_state():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    state = bot.state\n\n    assert not state.actions\n    assert not state.action_errors\n    assert not state.actions_unit_commands\n    assert not state.actions_toggle_autocast\n    assert not state.dead_units\n    assert not state.alerts\n    assert not state.player_result\n    assert not state.chat\n    assert state.common\n    assert state.psionic_matrix\n    assert state.game_loop == 0\n    assert state.score\n    assert not state.upgrades\n    assert not state.dead_units\n    assert state.visibility\n    assert state.creep\n    assert not state.effects\n\n\ndef test_pixelmap():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    pathing_grid: PixelMap = bot.game_info.pathing_grid\n    assert pathing_grid.bits_per_pixel\n    assert pathing_grid.bytes_per_pixel == pathing_grid.bits_per_pixel // 8\n    assert not pathing_grid.is_set((0, 0))\n    assert pathing_grid.is_empty((0, 0))\n    pathing_grid[Point2((0, 0))] = 123\n    assert pathing_grid.is_set((0, 0))\n    assert not pathing_grid.is_empty((0, 0))\n    pathing_grid.flood_fill_all(lambda i: True)\n    pathing_grid.copy()\n    pathing_grid.print()\n\n\ndef test_blip():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    # TODO this needs to be done in a test bot that has a sensor tower\n    # blips are enemy dots on the minimap that are out of vision\n\n\ndef test_score():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    assert bot.state.score\n    assert bot.state.score.summary\n\n\ndef test_unit():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    scv: Unit = bot.workers.random\n    townhall: Unit = bot.townhalls.first\n\n    assert scv.name\n    assert scv.race\n    assert scv.tag\n    assert not scv.is_structure\n    assert townhall.is_structure\n    assert scv.is_light\n    assert not townhall.is_light\n    assert not scv.is_armored\n    assert townhall.is_armored\n    assert scv.is_biological\n    assert not townhall.is_biological\n    assert scv.is_mechanical\n    assert townhall.is_mechanical\n    assert not scv.is_massive\n    assert not townhall.is_massive\n    assert not scv.is_psionic\n    assert not townhall.is_psionic\n    assert scv.tech_alias is None\n    assert townhall.tech_alias is None\n    assert scv.unit_alias is None\n    assert townhall.unit_alias is None\n    assert scv.can_attack\n    assert not townhall.can_attack\n    assert not scv.can_attack_both\n    assert not townhall.can_attack_both\n    assert scv.can_attack_ground\n    assert not townhall.can_attack_ground\n    assert scv.ground_dps\n    assert not townhall.ground_dps\n    assert scv.ground_range\n    assert not townhall.ground_range\n    assert not scv.can_attack_air\n    assert not townhall.can_attack_air\n    assert not scv.air_dps\n    assert not townhall.air_dps\n    assert not scv.air_range\n    assert not townhall.air_range\n    assert not scv.bonus_damage\n    assert not townhall.bonus_damage\n    assert not scv.armor\n    assert townhall.armor\n    assert scv.sight_range\n    assert townhall.sight_range\n    assert scv.movement_speed\n    assert scv.real_speed == scv.movement_speed\n    assert not townhall.movement_speed\n    assert townhall.real_speed == townhall.movement_speed\n    assert abs(scv.distance_per_step - 0.502231) < 1e-3\n    assert not townhall.distance_per_step\n    assert scv.distance_to_weapon_ready == 0\n    assert not townhall.distance_to_weapon_ready\n    assert not scv.is_mineral_field\n    assert not townhall.is_mineral_field\n    assert not scv.is_vespene_geyser\n    assert not townhall.is_vespene_geyser\n    assert scv.health\n    assert townhall.health\n    assert scv.health_max\n    assert townhall.health_max\n    assert scv.health_percentage\n    assert townhall.health_percentage\n    assert not scv.shield\n    assert not townhall.shield\n    assert not scv.shield_max\n    assert not townhall.shield_max\n    assert not scv.shield_percentage\n    assert not townhall.shield_percentage\n    assert scv.shield_health_percentage == 1\n    assert townhall.shield_health_percentage == 1\n    assert not scv.energy\n    assert not townhall.energy\n    assert not scv.energy_max\n    assert not townhall.energy_max\n    assert not scv.energy_percentage\n    assert not townhall.energy_percentage\n    assert not scv.age_in_frames\n    assert not townhall.age_in_frames\n    assert not scv.age\n    assert not townhall.age\n    assert not scv.is_memory\n    assert not townhall.is_memory\n    assert not scv.is_snapshot\n    assert not townhall.is_snapshot\n    assert scv.is_visible\n    assert townhall.is_visible\n    assert not scv.is_placeholder\n    assert not townhall.is_placeholder\n    assert scv.alliance\n    assert townhall.alliance\n    assert scv.is_mine\n    assert townhall.is_mine\n    assert not scv.is_enemy\n    assert not townhall.is_enemy\n    assert scv.owner_id\n    assert townhall.owner_id\n    assert scv.position\n    assert townhall.position\n    assert scv.position3d\n    assert townhall.position3d\n    assert scv.distance_to(townhall)\n    assert townhall.distance_to(scv)\n    # assert scv.facing\n    assert townhall.facing\n    assert scv.radius\n    assert townhall.radius\n    assert scv.build_progress\n    assert townhall.build_progress\n    assert scv.is_ready\n    assert townhall.is_ready\n    assert scv.cloak == CloakState.NotCloaked\n    assert townhall.cloak == CloakState.NotCloaked\n    assert not scv.is_cloaked\n    assert not townhall.is_cloaked\n    assert not scv.is_revealed\n    assert not townhall.is_revealed\n    assert scv.can_be_attacked\n    assert townhall.can_be_attacked\n    assert not scv.buffs\n    assert not townhall.buffs\n    assert not scv.is_carrying_minerals\n    assert not townhall.is_carrying_minerals\n    assert not scv.is_carrying_vespene\n    assert not townhall.is_carrying_vespene\n    assert not scv.is_carrying_resource\n    assert not townhall.is_carrying_resource\n    assert not scv.detect_range\n    assert not townhall.detect_range\n    assert not scv.radar_range\n    assert not townhall.radar_range\n    assert not scv.is_selected\n    assert not townhall.is_selected\n    assert scv.is_on_screen\n    assert townhall.is_on_screen\n    assert not scv.is_blip\n    assert not townhall.is_blip\n    assert not scv.is_powered\n    assert not townhall.is_powered\n    assert scv.is_active\n    assert not townhall.is_active\n    assert not scv.mineral_contents\n    assert not townhall.mineral_contents\n    assert not scv.vespene_contents\n    assert not townhall.vespene_contents\n    assert not scv.has_vespene\n    assert not townhall.has_vespene\n    assert not scv.is_flying\n    assert not townhall.is_flying\n    assert not scv.is_burrowed\n    assert not townhall.is_burrowed\n    assert not scv.is_hallucination\n    assert not townhall.is_hallucination\n    assert not scv.buff_duration_remain\n    assert not townhall.buff_duration_remain\n    assert not scv.buff_duration_max\n    assert not townhall.buff_duration_max\n    assert scv.orders\n    assert not townhall.orders\n    assert scv.order_target\n    assert not townhall.order_target\n    assert not scv.is_idle\n    assert townhall.is_idle\n    assert not scv.is_using_ability(AbilityId.TERRANBUILD_SUPPLYDEPOT)\n    assert not townhall.is_using_ability(AbilityId.COMMANDCENTERTRAIN_SCV)\n    assert not scv.is_moving\n    assert not townhall.is_moving\n    assert not scv.is_attacking\n    assert not townhall.is_attacking\n    assert not scv.is_patrolling\n    assert not townhall.is_patrolling\n    assert scv.is_gathering\n    assert not townhall.is_gathering\n    assert not scv.is_returning\n    assert not townhall.is_returning\n    assert scv.is_collecting\n    assert not townhall.is_collecting\n    assert not scv.is_constructing_scv\n    assert not townhall.is_constructing_scv\n    assert not scv.is_transforming\n    assert not townhall.is_transforming\n    assert not scv.is_repairing\n    assert not townhall.is_repairing\n    assert not scv.add_on_tag\n    assert not townhall.add_on_tag\n    assert not scv.has_add_on\n    assert not townhall.has_add_on\n    assert not scv.has_techlab\n    assert not townhall.has_techlab\n    assert not scv.has_reactor\n    assert not townhall.has_reactor\n    assert scv.add_on_land_position\n    assert townhall.add_on_land_position\n    assert scv.add_on_position\n    assert townhall.add_on_position\n    assert not scv.passengers\n    assert not townhall.passengers\n    assert not scv.passengers_tags\n    assert not townhall.passengers_tags\n    assert not scv.cargo_used\n    assert not townhall.cargo_used\n    assert not scv.has_cargo\n    assert not townhall.has_cargo\n    assert scv.cargo_size\n    assert not townhall.cargo_size\n    assert not scv.cargo_max\n    assert not townhall.cargo_max\n    assert not scv.cargo_left\n    assert not townhall.cargo_left\n    assert not scv.assigned_harvesters\n    assert townhall.assigned_harvesters == 12\n    assert not scv.ideal_harvesters\n    assert townhall.ideal_harvesters == 16\n    assert not scv.surplus_harvesters\n    assert townhall.surplus_harvesters == -4\n    assert not scv.weapon_cooldown\n    assert townhall.weapon_cooldown == -1\n    assert scv.weapon_ready\n    assert not townhall.weapon_ready\n    assert not scv.engaged_target_tag\n    assert not townhall.engaged_target_tag\n    assert not scv.is_detector\n    assert not townhall.is_detector\n    assert scv.distance_to_squared(townhall)\n    assert townhall.distance_to_squared(scv)\n    assert scv.target_in_range(townhall, bonus_distance=5)\n    assert not townhall.target_in_range(scv, bonus_distance=5)\n    assert not scv.has_buff(BuffId.STIMPACK)\n    assert not townhall.has_buff(BuffId.STIMPACK)\n\n    assert scv.calculate_damage_vs_target(townhall)[0] == 4\n    assert scv.calculate_damage_vs_target(townhall, ignore_armor=True)[0] == 5\n    assert townhall.calculate_damage_vs_target(scv) == (0, 0, 0)\n    assert townhall.calculate_damage_vs_target(scv, ignore_armor=True) == (0, 0, 0)\n\n    # TODO create one of each unit in the pickle tests to do damage calculations without having to create a mock class for each unit\n\n    assert scv.calculate_dps_vs_target(townhall) - 2.66 < 0.01\n    assert scv.calculate_dps_vs_target(townhall, ignore_armor=True) - 3.33 < 0.01\n    assert townhall.calculate_dps_vs_target(scv) == 0\n    assert townhall.calculate_dps_vs_target(scv, ignore_armor=True) == 0\n\n    assert scv.is_facing(townhall, angle_error=2 * math.pi)\n    assert not scv.is_facing(townhall)\n    assert townhall.is_facing(scv, angle_error=2 * math.pi)\n\n    assert scv.footprint_radius == 0\n    assert townhall.footprint_radius == 2.5\n\n    # marauder1 = Unit(marauder_proto, bot)\n    # marauder_15_hp = Unit(marauder_proto, bot)\n    # marauder_15_hp._proto.health = 15\n    # # Marauder1 should deal now 10+10vs_armored = 20 damage, but other marauder has 1 armor, so resulting damage should be 19\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp)[0] == 19\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True)[0] == 20\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True, include_overkill_damage=False)[0] == 15\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, include_overkill_damage=False)[0] == 15\n    #\n    # marauder1._proto.attack_upgrade_level = 2\n    # marauder_15_hp._proto.armor_upgrade_level = 1\n    # # Marauder1 should deal now 12+12vs_armored = 24 damage, but other marauder has 2 armor, so resulting damage should be 22\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp)[0] == 22\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True)[0] == 24\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, ignore_armor=True, include_overkill_damage=False)[0] == 15\n    # assert marauder1.calculate_damage_vs_target(marauder_15_hp, include_overkill_damage=False)[0] == 15\n\n\ndef test_units():\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n    scvs = bot.workers\n    townhalls = bot.townhalls\n\n    assert scvs.amount\n    assert townhalls.amount\n    assert not scvs.empty\n    assert not townhalls.empty\n    assert scvs.exists\n    assert townhalls.exists\n    assert scvs.find_by_tag(scvs.random.tag)\n    assert not townhalls.find_by_tag(0)\n    assert scvs.first\n    assert townhalls.first\n    assert scvs.take(11)\n    assert townhalls.take(1)\n    assert scvs.random\n    assert townhalls.random\n    assert scvs.random_or(1)\n    assert townhalls.random_or(0)\n    assert scvs.random_group_of(11)\n    assert not scvs.random_group_of(0)\n    assert not townhalls.random_group_of(0)\n    # assert not scvs.in_attack_range_of(townhalls.first)\n    # assert not townhalls.in_attack_range_of(scvs.first)\n    assert scvs.closest_distance_to(townhalls.first)\n    assert scvs.closest_distance_to(townhalls.first.position)\n    assert townhalls.closest_distance_to(scvs.first)\n    assert scvs.furthest_distance_to(townhalls.first)\n    assert scvs.furthest_distance_to(townhalls.first.position)\n    assert townhalls.furthest_distance_to(scvs.first)\n    assert scvs.closest_to(townhalls.first)\n    assert scvs.closest_to(townhalls.first.position)\n    assert townhalls.closest_to(scvs.first)\n    assert scvs.furthest_to(townhalls.first)\n    assert scvs.furthest_to(townhalls.first.position)\n    assert townhalls.furthest_to(scvs.first)\n    assert scvs.closer_than(10, townhalls.first)\n    assert scvs.closer_than(10, townhalls.first.position)\n    assert townhalls.closer_than(10, scvs.first)\n    assert scvs.further_than(0, townhalls.first)\n    assert scvs.further_than(0, townhalls.first.position)\n    assert townhalls.further_than(0, scvs.first)\n    assert townhalls.in_distance_between(scvs.first, 0, 999)\n    assert townhalls.in_distance_between(scvs.first.position, 0, 999)\n    assert townhalls.closest_n_units(scvs.first.position, n=1)\n    assert townhalls.furthest_n_units(scvs.first.position, n=1)\n    assert townhalls.in_distance_of_group(scvs, 999)\n    assert townhalls.in_closest_distance_to_group(scvs)\n    assert townhalls.n_closest_to_distance(scvs.first.position, 0, 1)\n    assert townhalls.n_furthest_to_distance(scvs.first.position, 0, 1)\n\n    empty_units = Units([], bot_object=bot)\n    assert not empty_units\n    assert not empty_units.closer_than(999, townhalls.first)\n    assert not empty_units.further_than(0, townhalls.first)\n    assert not empty_units.in_distance_between(townhalls.first, 0, 999)\n    assert not empty_units.closest_n_units(townhalls.first, 0)\n    assert not empty_units.furthest_n_units(townhalls.first, 0)\n\n    assert scvs.subgroup(scvs)\n    assert townhalls.subgroup(townhalls)\n    assert scvs.filter(pred=lambda x: x.type_id == UnitTypeId.SCV)\n    assert not townhalls.filter(pred=lambda x: x.type_id == UnitTypeId.NEXUS)\n    assert scvs.sorted\n    assert townhalls.sorted\n    assert scvs.sorted_by_distance_to(townhalls.first)\n    assert townhalls.sorted_by_distance_to(scvs.first)\n    assert scvs.tags_in(scvs.tags)\n    assert not townhalls.tags_in({0, 1, 2})\n    assert not scvs.tags_not_in(scvs.tags)\n    assert townhalls.tags_not_in({0, 1, 2})\n    assert scvs.of_type(UnitTypeId.SCV)\n    assert scvs.of_type([UnitTypeId.SCV])\n    assert townhalls.of_type({UnitTypeId.COMMANDCENTER, UnitTypeId.COMMANDCENTERFLYING})\n    assert not scvs.exclude_type(UnitTypeId.SCV)\n    assert townhalls.exclude_type({UnitTypeId.COMMANDCENTERFLYING})\n    assert not scvs.same_tech({UnitTypeId.PROBE})\n    assert townhalls.same_tech({UnitTypeId.ORBITALCOMMAND})\n    assert scvs.same_unit(UnitTypeId.SCV)\n    assert townhalls.same_unit({UnitTypeId.COMMANDCENTERFLYING})\n    assert scvs.center\n    assert townhalls.center == townhalls.first.position\n    assert not scvs.selected\n    assert not townhalls.selected\n    assert scvs.tags\n    assert townhalls.tags\n    assert scvs.ready\n    assert townhalls.ready\n    assert not scvs.not_ready\n    assert not townhalls.not_ready\n    assert not scvs.idle\n    assert townhalls.idle\n    assert scvs.owned\n    assert townhalls.owned\n    assert not scvs.enemy\n    assert not townhalls.enemy\n    assert not scvs.flying\n    assert not townhalls.flying\n    assert scvs.not_flying\n    assert townhalls.not_flying\n    assert not scvs.structure\n    assert townhalls.structure\n    assert scvs.not_structure\n    assert not townhalls.not_structure\n    assert scvs.gathering\n    assert not townhalls.gathering\n    assert not scvs.returning\n    assert not townhalls.returning\n    assert scvs.collecting\n    assert not townhalls.collecting\n    assert scvs.visible\n    assert townhalls.visible\n    assert not scvs.mineral_field\n    assert not townhalls.mineral_field\n    assert not scvs.vespene_geyser\n    assert not townhalls.vespene_geyser\n    assert scvs.prefer_idle\n    assert townhalls.prefer_idle\n    assert len(Unit.class_cache) == 2  # Filled with CC and SCV from previous tests\n    assert len(scvs + townhalls) == 13\n    assert hash(scvs + townhalls)\n    assert scvs.copy()\n    assert scvs.by_tag(scvs[0].tag)\n\n\ndef test_exact_creation_ability():\n    try:\n        from sc2.dicts.unit_abilities import UNIT_ABILITIES\n        from sc2.dicts.unit_unit_alias import UNIT_UNIT_ALIAS\n    except ImportError:\n        logger.info(\"Import error: dict sc2/dicts/ are missing!\")\n        return\n    test_case = unittest.TestCase()\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n\n    ignore_types = {\n        UnitTypeId.ADEPTPHASESHIFT,\n        UnitTypeId.ARBITERMP,\n        UnitTypeId.BROODLING,\n        UnitTypeId.BYPASSARMORDRONE,\n        UnitTypeId.CORSAIRMP,\n        UnitTypeId.EGG,\n        UnitTypeId.ELSECARO_COLONIST_HUT,\n        UnitTypeId.HERC,\n        UnitTypeId.HERCPLACEMENT,\n        UnitTypeId.INFESTEDTERRANSEGG,\n        UnitTypeId.LARVA,\n        UnitTypeId.NYDUSCANALCREEPER,\n        UnitTypeId.QUEENMP,\n        UnitTypeId.RAVENREPAIRDRONE,\n        UnitTypeId.REPLICANT,\n        UnitTypeId.SCOURGEMP,\n        UnitTypeId.SCOUTMP,\n        UnitTypeId.WARHOUND,\n    }\n\n    unit_types = list(UNIT_UNIT_ALIAS) + list(UNIT_UNIT_ALIAS.values()) + list(UNIT_ABILITIES) + list(ALL_GAS)\n    unit_types_unique_sorted = sorted({t.name for t in unit_types})\n    for unit_type_name in unit_types_unique_sorted:\n        unit_type = UnitTypeId[unit_type_name]\n        if unit_type in ignore_types:\n            continue\n\n        if unit_type in [\n            UnitTypeId.ARCHON,\n            UnitTypeId.ASSIMILATORRICH,\n            UnitTypeId.EXTRACTORRICH,\n            UnitTypeId.REFINERYRICH,\n        ]:\n            with test_case.assertRaises(AttributeError):\n                # pyrefly: ignore\n                _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id\n            continue\n\n        try:\n            # pyrefly: ignore\n            _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id\n        except AttributeError:\n            if unit_type not in CREATION_ABILITY_FIX:\n                assert False, f\"Unit type '{unit_type}' missing from CREATION_ABILITY_FIX\"\n\n\ndef test_dicts():\n    # May be missing but that should not fail the tests\n    try:\n        from sc2.dicts.unit_research_abilities import RESEARCH_INFO\n    except ImportError:\n        logger.info(\"Import error: dict sc2/dicts/unit_research_abilities.py is missing!\")\n        return\n\n    bot: BotAI = get_map_specific_bot(random.choice(MAPS))\n\n    for data in RESEARCH_INFO.values():\n        upgrade_id: UpgradeId\n        for upgrade_id, upgrade_data in data.items():\n            research_ability_correct: AbilityId = upgrade_data[\"ability\"]  # pyrefly: ignore\n            research_ability_data_from_api = bot.game_data.upgrades[upgrade_id.value].research_ability\n            if research_ability_data_from_api is None:\n                continue\n            research_ability_id_from_api: AbilityId = research_ability_data_from_api.exact_id\n            if upgrade_id.value in {116, 117, 118}:\n                # Research abilities for armory armor plating are mapped incorrectly in the API\n                continue\n            if research_ability_correct.value in {807, 1284}:\n                # Test broke on windows\n                continue\n            assert research_ability_correct == research_ability_id_from_api, (\n                f\"Research abilities do not match: Correct one is {research_ability_correct} but API returned {research_ability_id_from_api}\"\n            )\n\n\n@given(\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n)\n@settings(max_examples=500)\ndef test_position_pointlike(x1, y1, x2, y2, x3, y3):\n    pos1 = Point2((x1, y1))\n    pos2 = Point2((x2, y2))\n    pos3 = Point2((x3, y3))\n    epsilon = 1e-3\n    assert pos1.position == pos1\n    dist = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5\n    assert abs(pos1.distance_to(pos2) - dist) <= epsilon\n    assert abs(pos1.distance_to_point2(pos2) - dist) <= epsilon\n    assert abs(pos1._distance_squared(pos2) ** 0.5 - dist) <= epsilon\n\n    points = {pos2, pos3}\n    points2 = {pos1, pos2, pos3}\n    # All 3 points need to be different\n    if len(points2) == 3:\n        assert pos1.sort_by_distance(points2) == sorted(points2, key=lambda p: pos1._distance_squared(p))\n        assert pos1.closest(points2) == pos1\n        closest_point = min(points, key=lambda p: p._distance_squared(pos1))\n        dist_closest_point = pos1._distance_squared(closest_point) ** 0.5\n        furthest_point = max(points, key=lambda p: p._distance_squared(pos1))\n        dist_furthest_point = pos1._distance_squared(furthest_point) ** 0.5\n\n        # 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\n        assert pos1.closest(points) in {p for p in points2 if abs(pos1.distance_to(p) - dist_closest_point) < epsilon}\n        assert abs(pos1.distance_to_closest(points) - pos1._distance_squared(closest_point) ** 0.5) < epsilon\n        assert pos1.furthest(points) in {p for p in points2 if abs(pos1.distance_to(p) - dist_furthest_point) < epsilon}\n        assert abs(pos1.distance_to_furthest(points) - pos1._distance_squared(furthest_point) ** 0.5) < epsilon\n        assert pos1.offset(pos2) == Point2((pos1.x + pos2.x, pos1.y + pos2.y))\n        if pos1 != pos2:\n            assert pos1.unit_axes_towards(pos2) != Point2((0, 0))\n\n        if x3 > 0:\n            temp_pos = pos1.towards(pos2, x3)\n            if x3 <= pos1.distance_to(pos2):\n                # Using \"towards\" function to go between pos1 and pos2\n                dist1 = pos1.distance_to(temp_pos) + pos2.distance_to(temp_pos)\n                dist2 = pos1.distance_to(pos2)\n                assert abs(dist1 - dist2) <= epsilon\n            else:\n                # Using \"towards\" function to go past pos2\n                dist1 = pos1.distance_to(pos2) + pos2.distance_to(temp_pos)\n                dist2 = pos1.distance_to(temp_pos)\n                assert abs(dist1 - dist2) <= epsilon\n        elif x3 < 0:\n            # Using \"towards\" function with a negative value\n            temp_pos = pos1.towards(pos2, x3)\n            dist1 = temp_pos.distance_to(pos1) + pos1.distance_to(pos2)\n            dist2 = pos2.distance_to(temp_pos)\n            assert abs(dist1 - dist2) <= epsilon\n\n    assert pos1 == pos1\n    assert pos2 == pos2\n    assert pos3 == pos3\n    assert isinstance(hash(pos1), int)\n    assert isinstance(hash(pos2), int)\n    assert isinstance(hash(pos3), int)\n\n\n@given(\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n)\n@settings(max_examples=500)\ndef test_position_point2(x1, y1, x2, y2):\n    pos1 = Point2((x1, y1))\n    pos2 = Point2((x2, y2))\n    assert pos1.x == x1\n    assert pos1.y == y1\n    assert pos1.to2 == pos1\n    assert pos1.to3 == Point3((x1, y1, 0))\n\n    length1 = (pos1.x**2 + pos1.y**2) ** 0.5\n    assert abs(pos1.length - length1) < 0.001\n    if length1:\n        normalized1 = pos1 / length1\n        assert abs(pos1.normalized.is_same_as(pos1 / length1))\n        assert abs(normalized1.length - 1) < 0.001\n    length2 = (pos2.x**2 + pos2.y**2) ** 0.5\n    assert abs(pos2.length - length2) < 0.001\n    if length2:\n        normalized2 = pos2 / length2\n        assert abs(pos2.normalized.is_same_as(normalized2))\n        assert abs(normalized2.length - 1) < 0.001\n\n    assert isinstance(pos1.distance_to(pos2), float)\n    assert isinstance(pos1.distance_to_point2(pos2), float)\n    if x2 > 0:\n        assert pos1.random_on_distance(x2) != pos1\n        assert pos1.towards_with_random_angle(pos2, x2) != pos1\n    assert pos1.towards_with_random_angle(pos2) != pos1\n    if pos1 != pos2:\n        dist = pos1.distance_to(pos2)\n        intersections1 = pos1.circle_intersection(pos2, r=dist / 2)\n        assert len(intersections1) == 1\n        intersections2 = pos1.circle_intersection(pos2, r=dist * 2 / 3)\n        assert len(intersections2) == 2\n    neighbors4 = pos1.neighbors4\n    assert len(neighbors4) == 4\n    neighbors8 = pos1.neighbors8\n    assert len(neighbors8) == 8\n\n    assert pos1 + pos2 == Point2((x1 + x2, y1 + y2))\n    assert pos1 - pos2 == Point2((x1 - x2, y1 - y2))\n    assert pos1 * pos2 == Point2((x1 * x2, y1 * y2))\n    if 0 not in {x2, y2}:\n        assert pos2\n        assert pos1 / pos2 == Point2((x1 / x2, y1 / y2))\n\n    if pos1._distance_squared(pos2) < 0.1:\n        assert pos1.is_same_as(pos2, dist=0.1)\n\n    assert pos1.unit_axes_towards(pos2) == pos1.direction_vector(pos2)\n\n\n@given(\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n)\n@settings(max_examples=10)\ndef test_position_point3(x1, y1, z1):\n    pos1 = Point3((x1, y1, z1))\n    assert pos1.z == z1\n    assert pos1.to3 == pos1\n\n\n@given(\n    st.integers(\n        min_value=-1e5,  # pyrefly: ignore\n        max_value=1e5,  # pyrefly: ignore\n    ),\n    st.integers(\n        min_value=-1e5,  # pyrefly: ignore\n        max_value=1e5,  # pyrefly: ignore\n    ),\n)\n@settings(max_examples=20)\ndef test_position_size(w, h):\n    size = Size((w, h))\n    assert size.width == w\n    assert size.height == h\n\n\n@given(\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n    st.integers(min_value=-1e5, max_value=1e5),  # pyrefly: ignore\n)\n@settings(max_examples=20)\ndef test_position_rect(x, y, w, h):\n    rect = Rect((x, y, w, h))\n    assert rect.x == x\n    assert rect.y == y\n    assert rect.width == w\n    assert rect.height == h\n    assert rect.right == x + w\n    assert rect.top == y + h\n    assert rect.size == Size((w, h))\n    assert rect.center == Point2((rect.x + rect.width / 2, rect.y + rect.height / 2))\n    assert rect.offset((1, 1)) == Rect((x + 1, y + 1, w, h))\n\n\ndef test_missing_enum():\n    enum_number = 123456789\n    enum_converted = BuffId(enum_number)\n    assert enum_converted == BuffId.NULL\n\n\nif __name__ == \"__main__\":\n    test_unit()\n"
  },
  {
    "path": "test/test_pickled_ramp.py",
    "content": "\"\"\"\nYou can execute this test running the following command from the root python-sc2 folder:\nuv run pytest test/test_pickled_ramp.py\n\nThis 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.\n\nIt will load the pickle files, recreate the bot object from scratch and tests most of the bot properties and functions.\nAll 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.\n\"\"\"\n\nimport time\nfrom pathlib import Path\n\nfrom loguru import logger\n\nfrom sc2.game_info import Ramp\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\nfrom test.test_pickled_data import MAPS, get_map_specific_bot\n\n\n# From https://docs.pytest.org/en/latest/example/parametrize.html#a-quick-port-of-testscenarios\ndef pytest_generate_tests(metafunc):\n    idlist = []\n    argvalues = []\n    argnames = []\n    for scenario in metafunc.cls.scenarios:\n        idlist.append(scenario[0])\n        items = scenario[1].items()\n        argnames = [x[0] for x in items]\n        argvalues.append([x[1] for x in items])\n    metafunc.parametrize(argnames, argvalues, ids=idlist, scope=\"class\")\n\n\nclass TestClass:\n    # Load all pickle files and convert them into bot objects from raw data (game_data, game_info, game_state)\n    scenarios = [(map_path.name, {\"map_path\": map_path}) for map_path in MAPS]\n\n    MAPS_WITH_ODD_EXPANSION_COUNT = {\"Persephone AIE\", \"StargazersAIE\", \"Stasis LE\"}\n\n    def test_main_base_ramp(self, map_path: Path):\n        bot = get_map_specific_bot(map_path)\n\n        bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers()\n\n        # Test if main ramp works for all spawns\n        for spawn in bot.game_info.start_locations + [bot.townhalls[0].position]:\n            # Remove cached precalculated ramp\n            if hasattr(bot, \"main_base_ramp\"):\n                del bot.main_base_ramp\n\n            # Set start location as one of the opponent spawns\n            bot.game_info.player_start_location = spawn\n\n            # Find main base ramp for opponent\n            ramp: Ramp = bot.main_base_ramp\n            assert ramp.top_center\n            assert ramp.bottom_center\n            assert ramp.size\n            assert ramp.points\n            assert ramp.upper\n            assert ramp.lower\n            # Test if ramp was detected far away\n            logger.info(ramp.top_center)\n            distance = ramp.top_center.distance_to(bot.game_info.player_start_location)\n            assert distance < 30, (\n                f\"Distance from spawn to main ramp was detected as {distance:.2f}, which is too far. Spawn: {spawn}, Ramp: {ramp.top_center}\"\n            )\n            # 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\n            if len(ramp.upper) in {2, 5}:\n                assert ramp.upper2_for_ramp_wall\n                # Check if terran wall was found\n                assert ramp.barracks_correct_placement\n                assert ramp.barracks_in_middle\n                assert ramp.depot_in_middle\n                assert len(ramp.corner_depots) == 2\n                # Check if protoss wall was found\n                assert ramp.protoss_wall_pylon\n                assert len(ramp.protoss_wall_buildings) == 2\n                assert ramp.protoss_wall_warpin\n            else:\n                # On maps it is unable to find valid wall positions (Honorgrounds LE) it should return None, empty sets or empty lists\n                assert ramp.barracks_correct_placement is None\n                assert ramp.barracks_in_middle is None\n                assert ramp.depot_in_middle is None\n                assert ramp.corner_depots == set()\n                assert ramp.protoss_wall_pylon is None\n                assert ramp.protoss_wall_buildings == frozenset()\n                assert ramp.protoss_wall_warpin is None\n\n    def test_bot_ai(self, map_path: Path):\n        bot = get_map_specific_bot(map_path)\n\n        # Recalculate and time expansion locations\n        t0 = time.perf_counter()\n        bot._find_expansion_locations()\n        t1 = time.perf_counter()\n        logger.info(f\"Time to calculate expansion locations: {t1 - t0} s\")\n\n        # TODO: Cache all expansion positions for a map and check if it is the same\n        # BelShirVestigeLE has only 10 bases - perhaps it should be removed since it was a WOL / HOTS map\n        assert len(bot.expansion_locations_list) >= 10, f\"Too few expansions found: {len(bot.expansion_locations_list)}\"\n        # Honorgrounds LE has 24 bases\n        assert len(bot.expansion_locations_list) <= 24, (\n            f\"Too many expansions found: {len(bot.expansion_locations_list)}\"\n        )\n        # On N player maps, it is expected that there are N*X bases because of symmetry, at least for maps designed for 1vs1\n        # Those maps in the list have an un-even expansion count\n\n        expect_even_expansion_count = 1 if bot.game_info.map_name in self.MAPS_WITH_ODD_EXPANSION_COUNT else 0\n        assert (\n            len(bot.expansion_locations_list) % (len(bot.enemy_start_locations) + 1) == expect_even_expansion_count\n        ), f\"{bot.expansion_locations_list}\"\n        # Test if bot start location is in expansion locations\n        assert bot.townhalls.random.position in set(bot.expansion_locations_list), (\n            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}'\n        )\n        # Test if enemy start locations are in expansion locations\n        for location in bot.enemy_start_locations:\n            assert location in set(bot.expansion_locations_list), f\"{location}, {bot.expansion_locations_list}\"\n        # Each expansion is supposed to have at least one geysir and 6-12 minerals\n\n        for expansion, resource_positions in bot.expansion_locations_dict.items():\n            assert isinstance(expansion, Point2)\n            assert isinstance(resource_positions, Units)\n            if resource_positions:\n                assert isinstance(resource_positions[0], Unit)\n            # 2000 Atmospheres has bases with just 4 minerals patches and a rich geysir\n            # Neon violet has bases with just 6 resources. I think that was the back corner base with 4 minerals and 2 vespene\n            # Odyssey has bases with 10 mineral patches and 2 geysirs\n            # Blood boil returns 21?\n            assert 5 <= len(resource_positions) <= 12, (\n                f\"{len(resource_positions)} resource fields in one base on map {bot.game_info.map_name}\"\n            )\n\n        assert bot.owned_expansions == {bot.townhalls.first.position: bot.townhalls.first}\n"
  },
  {
    "path": "test/test_replays.py",
    "content": "from pathlib import Path\n\nfrom sc2.main import get_replay_version\n\nTHIS_FOLDER = Path(__file__).parent\nREPLAY_PATHS = [path for path in (THIS_FOLDER / \"replays\").iterdir() if path.suffix == \".SC2Replay\"]\n\n\ndef test_get_replay_version():\n    for replay_path in REPLAY_PATHS:\n        version = get_replay_version(replay_path)\n        assert version == (\"Base86383\", \"22EAC562CD0C6A31FB2C2C21E3AA3680\")\n"
  },
  {
    "path": "test/travis_test_script.py",
    "content": "\"\"\"\nThis 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)\nIdeally 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.\n\nUsage:\ncd into python-sc2/ directory\ndocker build -f test/Dockerfile -t test_image .\ndocker run test_image -c \"python test/travis_test_script.py test/autotest_bot.py\"\n\nOr if you want to run from windows:\nuv run python test/travis_test_script.py test/autotest_bot.py\n\"\"\"\n\nimport subprocess\nimport sys\nimport time\n\nfrom loguru import logger\n\nretries = 3\n# My maxout bot (reaching 200 supply in sc2) took 110 - 140 real seconds for 7 minutes in game time\n# How long the script should run before it will be killed:\ntimeout_time = 8 * 60  # 8 minutes real time\n\nif len(sys.argv) > 1:\n    # Attempt to run process with retries and timeouts\n    t0 = time.time()\n    process, result = None, None\n    output_as_list = []\n    i = 0\n    for i in range(retries):\n        t0 = time.time()\n\n        process = subprocess.Popen([\"python\", sys.argv[1]], stdout=subprocess.PIPE)\n        try:\n            # Stop the current bot if the timeout was reached - the bot needs to finish a game within 3 minutes real time\n            result = process.communicate(timeout=timeout_time)\n        except subprocess.TimeoutExpired:\n            continue\n        out, err = result\n        result = out.decode(\"utf-8\")\n        if process.returncode is not None and process.returncode != 0:\n            # Bot has thrown an error, try again\n            logger.info(\n                f\"Bot has thrown an error with error code {process.returncode}. This was try {i + 1} out of {retries}.\"\n            )\n            continue\n\n        # Break as the bot run was successful\n        break\n\n    if process is not None and process.returncode is not None and result is not None:\n        # Reformat the output into a list\n\n        linebreaks = [\n            (\"\\r\\n\", result.count(\"\\r\\n\")),\n            (\"\\r\", result.count(\"\\r\")),\n            (\"\\n\", result.count(\"\\n\")),\n        ]\n        most_linebreaks_type = max(linebreaks, key=lambda x: x[1])\n        linebreak_type, linebreak_count = most_linebreaks_type\n\n        output_as_list = result.split(linebreak_type)\n        logger.info(\"Travis test script, bot output:\\r\\n{}\\r\\nEnd of bot output\".format(\"\\r\\n\".join(output_as_list)))\n\n    time_taken = time.time() - t0\n\n    # Bot was not successfully run in time, returncode will be None\n    if process is not None and (process.returncode is None or process.returncode != 0):\n        logger.info(\n            f\"Exiting with exit code 5, error: Attempted to launch script {sys.argv[1]} timed out after {time_taken} seconds. Retries completed: {i}\"\n        )\n        sys.exit(5)\n\n    # 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)\n    if process is not None and process.returncode is not None:\n        logger.info(f\"Returncode: {process.returncode}\")\n    logger.info(f\"Game took {round(time.time() - t0, 1)} real time seconds\")\n    if process is not None and process.returncode == 0:\n        for line in output_as_list:\n            # This will throw an error even if a bot is called Traceback\n            if \"Traceback \" in line:\n                logger.info(\"Exiting with exit code 3\")\n                sys.exit(3)\n        logger.info(\"Exiting with exit code 0\")\n        sys.exit(0)\n\n    # Exit code 1: game crashed I think\n    logger.info(\"Exiting with exit code 1\")\n    sys.exit(1)\n\n# Exit code 2: bot was not launched\nlogger.info(\"Exiting with exit code 2\")\nsys.exit(2)\n"
  },
  {
    "path": "test/upgradestest_bot.py",
    "content": "from __future__ import annotations\n\nfrom loguru import logger\n\nfrom sc2 import maps\nfrom sc2.bot_ai import BotAI\nfrom sc2.data import Race\nfrom sc2.ids.ability_id import AbilityId\nfrom sc2.ids.unit_typeid import UnitTypeId\nfrom sc2.ids.upgrade_id import UpgradeId\nfrom sc2.main import run_game\nfrom sc2.player import Bot\nfrom sc2.position import Point2\nfrom sc2.unit import Unit\nfrom sc2.units import Units\n\n\nclass TestBot(BotAI):\n    def __init__(self):\n        BotAI.__init__(self)\n        # The time the bot has to complete all tests, here: the number of game seconds\n        self.game_time_timeout_limit = 20 * 60  # 20 minutes ingame time\n\n        # Check how many test action functions we have\n        # At least 4 tests because we test properties and variables\n        self.action_tests = [\n            getattr(self, f\"test_botai_actions{index}\")\n            for index in range(4000)\n            if hasattr(getattr(self, f\"test_botai_actions{index}\", 0), \"__call__\")\n        ]\n        self.tests_done_by_name = set()\n\n        # Keep track of the action index and when the last action was started\n        self.current_action_index = 1\n        self.iteration_last_action_started = 8\n        # There will be 20 iterations of the bot doing nothing between tests\n        self.iteration_wait_time_between_actions = 20\n\n        self.scv_action_list = [\"move\", \"patrol\", \"attack\", \"hold\", \"scan_move\"]\n\n        # Variables for test_botai_actions11\n\n    async def on_start(self):\n        self.client.game_step = 8\n        # await self.client.quick_save()\n        await self.distribute_workers()\n\n    async def on_step(self, iteration):\n        if iteration == 0:\n            await self.chat_send(\"(glhf)\")\n        # Test if chat message was sent correctly\n        if iteration == 1:\n            assert len(self.state.chat) >= 1, self.state.chat\n\n        # Test actions\n        if iteration == 7:\n            for action_test in self.action_tests:\n                await action_test()\n\n        # Exit bot\n        if iteration > 100:\n            logger.info(f\"Tests completed after {round(self.time, 1)} seconds\")\n            exit(0)\n\n    async def clean_up_center(self):\n        map_center = self.game_info.map_center\n        # Remove everything close to map center\n        my_units = self.units | self.structures\n        if my_units:\n            my_units = my_units.closer_than(20, map_center)\n        if my_units:\n            await self.client.debug_kill_unit(my_units)\n        enemy_units = self.enemy_units | self.enemy_structures\n        if enemy_units:\n            enemy_units = enemy_units.closer_than(20, map_center)\n        if enemy_units:\n            await self.client.debug_kill_unit(enemy_units)\n        await self._advance_steps(2)\n\n    # Create all upgrade research structures and research each possible upgrade\n    async def test_botai_actions1(self):\n        map_center: Point2 = self.game_info.map_center\n\n        from sc2.dicts.unit_research_abilities import RESEARCH_INFO\n        from sc2.dicts.upgrade_researched_from import UPGRADE_RESEARCHED_FROM\n\n        structure_types: list[UnitTypeId] = sorted(set(UPGRADE_RESEARCHED_FROM.values()), key=lambda data: data.name)\n        upgrade_types: list[UpgradeId] = list(UPGRADE_RESEARCHED_FROM)\n\n        # TODO if *techlab in name -> spawn rax/ fact / starport next to it\n        addon_structures: dict[str, UnitTypeId] = {\n            \"BARRACKS\": UnitTypeId.BARRACKS,\n            \"FACTORY\": UnitTypeId.FACTORY,\n            \"STARPORT\": UnitTypeId.STARPORT,\n        }\n\n        await self.client.debug_fast_build()\n\n        structure_type: UnitTypeId\n        for structure_type in structure_types:\n            # TODO: techlabs\n            if \"TECHLAB\" in structure_type.name:\n                continue\n\n            # pyrefly: ignore\n            structure_upgrade_types: dict[UpgradeId, dict[str, AbilityId]] = RESEARCH_INFO[structure_type]\n            data: dict[str, AbilityId]\n            for upgrade_id, data in structure_upgrade_types.items():\n                # Collect data to spawn\n                research_ability: AbilityId = data.get(\"ability\", None)  # pyrefly: ignore\n                requires_power: bool = data.get(\"requires_power\", False)  # pyrefly: ignore\n                required_building: UnitTypeId = data.get(\"required_building\", None)  # pyrefly: ignore\n\n                # Prevent linux crash\n                if (\n                    research_ability.value not in self.game_data.abilities\n                    or upgrade_id.value not in self.game_data.upgrades\n                    or self.game_data.upgrades[upgrade_id.value].research_ability is None\n                    # pyrefly: ignore\n                    or self.game_data.upgrades[upgrade_id.value].research_ability.exact_id != research_ability\n                ):\n                    logger.info(\n                        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\"\n                    )\n                    continue\n\n                # Spawn structure and requirements\n                spawn_structures: list[UnitTypeId] = []\n                if requires_power:\n                    spawn_structures.append(UnitTypeId.PYLON)\n                spawn_structures.append(structure_type)\n                if required_building:\n                    spawn_structures.append(required_building)\n\n                await self.client.debug_create_unit([(structure, 1, map_center, 1) for structure in spawn_structures])\n                logger.info(\n                    f\"Spawning {structure_type} to research upgrade {upgrade_id} via research ability {research_ability}\"\n                )\n                await self._advance_steps(2)\n\n                # Wait for the structure to spawn\n                while not self.structures(structure_type):\n                    # logger.info(f\"Waiting for structure {structure_type} to spawn, structures close to center so far: {self.structures.closer_than(20, map_center)}\")\n                    await self._advance_steps(2)\n\n                # If cannot afford to research: cheat money\n                while not self.can_afford(upgrade_id):\n                    # logger.info(f\"Cheating money to be able to afford {upgrade_id}, cost: {self.calculate_cost(upgrade_id)}\")\n                    await self.client.debug_all_resources()\n                    await self._advance_steps(2)\n\n                # Research upgrade\n                assert upgrade_id in upgrade_types, \"Given upgrade is not in the list of upgrade types\"\n                assert self.structures(structure_type), f\"Structure {structure_type} has not been spawned in time\"\n\n                # Try to research the upgrade\n                while True:\n                    upgrader_structures: Units = self.structures(structure_type)\n                    # Upgrade has been researched, break\n                    if upgrader_structures:\n                        upgrader_structure: Unit = upgrader_structures.closest_to(map_center)\n                        if upgrader_structure.is_idle:\n                            # logger.info(f\"Making {upgrader_structure} research upgrade {upgrade_id}\")\n                            upgrader_structure.research(upgrade_id)\n                        await self._advance_steps(2)\n                        if upgrade_id in self.state.upgrades:\n                            break\n\n                await self.clean_up_center()\n        logger.warning(\"Action test 1 successful.\")\n\n\nclass EmptyBot(BotAI):\n    async def on_start(self):\n        if self.units:\n            await self.client.debug_kill_unit(self.units)\n\n    async def on_step(self, iteration: int):\n        map_center = self.game_info.map_center\n        enemies = self.enemy_units | self.enemy_structures\n        if enemies:\n            enemies = enemies.closer_than(20, map_center)\n        if enemies:\n            # If attacker is visible: move command to attacker but try to not attack\n            for unit in self.units:\n                unit.move(enemies.closest_to(unit).position)\n        else:\n            # If attacker is invisible: dont move\n            for unit in self.units:\n                unit.hold_position()\n\n\ndef main():\n    run_game(maps.get(\"Empty128\"), [Bot(Race.Terran, TestBot()), Bot(Race.Zerg, EmptyBot())], realtime=False)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  }
]