diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 645001d4..64a37745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: uv run pre-commit install - name: Run pre-commit hooks - run: uv run pre-commit run --all-files --hook-stage push + run: uv run pre-commit run --all-files --hook-stage pre-push generate_dicts_from_data_json: name: Generate dicts from data.json @@ -80,7 +80,7 @@ jobs: run: | mv sc2/dicts sc2/dicts_old uv run python generate_dicts_from_data_json.py - uv run pre-commit run --all-files --hook-stage push || true + uv run pre-commit run --all-files --hook-stage pre-push || true rm -rf sc2/dicts/__pycache__ sc2/dicts_old/__pycache__ - name: Upload generated dicts folder as artifact diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c784213..03917649 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,22 +38,25 @@ repos: # Autoformat code - id: ruff-format-check name: Check if files are formatted - stages: [push] + stages: [pre-push] language: system + # Run the following command to fix: + # uv run ruff format . entry: uv run ruff format . --check --diff pass_filenames: false - id: ruff-lint name: Lint files - stages: [push] + stages: [pre-push] language: system + # Run the following command to fix: + # uv run ruff check . --fix entry: uv run ruff check . pass_filenames: false - # TODO Fix issues - # - id: pyrefly - # name: Static types checking with pyrefly - # stages: [push] - # language: system - # entry: uv run pyrefly check - # pass_filenames: false + - id: pyrefly + name: Static types checking with pyrefly + stages: [pre-push] + language: system + entry: uv run pyrefly check + pass_filenames: false diff --git a/.pyre_configuration b/.pyre_configuration deleted file mode 100644 index 6c670b47..00000000 --- a/.pyre_configuration +++ /dev/null @@ -1,17 +0,0 @@ -{ - "site_package_search_strategy": "pep561", - "source_directories": [ - { - "import_root": ".", - "source": "sc2" - }, - { - "import_root": ".", - "source": "examples" - }, - { - "import_root": ".", - "source": "test" - } - ] -} diff --git a/README.md b/README.md index 0ae3d0b8..835e80a0 100644 --- a/README.md +++ b/README.md @@ -186,5 +186,5 @@ Git commit messages use [imperative-style messages](https://stackoverflow.com/a/ To run pre-commit hooks (which run autoformatting and autosort imports) you can run ```sh uv run pre-commit install -uv run pre-commit run --all-files --hook-stage push +uv run pre-commit run --all-files --hook-stage pre-push ``` diff --git a/dockerfiles/test_docker_image.sh b/dockerfiles/test_docker_image.sh index 6b1e17e1..25dc5b7f 100644 --- a/dockerfiles/test_docker_image.sh +++ b/dockerfiles/test_docker_image.sh @@ -41,7 +41,7 @@ docker cp test test_container:/root/python-sc2/test # Run various test bots docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/autotest_bot.py" docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/queries_test_bot.py" -#docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/damagetest_bot.py" +docker exec -i test_container bash -c "cd python-sc2 && uv run python test/travis_test_script.py test/damagetest_bot.py" docker cp examples test_container:/root/python-sc2/examples docker exec -i test_container bash -c "cd python-sc2 && uv run python test/run_example_bots_vs_computer.py" diff --git a/examples/arcade_bot.py b/examples/arcade_bot.py index 811ee944..0c85286d 100644 --- a/examples/arcade_bot.py +++ b/examples/arcade_bot.py @@ -101,7 +101,6 @@ def position_around_unit( step_size: int = 1, exclude_out_of_bounds: bool = True, ): - # pyre-ignore[16] pos = pos.position.rounded positions = { pos.offset(Point2((x, y))) @@ -114,7 +113,6 @@ def position_around_unit( positions = { p for p in positions - # pyre-ignore[16] if 0 <= p[0] < self.game_info.pathing_grid.width and 0 <= p[1] < self.game_info.pathing_grid.height } return positions diff --git a/examples/bot_vs_bot.py b/examples/bot_vs_bot.py index 73d69fa7..5de26601 100644 --- a/examples/bot_vs_bot.py +++ b/examples/bot_vs_bot.py @@ -15,7 +15,7 @@ def main_old(): - result: list[Result] = run_game( + result: Result | list[Result | None] = run_game( maps.get("AcropolisLE"), [ Bot(Race.Protoss, WarpGateBot()), diff --git a/examples/competitive/__init__.py b/examples/competitive/__init__.py index 28a10595..2b3e2872 100644 --- a/examples/competitive/__init__.py +++ b/examples/competitive/__init__.py @@ -4,8 +4,9 @@ import aiohttp from loguru import logger -import sc2 from sc2.client import Client +from sc2.main import _play_game +from sc2.portconfig import Portconfig from sc2.protocol import ConnectionAlreadyClosedError @@ -41,7 +42,7 @@ def run_ladder_game(bot): else: ports = [lan_port + p for p in range(1, 6)] - portconfig = sc2.portconfig.Portconfig() + portconfig = Portconfig() portconfig.server = [ports[1], ports[2]] portconfig.players = [[ports[3], ports[4]]] @@ -56,10 +57,11 @@ def run_ladder_game(bot): # Modified version of sc2.main._join_game to allow custom host and port, and to not spawn an additional sc2process (thanks to alkurbatov for fix) async def join_ladder_game(host, port, players, realtime, portconfig, save_replay_as=None, game_time_limit=None): ws_url = f"ws://{host}:{port}/sc2api" + # pyrefly: ignore ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120) client = Client(ws_connection) try: - result = await sc2.main._play_game(players[0], client, realtime, portconfig, game_time_limit) + result = await _play_game(players[0], client, realtime, portconfig, game_time_limit) if save_replay_as is not None: await client.save_replay(save_replay_as) # await client.leave() @@ -68,6 +70,6 @@ async def join_ladder_game(host, port, players, realtime, portconfig, save_repla logger.error("Connection was closed before the game ended") return None finally: - ws_connection.close() + await ws_connection.close() return result diff --git a/examples/competitive/bot.py b/examples/competitive/bot.py index 253337b6..f575ae79 100644 --- a/examples/competitive/bot.py +++ b/examples/competitive/bot.py @@ -11,7 +11,6 @@ async def on_step(self, iteration: int): # Populate this function with whatever your bot should do! pass - # pyre-ignore[11] async def on_end(self, game_result: Result): print("Game ended.") # Do things here after the game ends diff --git a/examples/competitive/run.py b/examples/competitive/run.py index 10984103..eb911442 100644 --- a/examples/competitive/run.py +++ b/examples/competitive/run.py @@ -1,10 +1,9 @@ -# pyre-ignore-all-errors[16, 21] import sys -from __init__ import run_ladder_game +from examples.competitive.__init__ import run_ladder_game # Load bot -from bot import CompetitiveBot +from examples.competitive.bot import CompetitiveBot from sc2 import maps from sc2.data import Difficulty, Race diff --git a/examples/distributed_workers.py b/examples/distributed_workers.py index 9e7940e5..fa54588c 100644 --- a/examples/distributed_workers.py +++ b/examples/distributed_workers.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] from sc2 import maps from sc2.bot_ai import BotAI from sc2.data import Difficulty, Race diff --git a/examples/fastreload.py b/examples/fastreload.py index 4fc5439a..0837e8f3 100644 --- a/examples/fastreload.py +++ b/examples/fastreload.py @@ -20,7 +20,8 @@ def main(): input("Press enter to reload ") reload(zerg_rush) - player_config[0].ai = zerg_rush.ZergRushBot() + if isinstance(player_config[0], Bot): + player_config[0].ai = zerg_rush.ZergRushBot() gen.send(player_config) diff --git a/examples/host_external_norestart.py b/examples/host_external_norestart.py index c5626ac0..c3e33255 100644 --- a/examples/host_external_norestart.py +++ b/examples/host_external_norestart.py @@ -10,6 +10,7 @@ def main(): portconfig: Portconfig = Portconfig() print(portconfig.as_json) + # pyrefly: ignore player_config = [Bot(Race.Zerg, ZergRushBot()), Bot(Race.Zerg, None)] for g in _host_game_iter(maps.get("Abyssal Reef LE"), player_config, realtime=False, portconfig=portconfig): diff --git a/examples/protoss/find_adept_shades.py b/examples/protoss/find_adept_shades.py index d10896d3..2b287f55 100644 --- a/examples/protoss/find_adept_shades.py +++ b/examples/protoss/find_adept_shades.py @@ -18,7 +18,7 @@ def __init__(self): async def on_start(self): self.client.game_step = 2 await self.client.debug_create_unit( - [[UnitTypeId.ADEPT, 10, self.townhalls[0].position.towards(self.game_info.map_center, 5), 1]] + [(UnitTypeId.ADEPT, 10, self.townhalls[0].position.towards(self.game_info.map_center, 5), 1)] ) async def on_step(self, iteration: int): @@ -26,7 +26,6 @@ async def on_step(self, iteration: int): if adepts and not self.shaded: # Wait for adepts to spawn and then cast ability for adept in adepts: - # pyre-ignore[16] adept(AbilityId.ADEPTPHASESHIFT_ADEPTPHASESHIFT, self.game_info.map_center) self.shaded = True elif self.shades_mapping: @@ -38,7 +37,6 @@ async def on_step(self, iteration: int): # logger.info(f"Remaining shade time: {shade.buff_duration_remain} / {shade.buff_duration_max}") pass if adept and shade: - # pyre-ignore[16] self.client.debug_line_out(adept, shade, (0, 255, 0)) # logger.info(self.shades_mapping) elif self.shaded: @@ -53,7 +51,7 @@ async def on_step(self, iteration: int): previous_shade_location = shade.position.towards( forward_position, -(self.client.game_step / 16) * shade.movement_speed ) # See docstring of movement_speed attribute - # pyre-ignore[6] + closest_adept = remaining_adepts.closest_to(previous_shade_location) self.shades_mapping[closest_adept.tag] = shade.tag diff --git a/examples/protoss/threebase_voidray.py b/examples/protoss/threebase_voidray.py index 314f6696..be6429b7 100644 --- a/examples/protoss/threebase_voidray.py +++ b/examples/protoss/threebase_voidray.py @@ -94,16 +94,12 @@ async def on_step(self, iteration: int): for nexus in self.townhalls.ready: vgs = self.vespene_geyser.closer_than(15, nexus) for vg in vgs: - if not self.can_afford(UnitTypeId.ASSIMILATOR): - break - - worker = self.select_build_worker(vg.position) - if worker is None: - break - - if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): - worker.build_gas(vg) - worker.stop(queue=True) + if self.can_afford(UnitTypeId.ASSIMILATOR): + worker = self.select_build_worker(vg.position) + if worker is not None: + if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): + worker.build_gas(vg) + worker.stop(queue=True) # If we have less than 3 but at least 3 nexuses, build stargate if self.structures(UnitTypeId.PYLON).ready and self.structures(UnitTypeId.CYBERNETICSCORE).ready: diff --git a/examples/protoss/warpgate_push.py b/examples/protoss/warpgate_push.py index f1b36079..70404f89 100644 --- a/examples/protoss/warpgate_push.py +++ b/examples/protoss/warpgate_push.py @@ -79,14 +79,12 @@ async def on_step(self, iteration: int): for nexus in self.townhalls.ready: vgs = self.vespene_geyser.closer_than(15, nexus) for vg in vgs: - if not self.can_afford(UnitTypeId.ASSIMILATOR): - break - worker = self.select_build_worker(vg.position) - if worker is None: - break - if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): - worker.build_gas(vg) - worker.stop(queue=True) + if self.can_afford(UnitTypeId.ASSIMILATOR): + worker = self.select_build_worker(vg.position) + if worker is not None: + if not self.gas_buildings or not self.gas_buildings.closer_than(1, vg): + worker.build_gas(vg) + worker.stop(queue=True) # Research warp gate if cybercore is completed if ( diff --git a/examples/simulate_fight_scenario.py b/examples/simulate_fight_scenario.py index 2bee4390..00c95278 100644 --- a/examples/simulate_fight_scenario.py +++ b/examples/simulate_fight_scenario.py @@ -42,19 +42,21 @@ async def on_step(self, iteration: int): return async def reset_arena(self): + if self.enemy_location is None: + return await self.client.debug_kill_unit(self.all_units) await self.client.debug_create_unit( [ - [UnitTypeId.SUPPLYDEPOT, 1, self.enemy_location, OPPONENT_PLAYER_ID], - [UnitTypeId.MARINE, 4, self.enemy_location.towards(self.start_location, 8), OPPONENT_PLAYER_ID], + (UnitTypeId.SUPPLYDEPOT, 1, self.enemy_location, OPPONENT_PLAYER_ID), + (UnitTypeId.MARINE, 4, self.enemy_location.towards(self.start_location, 8), OPPONENT_PLAYER_ID), ] ) await self.client.debug_create_unit( [ - [UnitTypeId.SUPPLYDEPOT, 1, self.start_location, MY_PLAYER_ID], - [UnitTypeId.MARINE, 4, self.start_location.towards(self.enemy_location, 8), MY_PLAYER_ID], + (UnitTypeId.SUPPLYDEPOT, 1, self.start_location, MY_PLAYER_ID), + (UnitTypeId.MARINE, 4, self.start_location.towards(self.enemy_location, 8), MY_PLAYER_ID), ] ) @@ -63,6 +65,8 @@ async def manage_enemy_units(self): unit.attack(self.start_location) async def manage_own_units(self): + if self.enemy_location is None: + return for unit in self.units(UnitTypeId.MARINE): unit.attack(self.enemy_location) # TODO: implement your fight logic here diff --git a/examples/terran/cyclone_push.py b/examples/terran/cyclone_push.py index f66a18bb..e0aeb6ae 100644 --- a/examples/terran/cyclone_push.py +++ b/examples/terran/cyclone_push.py @@ -78,31 +78,29 @@ async def on_step(self, iteration: int): # Near same command as above with the depot await self.build(UnitTypeId.BARRACKS, near=cc.position.towards(self.game_info.map_center, 8)) - # If we have a barracks (complete or under construction) and less than 2 gas structures (here: refineries) - elif self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2: - if self.can_afford(UnitTypeId.REFINERY): - # All the vespene geysirs nearby, including ones with a refinery on top of it - vgs = self.vespene_geyser.closer_than(10, cc) - for vg in vgs: - if self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1): - continue - # Select a worker closest to the vespene geysir - worker: Unit | None = self.select_build_worker(vg) - # Worker can be none in cases where all workers are dead - # or 'select_build_worker' function only selects from workers which carry no minerals - if worker is None: - continue + # If we have a barracks (complete or under construction) and less than 2 gas structures (here: refineries) + if self.structures(UnitTypeId.BARRACKS) and self.gas_buildings.amount < 2: + if self.can_afford(UnitTypeId.REFINERY): + # All the vespene geysirs nearby, including ones with a refinery on top of it + vgs = self.vespene_geyser.closer_than(10, cc) + for vg in vgs: + has_refinery = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1) + if has_refinery: + continue + # Select a worker closest to the vespene geysir + worker: Unit | None = self.select_build_worker(vg) + # Worker can be none in cases where all workers are dead + # or 'select_build_worker' function only selects from workers which carry no minerals + if worker is not None: # Issue the build command to the worker, important: vg has to be a Unit, not a position worker.build_gas(vg) - # Only issue one build geysir command per frame - break - - # If we have at least one barracks that is compelted, build factory - if self.structures(UnitTypeId.BARRACKS).ready: - if self.structures(UnitTypeId.FACTORY).amount < 3 and not self.already_pending(UnitTypeId.FACTORY): - if self.can_afford(UnitTypeId.FACTORY): - position: Point2 = cc.position.towards_with_random_angle(self.game_info.map_center, 16) - await self.build(UnitTypeId.FACTORY, near=position) + + # If we have at least one barracks that is completed, build factory + if self.structures(UnitTypeId.BARRACKS).ready: + if self.structures(UnitTypeId.FACTORY).amount < 3 and not self.already_pending(UnitTypeId.FACTORY): + if self.can_afford(UnitTypeId.FACTORY): + position: Point2 = cc.position.towards_with_random_angle(self.game_info.map_center, 16) + await self.build(UnitTypeId.FACTORY, near=position) for factory in self.structures(UnitTypeId.FACTORY).ready.idle: # Reactor allows us to build two at a time diff --git a/examples/terran/mass_reaper.py b/examples/terran/mass_reaper.py index 4ca3f1aa..6e7f598c 100644 --- a/examples/terran/mass_reaper.py +++ b/examples/terran/mass_reaper.py @@ -6,7 +6,10 @@ Bot made by Burny """ +from __future__ import annotations + import random +from typing import Literal from sc2 import maps from sc2.bot_ai import BotAI @@ -124,9 +127,6 @@ async def on_step(self, iteration: int): # Caution: the target for the refinery has to be the vespene geyser, not its position! worker.build_gas(vg) - # Dont build more than one each frame - break - # Make scvs until 22, usually you only need 1:1 mineral:gas ratio for reapers, but if you don't lose any then you will need additional depots (mule income should take care of that) # Stop scv production when barracks is complete but we still have a command center (priotize morphing to orbital command) if ( @@ -156,32 +156,32 @@ async def on_step(self, iteration: int): # Reaper micro enemies: Units = self.enemy_units | self.enemy_structures enemies_can_attack: Units = enemies.filter(lambda unit: unit.can_attack_ground) - for r in self.units(UnitTypeId.REAPER): + for reaper_unit in self.units(UnitTypeId.REAPER): # Move to range 15 of closest unit if reaper is below 20 hp and not regenerating enemy_threats_close: Units = enemies_can_attack.filter( - lambda unit: unit.distance_to(r) < 15 + lambda unit: unit.distance_to(reaper_unit) < 15 ) # Threats that can attack the reaper - if r.health_percentage < 2 / 5 and enemy_threats_close: - retreat_points: set[Point2] = self.neighbors8(r.position, distance=2) | self.neighbors8( - r.position, distance=4 + if reaper_unit.health_percentage < 2 / 5 and enemy_threats_close: + retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8( + reaper_unit.position, distance=4 ) # Filter points that are pathable retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)} if retreat_points: - closest_enemy: Unit = enemy_threats_close.closest_to(r) + closest_enemy: Unit = enemy_threats_close.closest_to(reaper_unit) retreat_point: Point2 = closest_enemy.position.furthest(retreat_points) - r.move(retreat_point) + reaper_unit.move(retreat_point) continue # Continue for loop, dont execute any of the following # Reaper is ready to attack, shoot nearest ground unit enemy_ground_units: Units = enemies.filter( - lambda unit: unit.distance_to(r) < 5 and not unit.is_flying + lambda unit: unit.distance_to(reaper_unit) < 5 and not unit.is_flying ) # Hardcoded attackrange of 5 - if r.weapon_cooldown == 0 and enemy_ground_units: - enemy_ground_units: Units = enemy_ground_units.sorted(lambda x: x.distance_to(r)) + if reaper_unit.weapon_cooldown == 0 and enemy_ground_units: + enemy_ground_units: Units = enemy_ground_units.sorted(lambda x: x.distance_to(reaper_unit)) closest_enemy: Unit = enemy_ground_units[0] - r.attack(closest_enemy) + reaper_unit.attack(closest_enemy) continue # Continue for loop, dont execute any of the following # Attack is on cooldown, check if grenade is on cooldown, if not then throw it to furthest enemy in range 5 @@ -192,51 +192,53 @@ async def on_step(self, iteration: int): lambda unit: not unit.is_structure and not unit.is_flying and unit.type_id not in {UnitTypeId.LARVA, UnitTypeId.EGG} - and unit.distance_to(r) < reaper_grenade_range + and unit.distance_to(reaper_unit) < reaper_grenade_range ) - if enemy_ground_units_in_grenade_range and (r.is_attacking or r.is_moving): + if enemy_ground_units_in_grenade_range and (reaper_unit.is_attacking or reaper_unit.is_moving): # If AbilityId.KD8CHARGE_KD8CHARGE in abilities, we check that to see if the reaper grenade is off cooldown - abilities = await self.get_available_abilities(r) + abilities: list[AbilityId] = await self.get_available_abilities(reaper_unit) # pyrefly: ignore enemy_ground_units_in_grenade_range = enemy_ground_units_in_grenade_range.sorted( - lambda x: x.distance_to(r), reverse=True + lambda x: x.distance_to(reaper_unit), reverse=True ) - furthest_enemy: Unit = None + furthest_enemy: Unit | None = None for enemy in enemy_ground_units_in_grenade_range: - if await self.can_cast(r, AbilityId.KD8CHARGE_KD8CHARGE, enemy, cached_abilities_of_unit=abilities): - furthest_enemy: Unit = enemy + if await self.can_cast( + reaper_unit, AbilityId.KD8CHARGE_KD8CHARGE, enemy, cached_abilities_of_unit=abilities + ): + furthest_enemy = enemy break - if furthest_enemy: - r(AbilityId.KD8CHARGE_KD8CHARGE, furthest_enemy) + if furthest_enemy is not None: + reaper_unit(AbilityId.KD8CHARGE_KD8CHARGE, furthest_enemy) continue # Continue for loop, don't execute any of the following # Move to max unit range if enemy is closer than 4 enemy_threats_very_close: Units = enemies.filter( - lambda unit: unit.can_attack_ground and unit.distance_to(r) < 4.5 + lambda unit: unit.can_attack_ground and unit.distance_to(reaper_unit) < 4.5 ) # Hardcoded attackrange minus 0.5 # Threats that can attack the reaper - if r.weapon_cooldown != 0 and enemy_threats_very_close: - retreat_points: set[Point2] = self.neighbors8(r.position, distance=2) | self.neighbors8( - r.position, distance=4 + if reaper_unit.weapon_cooldown != 0 and enemy_threats_very_close: + retreat_points: set[Point2] = self.neighbors8(reaper_unit.position, distance=2) | self.neighbors8( + reaper_unit.position, distance=4 ) # Filter points that are pathable by a reaper retreat_points: set[Point2] = {x for x in retreat_points if self.in_pathing_grid(x)} if retreat_points: - closest_enemy: Unit = enemy_threats_very_close.closest_to(r) + closest_enemy: Unit = enemy_threats_very_close.closest_to(reaper_unit) retreat_point: Point2 = max( - retreat_points, key=lambda x: x.distance_to(closest_enemy) - x.distance_to(r) + retreat_points, key=lambda x: x.distance_to(closest_enemy) - x.distance_to(reaper_unit) ) - r.move(retreat_point) + reaper_unit.move(retreat_point) continue # Continue for loop, don't execute any of the following # Move to nearest enemy ground unit/building because no enemy unit is closer than 5 all_enemy_ground_units: Units = self.enemy_units.not_flying if all_enemy_ground_units: - closest_enemy: Unit = all_enemy_ground_units.closest_to(r) - r.move(closest_enemy) + closest_enemy: Unit = all_enemy_ground_units.closest_to(reaper_unit) + reaper_unit.move(closest_enemy) continue # Continue for loop, don't execute any of the following # Move to random enemy start location if no enemy buildings have been seen - r.move(random.choice(self.enemy_start_locations)) + reaper_unit.move(random.choice(self.enemy_start_locations)) # Manage idle scvs, would be taken care by distribute workers aswell if self.townhalls: @@ -287,7 +289,7 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= # Find all gas_buildings that have surplus or deficit deficit_gas_buildings = {} - surplusgas_buildings = {} + surplus_gas_buildings = {} for g in self.gas_buildings.filter(lambda x: x.vespene_contents > 0): # Only loop over gas_buildings that have still gas in them deficit = g.ideal_harvesters - g.assigned_harvesters @@ -305,11 +307,11 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= w = surplus_workers.pop() worker_pool.append(w) worker_pool_tags.add(w.tag) - surplusgas_buildings[g.tag] = {"unit": g, "deficit": deficit} + surplus_gas_buildings[g.tag] = {"unit": g, "deficit": deficit} # Find all townhalls that have surplus or deficit - deficit_townhalls = {} - surplus_townhalls = {} + deficit_townhalls: dict[int, dict[Literal["unit", "deficit"], Unit | int]] = {} + surplus_townhalls: dict[int, dict[Literal["unit", "deficit"], Unit | int]] = {} if not only_saturate_gas: for th in self.townhalls: deficit = th.ideal_harvesters - th.assigned_harvesters @@ -333,7 +335,7 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= if all( [ len(deficit_gas_buildings) == 0, - len(surplusgas_buildings) == 0, + len(surplus_gas_buildings) == 0, len(surplus_townhalls) == 0 or deficit_townhalls == 0, ] ): @@ -341,15 +343,26 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= return # Check if deficit in gas less or equal than what we have in surplus, else grab some more workers from surplus bases + # pyrefly: ignore deficit_gas_count = sum( - gasInfo["deficit"] for gasTag, gasInfo in deficit_gas_buildings.items() if gasInfo["deficit"] > 0 + # pyrefly: ignore + gas_info["deficit"] + for _gas_tag, gas_info in deficit_gas_buildings.items() + # pyrefly: ignore + if gas_info["deficit"] > 0 ) surplus_count = sum( - -gasInfo["deficit"] for gasTag, gasInfo in surplusgas_buildings.items() if gasInfo["deficit"] < 0 + # pyrefly: ignore + -gas_info["deficit"] + for _gas_tag, gas_info in surplus_gas_buildings.items() + # pyrefly: ignore + if gas_info["deficit"] < 0 ) surplus_count += sum( + # pyrefly: ignore -townhall_info["deficit"] - for townhall_tag, townhall_info in surplus_townhalls.items() + for _townhall_tag, townhall_info in surplus_townhalls.items() + # pyrefly: ignore if townhall_info["deficit"] < 0 ) @@ -358,6 +371,7 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= for _gas_tag, gas_info in deficit_gas_buildings.items(): if worker_pool.amount >= deficit_gas_count: break + # pyrefly: ignore workers_near_gas = self.workers.closer_than(10, gas_info["unit"]).filter( lambda w: w.tag not in worker_pool_tags and len(w.orders) == 1 @@ -374,12 +388,15 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= if performance_heavy: # Sort furthest away to closest (as the pop() function will take the last element) worker_pool.sort(key=lambda x: x.distance_to(gas_info["unit"]), reverse=True) + # pyrefly: ignore for _ in range(gas_info["deficit"]): if worker_pool.amount > 0: w = worker_pool.pop() if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: + # pyrefly: ignore w.gather(gas_info["unit"], queue=True) else: + # pyrefly: ignore w.gather(gas_info["unit"]) if not only_saturate_gas: @@ -388,10 +405,15 @@ async def my_distribute_workers(self, performance_heavy=True, only_saturate_gas= if performance_heavy: # Sort furthest away to closest (as the pop() function will take the last element) worker_pool.sort(key=lambda x: x.distance_to(townhall_info["unit"]), reverse=True) + # pyrefly: ignore for _ in range(townhall_info["deficit"]): if worker_pool.amount > 0: w = worker_pool.pop() - mf = self.mineral_field.closer_than(10, townhall_info["unit"]).closest_to(w) + mf = self.mineral_field.closer_than( + 10, + # pyrefly: ignore + townhall_info["unit"], + ).closest_to(w) if len(w.orders) == 1 and w.orders[0].ability.id in [AbilityId.HARVEST_RETURN]: w.gather(mf, queue=True) else: diff --git a/examples/terran/onebase_battlecruiser.py b/examples/terran/onebase_battlecruiser.py index 47cd5f62..1c0b1f58 100644 --- a/examples/terran/onebase_battlecruiser.py +++ b/examples/terran/onebase_battlecruiser.py @@ -24,7 +24,6 @@ def select_target(self) -> tuple[Point2, bool]: return targets.random.position, True if self.units and min(u.position.distance_to(self.enemy_start_locations[0]) for u in self.units) < 5: - # pyre-ignore[7] return self.enemy_start_locations[0].position, False return self.mineral_field.random.position, False @@ -60,11 +59,10 @@ async def on_step(self, iteration: int): # Build more BCs if self.structures(UnitTypeId.FUSIONCORE) and self.can_afford(UnitTypeId.BATTLECRUISER): - for sp in self.structures(UnitTypeId.STARPORT).idle: - if sp.has_add_on: - if not self.can_afford(UnitTypeId.BATTLECRUISER): - break - sp.train(UnitTypeId.BATTLECRUISER) + for starport in self.structures(UnitTypeId.STARPORT).idle: + if starport.has_add_on: + if self.can_afford(UnitTypeId.BATTLECRUISER): + starport.train(UnitTypeId.BATTLECRUISER) # Build more supply depots if self.supply_left < 6 and self.supply_used >= 14 and not self.already_pending(UnitTypeId.SUPPLYDEPOT): @@ -82,15 +80,11 @@ async def on_step(self, iteration: int): if self.can_afford(UnitTypeId.REFINERY): vgs: Units = self.vespene_geyser.closer_than(20, cc) for vg in vgs: - if self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1): - break - - worker: Unit | None = self.select_build_worker(vg.position) - if worker is None: - break - - worker.build_gas(vg) - break + has_gas_building = self.gas_buildings.filter(lambda unit: unit.distance_to(vg) < 1) + if not has_gas_building: + worker: Unit | None = self.select_build_worker(vg.position) + if worker is not None: + worker.build_gas(vg) # Build factory if we dont have one if self.tech_requirement_progress(UnitTypeId.FACTORY) == 1: @@ -121,47 +115,53 @@ def starport_points_to_build_addon(sp_position: Point2) -> list[Point2]: return addon_points # Build starport techlab or lift if no room to build techlab - sp: Unit - for sp in self.structures(UnitTypeId.STARPORT).ready.idle: - if not sp.has_add_on and self.can_afford(UnitTypeId.STARPORTTECHLAB): - addon_points = starport_points_to_build_addon(sp.position) + starport: Unit + for starport in self.structures(UnitTypeId.STARPORT).ready.idle: + if not starport.has_add_on and self.can_afford(UnitTypeId.STARPORTTECHLAB): + addon_points = starport_points_to_build_addon(starport.position) if all( self.in_map_bounds(addon_point) and self.in_placement_grid(addon_point) and self.in_pathing_grid(addon_point) for addon_point in addon_points ): - sp.build(UnitTypeId.STARPORTTECHLAB) + starport.build(UnitTypeId.STARPORTTECHLAB) else: - sp(AbilityId.LIFT) + starport(AbilityId.LIFT) def starport_land_positions(sp_position: Point2) -> list[Point2]: """Return all points that need to be checked when trying to land at a location where there is enough space to build an addon. Returns 13 points.""" land_positions = [(sp_position + Point2((x, y))).rounded for x in range(-1, 2) for y in range(-1, 2)] return land_positions + starport_points_to_build_addon(sp_position) - # Find a position to land for a flying starport so that it can build an addon - for sp in self.structures(UnitTypeId.STARPORTFLYING).idle: - possible_land_positions_offset = sorted( - (Point2((x, y)) for x in range(-10, 10) for y in range(-10, 10)), - key=lambda point: point.x**2 + point.y**2, - ) - offset_point: Point2 = Point2((-0.5, -0.5)) - possible_land_positions = (sp.position.rounded + offset_point + p for p in possible_land_positions_offset) - for target_land_position in possible_land_positions: - land_and_addon_points: list[Point2] = starport_land_positions(target_land_position) - if all( - self.in_map_bounds(land_pos) and self.in_placement_grid(land_pos) and self.in_pathing_grid(land_pos) - for land_pos in land_and_addon_points - ): - sp(AbilityId.LAND, target_land_position) - break + def try_land_starports(): + # Find a position to land for a flying starport so that it can build an addon + for starport in self.structures(UnitTypeId.STARPORTFLYING).idle: + possible_land_positions_offset = sorted( + (Point2((x, y)) for x in range(-10, 10) for y in range(-10, 10)), + key=lambda point: point.x**2 + point.y**2, + ) + offset_point: Point2 = Point2((-0.5, -0.5)) + possible_land_positions = ( + starport.position.rounded + offset_point + p for p in possible_land_positions_offset + ) + for target_land_position in possible_land_positions: + land_and_addon_points: list[Point2] = starport_land_positions(target_land_position) + if all( + self.in_map_bounds(land_pos) + and self.in_placement_grid(land_pos) + and self.in_pathing_grid(land_pos) + for land_pos in land_and_addon_points + ): + starport(AbilityId.LAND, target_land_position) + return + + try_land_starports() # Show where it is flying to and show grid - unit: Unit - for sp in self.structures(UnitTypeId.STARPORTFLYING).filter(lambda unit: not unit.is_idle): - if isinstance(sp.order_target, Point2): - p: Point3 = Point3((*sp.order_target, self.get_terrain_z_height(sp.order_target))) + for starport in self.structures(UnitTypeId.STARPORTFLYING).filter(lambda unit: not unit.is_idle): + if isinstance(starport.order_target, Point2): + p: Point3 = Point3((*starport.order_target, self.get_terrain_z_height(starport.order_target))) self.client.debug_box2_out(p, color=Point3((255, 0, 0))) # Build fusion core diff --git a/examples/terran/ramp_wall.py b/examples/terran/ramp_wall.py index 0aa12fea..3711c1b1 100644 --- a/examples/terran/ramp_wall.py +++ b/examples/terran/ramp_wall.py @@ -31,20 +31,22 @@ async def on_step(self, iteration: int): if self.can_afford(UnitTypeId.SCV) and self.workers.amount < 16 and cc.is_idle: cc.train(UnitTypeId.SCV) - # Raise depos when enemies are nearby - for depo in self.structures(UnitTypeId.SUPPLYDEPOT).ready: - for unit in self.enemy_units: - if unit.distance_to(depo) < 15: - break - else: - depo(AbilityId.MORPH_SUPPLYDEPOT_LOWER) - - # Lower depos when no enemies are nearby - for depo in self.structures(UnitTypeId.SUPPLYDEPOTLOWERED).ready: - for unit in self.enemy_units: - if unit.distance_to(depo) < 10: - depo(AbilityId.MORPH_SUPPLYDEPOT_RAISE) - break + def raise_and_lower_depots(): + # Raise depos when enemies are nearby + for depo in self.structures(UnitTypeId.SUPPLYDEPOT).ready: + for unit in self.enemy_units: + if unit.distance_to(depo) < 15: + return + else: + depo(AbilityId.MORPH_SUPPLYDEPOT_LOWER) + # Lower depos when no enemies are nearby + for depo in self.structures(UnitTypeId.SUPPLYDEPOTLOWERED).ready: + for unit in self.enemy_units: + if unit.distance_to(depo) < 10: + depo(AbilityId.MORPH_SUPPLYDEPOT_RAISE) + return + + raise_and_lower_depots() # Draw ramp points self.draw_ramp_points() @@ -252,7 +254,7 @@ def draw_facing_units(self): for selected_unit2 in self.units.selected: if selected_unit1 == selected_unit2: continue - if selected_unit2.is_facing_unit(selected_unit1): + if selected_unit2.is_facing(selected_unit1): self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=green) else: self.client.debug_box2_out(selected_unit2, half_vertex_length=0.25, color=red) diff --git a/examples/watch_replay.py b/examples/watch_replay.py index 251eb5ad..5d9793c3 100644 --- a/examples/watch_replay.py +++ b/examples/watch_replay.py @@ -21,7 +21,7 @@ async def on_step(self, iteration: int): if __name__ == "__main__": - my_observer_ai = ObserverBot() + my_observer_ai = ObserverBot() # pyrefly: ignore # Enter replay name here # The replay should be either in this folder and you can give it a relative path, or change it to the absolute path replay_name = "WorkerRush.SC2Replay" diff --git a/examples/worker_stack_bot.py b/examples/worker_stack_bot.py index 0a0cbbb2..805b24d6 100644 --- a/examples/worker_stack_bot.py +++ b/examples/worker_stack_bot.py @@ -89,7 +89,7 @@ async def on_step(self, iteration: int): # Move worker in front of the nexus to avoid deceleration until the last moment if worker.distance_to(th) > th.radius + worker.radius + self.townhall_distance_threshold: pos: Point2 = th.position - # pyre-ignore[6] + worker.move(pos.towards(worker, th.radius * self.townhall_distance_factor)) worker.return_resource(queue=True) else: @@ -97,7 +97,7 @@ async def on_step(self, iteration: int): worker.gather(mineral, queue=True) # Print info every 30 game-seconds - # pyre-ignore[16] + if self.state.game_loop % (22.4 * 30) == 0: logger.info(f"{self.time_formatted} Mined a total of {int(self.state.score.collected_minerals)} minerals") diff --git a/examples/zerg/banes_banes_banes.py b/examples/zerg/banes_banes_banes.py index c5216977..8d105423 100644 --- a/examples/zerg/banes_banes_banes.py +++ b/examples/zerg/banes_banes_banes.py @@ -99,7 +99,7 @@ async def on_step(self, iteration: int): for vg in self.vespene_geyser.closer_than(10, hq): drone: Unit = self.workers.random drone.build_gas(vg) - break + return # If we have less than 22 drones, build drones if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: diff --git a/examples/zerg/hydralisk_push.py b/examples/zerg/hydralisk_push.py index 34f80003..56d7a939 100644 --- a/examples/zerg/hydralisk_push.py +++ b/examples/zerg/hydralisk_push.py @@ -86,6 +86,13 @@ async def on_step(self, iteration: int): if self.can_afford(UnitTypeId.HYDRALISKDEN): await self.build(UnitTypeId.HYDRALISKDEN, near=hq.position.towards(self.game_info.map_center, 5)) + # If we have less than 22 drones, build drones + if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: + if larvae and self.can_afford(UnitTypeId.DRONE): + larva: Unit = larvae.random + larva.train(UnitTypeId.DRONE) + return + # If we dont have both extractors: build them if ( self.structures(UnitTypeId.SPAWNINGPOOL) @@ -93,17 +100,10 @@ async def on_step(self, iteration: int): ): if self.can_afford(UnitTypeId.EXTRACTOR): # May crash if we dont have any drones - for vg in self.vespene_geyser.closer_than(10, hq): + for vespene_geyser in self.vespene_geyser.closer_than(10, hq): drone: Unit = self.workers.random - drone.build_gas(vg) - break - - # If we have less than 22 drones, build drones - if self.supply_workers + self.already_pending(UnitTypeId.DRONE) < 22: - if larvae and self.can_afford(UnitTypeId.DRONE): - larva: Unit = larvae.random - larva.train(UnitTypeId.DRONE) - return + drone.build_gas(vespene_geyser) + return # Saturate gas for a in self.gas_buildings: diff --git a/examples/zerg/zerg_rush.py b/examples/zerg/zerg_rush.py index 15d0df50..e8a6fedb 100644 --- a/examples/zerg/zerg_rush.py +++ b/examples/zerg/zerg_rush.py @@ -136,7 +136,6 @@ def draw_creep_pixelmap(self): color = Point3((0, 255, 0)) self.client.debug_box2_out(pos, half_vertex_length=0.25, color=color) - # pyre-ignore[11] async def on_end(self, game_result: Result): self.on_end_called = True logger.info(f"{self.time_formatted} On end was called") diff --git a/pyproject.toml b/pyproject.toml index d3b38928..84676776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dev = [ "pyglet>=2.0.20", "pylint>=3.3.2", # Type checker - "pyrefly>=0.21.0", + "pyrefly>=0.46.3", "pytest>=8.3.4", "pytest-asyncio>=0.25.0", "pytest-benchmark>=5.1.0", @@ -93,7 +93,19 @@ dedent_closing_brackets = true allow_split_before_dict_value = false [tool.pyrefly] -project_includes = ["sc2", "examples", "test"] +project_includes = [ + "sc2", + "examples", + "test" +] +project-excludes = [ + # Disable for those files and folders + "sc2/data.py", +] + +[tool.pyrefly.errors] +bad-override = false +inconsistent-overload = false [tool.ruff] target-version = 'py310' @@ -132,11 +144,11 @@ ignore = [ "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` ] -[tool.ruff.pyupgrade] +[tool.pyupgrade] # Preserve types, even if a file imports `from __future__ import annotations`. # Remove once support for py3.8 and 3.9 is dropped keep-runtime-typing = true -[tool.ruff.pep8-naming] +[tool.pep8-naming] # Allow Pydantic's `@validator` decorator to trigger class method treatment. classmethod-decorators = ["pydantic.validator", "classmethod"] diff --git a/s2clientprotocol/data_pb2.pyi b/s2clientprotocol/data_pb2.pyi index 8b839cf5..dd663eb7 100644 --- a/s2clientprotocol/data_pb2.pyi +++ b/s2clientprotocol/data_pb2.pyi @@ -1,4 +1,4 @@ -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message @@ -71,7 +71,7 @@ class TargetType(Enum): class Weapon(Message): type: int damage: float - damage_bonus: Iterable[DamageBonus] + damage_bonus: Sequence[DamageBonus] attacks: int range: float speed: float @@ -79,7 +79,7 @@ class Weapon(Message): self, type: int = ..., damage: float = ..., - damage_bonus: Iterable[DamageBonus] = ..., + damage_bonus: Sequence[DamageBonus] = ..., attacks: int = ..., range: float = ..., speed: float = ..., @@ -100,14 +100,14 @@ class UnitTypeData(Message): has_vespene: bool has_minerals: bool sight_range: float - tech_alias: Iterable[int] + tech_alias: Sequence[int] unit_alias: int tech_requirement: int require_attached: bool - attributes: Iterable[int] + attributes: Sequence[int] movement_speed: float armor: float - weapons: Iterable[Weapon] + weapons: Sequence[Weapon] def __init__( self, unit_id: int = ..., @@ -124,14 +124,14 @@ class UnitTypeData(Message): has_vespene: bool = ..., has_minerals: bool = ..., sight_range: float = ..., - tech_alias: Iterable[int] = ..., + tech_alias: Sequence[int] = ..., unit_alias: int = ..., tech_requirement: int = ..., require_attached: bool = ..., - attributes: Iterable[int] = ..., + attributes: Sequence[int] = ..., movement_speed: float = ..., armor: float = ..., - weapons: Iterable[Weapon] = ..., + weapons: Sequence[Weapon] = ..., ) -> None: ... class UpgradeData(Message): diff --git a/s2clientprotocol/debug_pb2.pyi b/s2clientprotocol/debug_pb2.pyi index edc956ee..49f52525 100644 --- a/s2clientprotocol/debug_pb2.pyi +++ b/s2clientprotocol/debug_pb2.pyi @@ -1,4 +1,4 @@ -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message @@ -27,16 +27,16 @@ class DebugCommand(Message): ) -> None: ... class DebugDraw(Message): - text: Iterable[DebugText] - lines: Iterable[DebugLine] - boxes: Iterable[DebugBox] - spheres: Iterable[DebugSphere] + text: Sequence[DebugText] + lines: Sequence[DebugLine] + boxes: Sequence[DebugBox] + spheres: Sequence[DebugSphere] def __init__( self, - text: Iterable[DebugText] = ..., - lines: Iterable[DebugLine] = ..., - boxes: Iterable[DebugBox] = ..., - spheres: Iterable[DebugSphere] = ..., + text: Sequence[DebugText] = ..., + lines: Sequence[DebugLine] = ..., + boxes: Sequence[DebugBox] = ..., + spheres: Sequence[DebugSphere] = ..., ) -> None: ... class Line(Message): @@ -110,8 +110,8 @@ class DebugCreateUnit(Message): ) -> None: ... class DebugKillUnit(Message): - tag: Iterable[int] - def __init__(self, tag: Iterable[int] = ...) -> None: ... + tag: Sequence[int] + def __init__(self, tag: Sequence[int] = ...) -> None: ... class Test(Enum): hang: int diff --git a/s2clientprotocol/query_pb2.pyi b/s2clientprotocol/query_pb2.pyi index 746d86d9..67831b5f 100644 --- a/s2clientprotocol/query_pb2.pyi +++ b/s2clientprotocol/query_pb2.pyi @@ -1,31 +1,31 @@ -from collections.abc import Iterable +from collections.abc import Sequence from google.protobuf.message import Message from .common_pb2 import AvailableAbility, Point2D class RequestQuery(Message): - pathing: Iterable[RequestQueryPathing] - abilities: Iterable[RequestQueryAvailableAbilities] - placements: Iterable[RequestQueryBuildingPlacement] + pathing: Sequence[RequestQueryPathing] + abilities: Sequence[RequestQueryAvailableAbilities] + placements: Sequence[RequestQueryBuildingPlacement] ignore_resource_requirements: bool def __init__( self, - pathing: Iterable[RequestQueryPathing] = ..., - abilities: Iterable[RequestQueryAvailableAbilities] = ..., - placements: Iterable[RequestQueryBuildingPlacement] = ..., + pathing: Sequence[RequestQueryPathing] = ..., + abilities: Sequence[RequestQueryAvailableAbilities] = ..., + placements: Sequence[RequestQueryBuildingPlacement] = ..., ignore_resource_requirements: bool = ..., ) -> None: ... class ResponseQuery(Message): - pathing: Iterable[ResponseQueryPathing] - abilities: Iterable[ResponseQueryAvailableAbilities] - placements: Iterable[ResponseQueryBuildingPlacement] + pathing: Sequence[ResponseQueryPathing] + abilities: Sequence[ResponseQueryAvailableAbilities] + placements: Sequence[ResponseQueryBuildingPlacement] def __init__( self, - pathing: Iterable[ResponseQueryPathing] = ..., - abilities: Iterable[ResponseQueryAvailableAbilities] = ..., - placements: Iterable[ResponseQueryBuildingPlacement] = ..., + pathing: Sequence[ResponseQueryPathing] = ..., + abilities: Sequence[ResponseQueryAvailableAbilities] = ..., + placements: Sequence[ResponseQueryBuildingPlacement] = ..., ) -> None: ... class RequestQueryPathing(Message): @@ -48,12 +48,12 @@ class RequestQueryAvailableAbilities(Message): def __init__(self, unit_tag: int = ...) -> None: ... class ResponseQueryAvailableAbilities(Message): - abilities: Iterable[AvailableAbility] + abilities: Sequence[AvailableAbility] unit_tag: int unit_type_id: int def __init__( self, - abilities: Iterable[AvailableAbility] = ..., + abilities: Sequence[AvailableAbility] = ..., unit_tag: int = ..., unit_type_id: int = ..., ) -> None: ... diff --git a/s2clientprotocol/raw_pb2.pyi b/s2clientprotocol/raw_pb2.pyi index 34d89c6d..3db88e38 100644 --- a/s2clientprotocol/raw_pb2.pyi +++ b/s2clientprotocol/raw_pb2.pyi @@ -1,4 +1,4 @@ -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message @@ -11,7 +11,7 @@ class StartRaw(Message): terrain_height: ImageData placement_grid: ImageData playable_area: RectangleI - start_locations: Iterable[Point2D] + start_locations: Sequence[Point2D] def __init__( self, map_size: Size2DI = ..., @@ -19,24 +19,24 @@ class StartRaw(Message): terrain_height: ImageData = ..., placement_grid: ImageData = ..., playable_area: RectangleI = ..., - start_locations: Iterable[Point2D] = ..., + start_locations: Sequence[Point2D] = ..., ) -> None: ... class ObservationRaw(Message): player: PlayerRaw - units: Iterable[Unit] + units: Sequence[Unit] map_state: MapState event: Event - effects: Iterable[Effect] - radar: Iterable[RadarRing] + effects: Sequence[Effect] + radar: Sequence[RadarRing] def __init__( self, player: PlayerRaw = ..., - units: Iterable[Unit] = ..., + units: Sequence[Unit] = ..., map_state: MapState = ..., event: Event = ..., - effects: Iterable[Effect] = ..., - radar: Iterable[RadarRing] = ..., + effects: Sequence[Effect] = ..., + radar: Sequence[RadarRing] = ..., ) -> None: ... class RadarRing(Message): @@ -51,14 +51,14 @@ class PowerSource(Message): def __init__(self, pos: Point = ..., radius: float = ..., tag: int = ...) -> None: ... class PlayerRaw(Message): - power_sources: Iterable[PowerSource] + power_sources: Sequence[PowerSource] camera: Point - upgrade_ids: Iterable[int] + upgrade_ids: Sequence[int] def __init__( self, - power_sources: Iterable[PowerSource] = ..., + power_sources: Sequence[PowerSource] = ..., camera: Point = ..., - upgrade_ids: Iterable[int] = ..., + upgrade_ids: Sequence[int] = ..., ) -> None: ... class UnitOrder(Message): @@ -130,7 +130,7 @@ class Unit(Message): radius: float build_progress: float cloak: int - buff_ids: Iterable[int] + buff_ids: Sequence[int] detect_range: float radar_range: float is_selected: bool @@ -152,9 +152,9 @@ class Unit(Message): is_flying: bool is_burrowed: bool is_hallucination: bool - orders: Iterable[UnitOrder] + orders: Sequence[UnitOrder] add_on_tag: int - passengers: Iterable[PassengerUnit] + passengers: Sequence[PassengerUnit] cargo_space_taken: int cargo_space_max: int assigned_harvesters: int @@ -163,7 +163,7 @@ class Unit(Message): engaged_target_tag: int buff_duration_remain: int buff_duration_max: int - rally_targets: Iterable[RallyTarget] + rally_targets: Sequence[RallyTarget] def __init__( self, display_type: int = ..., @@ -176,7 +176,7 @@ class Unit(Message): radius: float = ..., build_progress: float = ..., cloak: int = ..., - buff_ids: Iterable[int] = ..., + buff_ids: Sequence[int] = ..., detect_range: float = ..., radar_range: float = ..., is_selected: bool = ..., @@ -198,9 +198,9 @@ class Unit(Message): is_flying: bool = ..., is_burrowed: bool = ..., is_hallucination: bool = ..., - orders: Iterable[UnitOrder] = ..., + orders: Sequence[UnitOrder] = ..., add_on_tag: int = ..., - passengers: Iterable[PassengerUnit] = ..., + passengers: Sequence[PassengerUnit] = ..., cargo_space_taken: int = ..., cargo_space_max: int = ..., assigned_harvesters: int = ..., @@ -209,7 +209,7 @@ class Unit(Message): engaged_target_tag: int = ..., buff_duration_remain: int = ..., buff_duration_max: int = ..., - rally_targets: Iterable[RallyTarget] = ..., + rally_targets: Sequence[RallyTarget] = ..., ) -> None: ... class MapState(Message): @@ -218,19 +218,19 @@ class MapState(Message): def __init__(self, visibility: ImageData = ..., creep: ImageData = ...) -> None: ... class Event(Message): - dead_units: Iterable[int] - def __init__(self, dead_units: Iterable[int] = ...) -> None: ... + dead_units: Sequence[int] + def __init__(self, dead_units: Sequence[int] = ...) -> None: ... class Effect(Message): effect_id: int - pos: Iterable[Point2D] + pos: Sequence[Point2D] alliance: int owner: int radius: float def __init__( self, effect_id: int = ..., - pos: Iterable[Point2D] = ..., + pos: Sequence[Point2D] = ..., alliance: int = ..., owner: int = ..., radius: float = ..., @@ -251,14 +251,14 @@ class ActionRawUnitCommand(Message): ability_id: int target_world_space_pos: Point2D target_unit_tag: int - unit_tags: Iterable[int] + unit_tags: Sequence[int] queue_command: bool def __init__( self, ability_id: int = ..., target_world_space_pos: Point2D = ..., target_unit_tag: int = ..., - unit_tags: Iterable[int] = ..., + unit_tags: Sequence[int] = ..., queue_command: bool = ..., ) -> None: ... @@ -268,5 +268,5 @@ class ActionRawCameraMove(Message): class ActionRawToggleAutocast(Message): ability_id: int - unit_tags: Iterable[int] - def __init__(self, ability_id: int = ..., unit_tags: Iterable[int] = ...) -> None: ... + unit_tags: Sequence[int] + def __init__(self, ability_id: int = ..., unit_tags: Sequence[int] = ...) -> None: ... diff --git a/s2clientprotocol/sc2api_pb2.pyi b/s2clientprotocol/sc2api_pb2.pyi index 67574e00..629d020e 100644 --- a/s2clientprotocol/sc2api_pb2.pyi +++ b/s2clientprotocol/sc2api_pb2.pyi @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message @@ -90,7 +90,7 @@ class Response(Message): ping: ResponsePing debug: ResponseDebug id: int - error: Iterable[str] + error: Sequence[str] status: int def __init__( self, @@ -117,7 +117,7 @@ class Response(Message): ping: ResponsePing = ..., debug: ResponseDebug = ..., id: int = ..., - error: Iterable[str] = ..., + error: Sequence[str] = ..., status: int = ..., ) -> None: ... @@ -133,7 +133,7 @@ class Status(Enum): class RequestCreateGame(Message): local_map: LocalMap battlenet_map_name: str - player_setup: Iterable[PlayerSetup] + player_setup: Sequence[PlayerSetup] disable_fog: bool random_seed: int realtime: bool @@ -141,7 +141,7 @@ class RequestCreateGame(Message): self, local_map: LocalMap = ..., battlenet_map_name: str = ..., - player_setup: Iterable[PlayerSetup] = ..., + player_setup: Sequence[PlayerSetup] = ..., disable_fog: bool = ..., random_seed: int = ..., realtime: bool = ..., @@ -172,7 +172,7 @@ class RequestJoinGame(Message): observed_player_id: int options: InterfaceOptions server_ports: PortSet - client_ports: Iterable[PortSet] + client_ports: Sequence[PortSet] shared_port: int player_name: str host_ip: str @@ -182,7 +182,7 @@ class RequestJoinGame(Message): observed_player_id: int = ..., options: InterfaceOptions = ..., server_ports: PortSet = ..., - client_ports: Iterable[PortSet] = ..., + client_ports: Sequence[PortSet] = ..., shared_port: int = ..., player_name: str = ..., host_ip: str = ..., @@ -302,17 +302,17 @@ class RequestGameInfo(Message): class ResponseGameInfo(Message): map_name: str - mod_names: Iterable[str] + mod_names: Sequence[str] local_map_path: str - player_info: Iterable[PlayerInfo] + player_info: Sequence[PlayerInfo] start_raw: StartRaw options: InterfaceOptions def __init__( self, map_name: str = ..., - mod_names: Iterable[str] = ..., + mod_names: Sequence[str] = ..., local_map_path: str = ..., - player_info: Iterable[PlayerInfo] = ..., + player_info: Sequence[PlayerInfo] = ..., start_raw: StartRaw = ..., options: InterfaceOptions = ..., ) -> None: ... @@ -323,18 +323,18 @@ class RequestObservation(Message): def __init__(self, disable_fog: bool = ..., game_loop: int = ...) -> None: ... class ResponseObservation(Message): - actions: Iterable[Action] - action_errors: Iterable[ActionError] + actions: Sequence[Action] + action_errors: Sequence[ActionError] observation: Observation - player_result: Iterable[PlayerResult] - chat: Iterable[ChatReceived] + player_result: Sequence[PlayerResult] + chat: Sequence[ChatReceived] def __init__( self, - actions: Iterable[Action] = ..., - action_errors: Iterable[ActionError] = ..., + actions: Sequence[Action] = ..., + action_errors: Sequence[ActionError] = ..., observation: Observation = ..., - player_result: Iterable[PlayerResult] = ..., - chat: Iterable[ChatReceived] = ..., + player_result: Sequence[PlayerResult] = ..., + chat: Sequence[ChatReceived] = ..., ) -> None: ... class ChatReceived(Message): @@ -343,16 +343,16 @@ class ChatReceived(Message): def __init__(self, player_id: int = ..., message: str = ...) -> None: ... class RequestAction(Message): - actions: Iterable[Action] - def __init__(self, actions: Iterable[Action] = ...) -> None: ... + actions: Sequence[Action] + def __init__(self, actions: Sequence[Action] = ...) -> None: ... class ResponseAction(Message): - result: Iterable[int] - def __init__(self, result: Iterable[int] = ...) -> None: ... + result: Sequence[int] + def __init__(self, result: Sequence[int] = ...) -> None: ... class RequestObserverAction(Message): - actions: Iterable[ObserverAction] - def __init__(self, actions: Iterable[ObserverAction] = ...) -> None: ... + actions: Sequence[ObserverAction] + def __init__(self, actions: Sequence[ObserverAction] = ...) -> None: ... class ResponseObserverAction(Message): def __init__(self) -> None: ... @@ -381,18 +381,18 @@ class RequestData(Message): ) -> None: ... class ResponseData(Message): - abilities: Iterable[AbilityData] - units: Iterable[UnitTypeData] - upgrades: Iterable[UpgradeData] - buffs: Iterable[BuffData] - effects: Iterable[EffectData] + abilities: Sequence[AbilityData] + units: Sequence[UnitTypeData] + upgrades: Sequence[UpgradeData] + buffs: Sequence[BuffData] + effects: Sequence[EffectData] def __init__( self, - abilities: Iterable[AbilityData] = ..., - units: Iterable[UnitTypeData] = ..., - upgrades: Iterable[UpgradeData] = ..., - buffs: Iterable[BuffData] = ..., - effects: Iterable[EffectData] = ..., + abilities: Sequence[AbilityData] = ..., + units: Sequence[UnitTypeData] = ..., + upgrades: Sequence[UpgradeData] = ..., + buffs: Sequence[BuffData] = ..., + effects: Sequence[EffectData] = ..., ) -> None: ... class RequestSaveReplay(Message): @@ -436,7 +436,7 @@ class ResponseReplayInfo(Message): map_name: str local_map_path: str - player_info: Iterable[PlayerInfoExtra] + player_info: Sequence[PlayerInfoExtra] game_duration_loops: int game_duration_seconds: float game_version: str @@ -449,7 +449,7 @@ class ResponseReplayInfo(Message): self, map_name: str = ..., local_map_path: str = ..., - player_info: Iterable[PlayerInfoExtra] = ..., + player_info: Sequence[PlayerInfoExtra] = ..., game_duration_loops: int = ..., game_duration_seconds: float = ..., game_version: str = ..., @@ -464,9 +464,9 @@ class RequestAvailableMaps(Message): def __init__(self) -> None: ... class ResponseAvailableMaps(Message): - local_map_paths: Iterable[str] - battlenet_map_names: Iterable[str] - def __init__(self, local_map_paths: Iterable[str] = ..., battlenet_map_names: Iterable[str] = ...) -> None: ... + local_map_paths: Sequence[str] + battlenet_map_names: Sequence[str] + def __init__(self, local_map_paths: Sequence[str] = ..., battlenet_map_names: Sequence[str] = ...) -> None: ... class RequestSaveMap(Message): map_path: str @@ -497,8 +497,8 @@ class ResponsePing(Message): ) -> None: ... class RequestDebug(Message): - debug: Iterable[DebugCommand] - def __init__(self, debug: Iterable[DebugCommand] = ...) -> None: ... + debug: Sequence[DebugCommand] + def __init__(self, debug: Sequence[DebugCommand] = ...) -> None: ... class ResponseDebug(Message): def __init__(self) -> None: ... @@ -630,8 +630,8 @@ class PlayerCommon(Message): class Observation(Message): game_loop: int player_common: PlayerCommon - alerts: Iterable[int] - abilities: Iterable[AvailableAbility] + alerts: Sequence[int] + abilities: Sequence[AvailableAbility] score: Score raw_data: ObservationRaw feature_layer_data: ObservationFeatureLayer @@ -641,8 +641,8 @@ class Observation(Message): self, game_loop: int = ..., player_common: PlayerCommon = ..., - alerts: Iterable[int] = ..., - abilities: Iterable[AvailableAbility] = ..., + alerts: Sequence[int] = ..., + abilities: Sequence[AvailableAbility] = ..., score: Score = ..., raw_data: ObservationRaw = ..., feature_layer_data: ObservationFeatureLayer = ..., @@ -709,8 +709,8 @@ class ActionObserverCameraFollowPlayer(Message): def __init__(self, player_id: int = ...) -> None: ... class ActionObserverCameraFollowUnits(Message): - unit_tags: Iterable[int] - def __init__(self, unit_tags: Iterable[int] = ...) -> None: ... + unit_tags: Sequence[int] + def __init__(self, unit_tags: Sequence[int] = ...) -> None: ... class Alert(Enum): AlertError: int diff --git a/s2clientprotocol/spatial_pb2.pyi b/s2clientprotocol/spatial_pb2.pyi index a1b72a29..92f6afe9 100644 --- a/s2clientprotocol/spatial_pb2.pyi +++ b/s2clientprotocol/spatial_pb2.pyi @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message @@ -149,6 +149,6 @@ class ActionSpatialUnitSelectionPoint(Message): def __init__(self, selection_screen_coord: PointI = ..., type: int = ...) -> None: ... class ActionSpatialUnitSelectionRect(Message): - selection_screen_coord: Iterable[RectangleI] + selection_screen_coord: Sequence[RectangleI] selection_add: bool - def __init__(self, selection_screen_coord: Iterable[RectangleI] = ..., selection_add: bool = ...) -> None: ... + def __init__(self, selection_screen_coord: Sequence[RectangleI] = ..., selection_add: bool = ...) -> None: ... diff --git a/s2clientprotocol/ui_pb2.pyi b/s2clientprotocol/ui_pb2.pyi index dbf39f3b..82706576 100644 --- a/s2clientprotocol/ui_pb2.pyi +++ b/s2clientprotocol/ui_pb2.pyi @@ -1,19 +1,19 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Sequence from enum import Enum from google.protobuf.message import Message class ObservationUI(Message): - groups: Iterable[ControlGroup] + groups: Sequence[ControlGroup] single: SinglePanel multi: MultiPanel cargo: CargoPanel production: ProductionPanel def __init__( self, - groups: Iterable[ControlGroup] = ..., + groups: Sequence[ControlGroup] = ..., single: SinglePanel = ..., multi: MultiPanel = ..., cargo: CargoPanel = ..., @@ -63,28 +63,28 @@ class SinglePanel(Message): attack_upgrade_level: int armor_upgrade_level: int shield_upgrade_level: int - buffs: Iterable[int] + buffs: Sequence[int] def __init__( self, unit: UnitInfo = ..., attack_upgrade_level: int = ..., armor_upgrade_level: int = ..., shield_upgrade_level: int = ..., - buffs: Iterable[int] = ..., + buffs: Sequence[int] = ..., ) -> None: ... class MultiPanel(Message): - units: Iterable[UnitInfo] - def __init__(self, units: Iterable[UnitInfo] = ...) -> None: ... + units: Sequence[UnitInfo] + def __init__(self, units: Sequence[UnitInfo] = ...) -> None: ... class CargoPanel(Message): unit: UnitInfo - passengers: Iterable[UnitInfo] + passengers: Sequence[UnitInfo] slots_available: int def __init__( self, unit: UnitInfo = ..., - passengers: Iterable[UnitInfo] = ..., + passengers: Sequence[UnitInfo] = ..., slots_available: int = ..., ) -> None: ... @@ -95,13 +95,13 @@ class BuildItem(Message): class ProductionPanel(Message): unit: UnitInfo - build_queue: Iterable[UnitInfo] - production_queue: Iterable[BuildItem] + build_queue: Sequence[UnitInfo] + production_queue: Sequence[BuildItem] def __init__( self, unit: UnitInfo = ..., - build_queue: Iterable[UnitInfo] = ..., - production_queue: Iterable[BuildItem] = ..., + build_queue: Sequence[UnitInfo] = ..., + production_queue: Sequence[BuildItem] = ..., ) -> None: ... class ActionUI(Message): diff --git a/sc2/action.py b/sc2/action.py index b43124ae..019f3803 100644 --- a/sc2/action.py +++ b/sc2/action.py @@ -33,7 +33,10 @@ def combine_actions(action_iter: list[UnitCommand]): if combineable: # Combine actions with no target, e.g. lift, burrowup, burrowdown, siege, unsiege, uproot spines cmd = raw_pb.ActionRawUnitCommand( - ability_id=ability.value, unit_tags={u.unit.tag for u in items}, queue_command=queue + ability_id=ability.value, + # pyrefly: ignore + unit_tags={u.unit.tag for u in items}, + queue_command=queue, ) # Combine actions with target point, e.g. attack_move or move commands on a position if isinstance(target, Point2): @@ -58,13 +61,17 @@ def combine_actions(action_iter: list[UnitCommand]): if target is None: for u in items: cmd = raw_pb.ActionRawUnitCommand( - ability_id=ability.value, unit_tags={u.unit.tag}, queue_command=queue + ability_id=ability.value, + # pyrefly: ignore + unit_tags={u.unit.tag}, + queue_command=queue, ) yield raw_pb.ActionRaw(unit_command=cmd) elif isinstance(target, Point2): for u in items: cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, + # pyrefly: ignore unit_tags={u.unit.tag}, queue_command=queue, target_world_space_pos=target.as_Point2D, @@ -74,6 +81,7 @@ def combine_actions(action_iter: list[UnitCommand]): for u in items: cmd = raw_pb.ActionRawUnitCommand( ability_id=ability.value, + # pyrefly: ignore unit_tags={u.unit.tag}, queue_command=queue, target_unit_tag=target.tag, diff --git a/sc2/bot_ai.py b/sc2/bot_ai.py index d98e72a0..f16d8596 100644 --- a/sc2/bot_ai.py +++ b/sc2/bot_ai.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 16] from __future__ import annotations import math @@ -71,7 +70,6 @@ def step_time(self) -> tuple[float, float, float, float]: self._last_step_step_time * 1000, ) - # pyre-ignore[11] def alert(self, alert_code: Alert) -> bool: """ Check if alert is triggered in the current step. @@ -252,24 +250,24 @@ async def get_next_expansion(self) -> Point2 | None: """Find next expansion location.""" closest = None - distance = math.inf - for el in self.expansion_locations_list: + best_distance = math.inf + start_position = self.game_info.player_start_location + for position in self.expansion_locations_list: def is_near_to_expansion(t): - return t.distance_to(el) < self.EXPANSION_GAP_THRESHOLD + return t.distance_to(position) < self.EXPANSION_GAP_THRESHOLD if any(map(is_near_to_expansion, self.townhalls)): # already taken continue - startp = self.game_info.player_start_location - d = await self.client.query_pathing(startp, el) - if d is None: + distance = await self.client.query_pathing(start_position, position) + if distance is None: continue - if d < distance: - distance = d - closest = el + if distance < best_distance: + best_distance = distance + closest = position return closest @@ -494,7 +492,11 @@ def calculate_cost(self, item_id: UnitTypeId | UpgradeId | AbilityId) -> Cost: return self.calculate_unit_value(UnitTypeId.ARCHON) unit_data = self.game_data.units[item_id.value] # Cost of morphs is automatically correctly calculated by 'calculate_ability_cost' - return self.game_data.calculate_ability_cost(unit_data.creation_ability.exact_id) + creation_ability = unit_data.creation_ability + if creation_ability is None: + logger.error(f"Unknown creation_ability in calculate_cost for item_id: {item_id}") + return Cost(0, 0) + return self.game_data.calculate_ability_cost(creation_ability.exact_id) if isinstance(item_id, UpgradeId): cost = self.game_data.upgrades[item_id.value].cost @@ -568,11 +570,18 @@ async def can_cast( ability_target: int = self.game_data.abilities[ability_id.value]._proto.target # Check if target is in range (or is a self cast like stimpack) if ( + # Can't replace 1 with "Target.None.value" because ".None" doesn't seem to be a valid enum name ability_target == 1 or ability_target == Target.PointOrNone.value - and isinstance(target, Point2) - and unit.distance_to(target) <= unit.radius + target.radius + cast_range - ): # cant replace 1 with "Target.None.value" because ".None" doesnt seem to be a valid enum name + and ( + # Target is unit + isinstance(target, Unit) + and unit.distance_to(target) <= unit.radius + target.radius + cast_range + # Target is position + or isinstance(target, Point2) + and unit.distance_to(target) <= unit.radius + cast_range + ) + ): return True # Check if able to use ability on a unit if ( @@ -622,8 +631,12 @@ def select_build_worker(self, pos: Unit | Point2, force: bool = False) -> Unit | async def can_place_single(self, building: AbilityId | UnitTypeId, position: Point2) -> bool: """Checks the placement for only one position.""" if isinstance(building, UnitTypeId): - creation_ability = self.game_data.units[building.value].creation_ability.id - return (await self.client._query_building_placement_fast(creation_ability, [position]))[0] + creation_ability = self.game_data.units[building.value].creation_ability + if creation_ability is None: + logger.error(f"Unknown creation_ability in can_place_single for building: {building}") + return False + creation_ability_id = creation_ability.id + return (await self.client._query_building_placement_fast(creation_ability_id, [position]))[0] return (await self.client._query_building_placement_fast(building, [position]))[0] async def can_place(self, building: AbilityData | AbilityId | UnitTypeId, positions: list[Point2]) -> list[bool]: @@ -639,17 +652,18 @@ async def can_place(self, building: AbilityData | AbilityId | UnitTypeId, positi :param building: :param position:""" - building_type = type(building) - assert type(building) in {AbilityData, AbilityId, UnitTypeId}, f"{building}, {building_type}" - if building_type == UnitTypeId: - building = self.game_data.units[building.value].creation_ability.id - elif building_type == AbilityData: + if isinstance(building, UnitTypeId): + creation_ability = self.game_data.units[building.value].creation_ability + if creation_ability is None: + return [False for _ in positions] + building = creation_ability.id + elif isinstance(building, AbilityData): warnings.warn( "Using AbilityData is deprecated and may be removed soon. Please use AbilityId or UnitTypeId instead.", DeprecationWarning, stacklevel=2, ) - building = building_type.id + building = building.id if isinstance(positions, (Point2, tuple)): warnings.warn( @@ -657,6 +671,7 @@ async def can_place(self, building: AbilityData | AbilityId | UnitTypeId, positi DeprecationWarning, stacklevel=2, ) + # pyrefly: ignore return await self.can_place_single(building, positions) assert isinstance(positions, list), f"Expected an iterable (list, tuple), but was: {positions}" assert isinstance(positions[0], Point2), ( @@ -692,7 +707,10 @@ async def find_placement( assert isinstance(near, Point2), f"{near} is no Point2 object" if isinstance(building, UnitTypeId): - building = self.game_data.units[building.value].creation_ability.id + creation_ability = self.game_data.units[building.value].creation_ability + if creation_ability is None: + return None + building = creation_ability.id if await self.can_place_single(building, near) and ( not addon_place or await self.can_place_single(AbilityId.TERRANBUILD_SUPPLYDEPOT, near.offset((2.5, -0.5))) @@ -751,10 +769,13 @@ def already_pending_upgrade(self, upgrade_type: UpgradeId) -> float: assert isinstance(upgrade_type, UpgradeId), f"{upgrade_type} is no UpgradeId" if upgrade_type in self.state.upgrades: return 1 - creationAbilityID = self.game_data.upgrades[upgrade_type.value].research_ability.exact_id + research_ability = self.game_data.upgrades[upgrade_type.value].research_ability + if research_ability is None: + return 0 + creation_ability_id = research_ability.exact_id for structure in self.structures.filter(lambda unit: unit.is_ready): for order in structure.orders: - if order.ability.exact_id == creationAbilityID: + if order.ability.exact_id == creation_ability_id: return order.progress return 0 @@ -800,7 +821,7 @@ def structure_type_build_progress(self, structure_type: UnitTypeId | int) -> flo # SUPPLYDEPOTDROP is not in self.game_data.units, so bot_ai should not check the build progress via creation ability (worker abilities) if structure_type_value not in self.game_data.units: return max((s.build_progress for s in self.structures if s._proto.unit_type in equiv_values), default=0) - creation_ability_data: AbilityData = self.game_data.units[structure_type_value].creation_ability + creation_ability_data = self.game_data.units[structure_type_value].creation_ability if creation_ability_data is None: return 0 creation_ability: AbilityId = creation_ability_data.exact_id @@ -865,24 +886,30 @@ def already_pending(self, unit_type: UpgradeId | UnitTypeId) -> float: """ if isinstance(unit_type, UpgradeId): return self.already_pending_upgrade(unit_type) - try: - ability = self.game_data.units[unit_type.value].creation_ability.exact_id - except AttributeError: - if unit_type in CREATION_ABILITY_FIX: - # Hotfix for checking pending archons - if unit_type == UnitTypeId.ARCHON: - return self._abilities_count_and_build_progress[0][AbilityId.ARCHON_WARP_TARGET] / 2 - # Hotfix for rich geysirs - return self._abilities_count_and_build_progress[0][CREATION_ABILITY_FIX[unit_type]] - logger.error(f"Uncaught UnitTypeId: {unit_type}") + + if unit_type in CREATION_ABILITY_FIX: + # Hotfix for checking pending archons and other abilities + if unit_type == UnitTypeId.ARCHON: + return self._abilities_count_and_build_progress[0][AbilityId.ARCHON_WARP_TARGET] / 2 + # Hotfix for rich geysirs + return self._abilities_count_and_build_progress[0][CREATION_ABILITY_FIX[unit_type]] + + creation_ability = self.game_data.units[unit_type.value].creation_ability + if creation_ability is None: + logger.error(f"Unknown creation_ability in already_pending for unit_type: {unit_type}") return 0 - return self._abilities_count_and_build_progress[0][ability] + ability_id = creation_ability.exact_id + return self._abilities_count_and_build_progress[0][ability_id] def worker_en_route_to_build(self, unit_type: UnitTypeId) -> float: """This function counts how many workers are on the way to start the construction a building. :param unit_type:""" - ability = self.game_data.units[unit_type.value].creation_ability.exact_id + creation_ability = self.game_data.units[unit_type.value].creation_ability + if creation_ability is None: + logger.error(f"Unknown creation_ability in worker_en_route_to_build for unit_type: {unit_type}") + return 0 + ability = creation_ability.exact_id return self._worker_orders[ability] @property_cache_once_per_frame @@ -896,7 +923,7 @@ def structures_without_construction_SCVs(self) -> Units: continue for order in worker.orders: # When a construction is resumed, the worker.orders[0].target is the tag of the structure, else it is a Point2 - worker_targets.add(order.target) + worker_targets.add(order.target) # pyrefly: ignore return self.structures.filter( lambda structure: structure.build_progress < 1 # Redundant check? @@ -930,15 +957,15 @@ async def build( assert isinstance(near, (Unit, Point2)) if not self.can_afford(building): return False - p = None + position = None gas_buildings = {UnitTypeId.EXTRACTOR, UnitTypeId.ASSIMILATOR, UnitTypeId.REFINERY} if isinstance(near, Unit) and building not in gas_buildings: near = near.position if isinstance(near, Point2): near = near.to2 if isinstance(near, Point2): - p = await self.find_placement(building, near, max_distance, random_alternative, placement_step) - if p is None: + position = await self.find_placement(building, near, max_distance, random_alternative, placement_step) + if position is None: return False builder = build_worker or self.select_build_worker(near) if builder is None: @@ -947,7 +974,8 @@ async def build( assert isinstance(near, Unit) builder.build_gas(near) return True - self.do(builder.build(building, p), subtract_cost=True, ignore_warning=True) + # pyrefly: ignore + self.do(builder.build(building, position), subtract_cost=True, ignore_warning=True) return True def train( @@ -1056,6 +1084,7 @@ def train( else: # Normal train a unit from larva or inside a structure successfully_trained = self.do( + # pyrefly: ignore structure.train(unit_type), subtract_cost=True, subtract_supply=True, @@ -1079,6 +1108,7 @@ def train( trained_amount += 1 # With one command queue=False and one queue=True, you can queue 2 marines in a reactored barracks in one frame successfully_trained = self.do( + # pyrefly: ignore structure.train(unit_type, queue=True), subtract_cost=True, subtract_supply=True, @@ -1125,7 +1155,8 @@ def research(self, upgrade_type: UpgradeId) -> bool: return False research_structure_type: UnitTypeId = UPGRADE_RESEARCHED_FROM[upgrade_type] - # pyre-ignore[9] + + # pyrefly: ignore required_tech_building: UnitTypeId | None = RESEARCH_INFO[research_structure_type][upgrade_type].get( "required_building", None ) @@ -1168,6 +1199,7 @@ def research(self, upgrade_type: UpgradeId) -> bool: ): # Can_afford check was already done earlier in this function successful_action: bool = self.do( + # pyrefly: ignore structure.research(upgrade_type), subtract_cost=True, ignore_warning=True, @@ -1368,7 +1400,6 @@ async def on_step(self, iteration: int): """ raise NotImplementedError - # pyre-ignore[11] async def on_end(self, game_result: Result) -> None: """Override this in your bot class. This function is called at the end of a game. Unsure if this function will be called on the laddermanager client as the bot process may forcefully be terminated. diff --git a/sc2/bot_ai_internal.py b/sc2/bot_ai_internal.py index d2bde3f7..056fa2a6 100644 --- a/sc2/bot_ai_internal.py +++ b/sc2/bot_ai_internal.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 16, 29] from __future__ import annotations import itertools @@ -45,16 +44,17 @@ if TYPE_CHECKING: from sc2.client import Client from sc2.game_info import GameInfo + from sc2.bot_ai import BotAI class BotAIInternal(ABC): """Base class for bots.""" - def __init__(self) -> None: + def __init__(self: BotAI) -> None: self._initialize_variables() @final - def _initialize_variables(self) -> None: + def _initialize_variables(self: BotAI) -> None: """Called from main.py internally""" self.cache: dict[str, Any] = {} # Specific opponent bot ID used in sc2ai ladder games http://sc2ai.net/ and on ai arena https://aiarena.net @@ -103,8 +103,9 @@ def _initialize_variables(self) -> None: self.warp_gate_count: int = 0 self.actions: list[UnitCommand] = [] self.blips: set[Blip] = set() - # pyre-ignore[11] - self.race: Race | None = None + + # Will be set on AbstractPlayer init + self.race: Race = None # pyrefly: ignore self.enemy_race: Race | None = None self._generated_frame = -100 self._units_created: Counter = Counter() @@ -162,7 +163,7 @@ def _client(self) -> Client: @final @property_cache_once_per_frame - def expansion_locations(self) -> dict[Point2, Units]: + def expansion_locations(self: BotAI) -> dict[Point2, Units]: """Same as the function above.""" assert self._expansion_positions_list, ( "self._find_expansion_locations() has not been run yet, so accessing the list of expansion locations is pointless." @@ -191,7 +192,8 @@ def _cluster_center(self, group: list[Unit]) -> Point2: if not group: raise ValueError("Cannot calculate center of empty group") - total_x = total_y = 0 + total_x: float = 0 + total_y: float = 0 for unit in group: total_x += unit.position.x total_y += unit.position.y @@ -354,7 +356,7 @@ def _find_expansion_locations(self) -> None: # Distance offsets we apply to center of each resource group to find expansion position offset_range: int = 7 - offsets = [ + offsets: list[tuple[float, float]] = [ (x, y) for x, y in itertools.product(range(-offset_range, offset_range + 1), repeat=2) if 4 < math.hypot(x, y) <= 8 @@ -437,17 +439,20 @@ def _abilities_count_and_build_progress(self) -> tuple[Counter[AbilityId], dict[ if unit.type_id in CREATION_ABILITY_FIX: if unit.type_id == UnitTypeId.ARCHON: # Hotfix for archons in morph state - creation_ability = AbilityId.ARCHON_WARP_TARGET - abilities_amount[creation_ability] += 2 + creation_ability_id = AbilityId.ARCHON_WARP_TARGET + abilities_amount[creation_ability_id] += 2 else: # Hotfix for rich geysirs - creation_ability = CREATION_ABILITY_FIX[unit.type_id] - abilities_amount[creation_ability] += 1 + creation_ability_id = CREATION_ABILITY_FIX[unit.type_id] + abilities_amount[creation_ability_id] += 1 else: - creation_ability: AbilityId = self.game_data.units[unit.type_id.value].creation_ability.exact_id - abilities_amount[creation_ability] += 1 - max_build_progress[creation_ability] = max( - max_build_progress.get(creation_ability, 0), unit.build_progress + creation_ability = self.game_data.units[unit.type_id.value].creation_ability + if creation_ability is None: + continue + creation_ability_id = creation_ability.exact_id + abilities_amount[creation_ability_id] += 1 + max_build_progress[creation_ability_id] = max( + max_build_progress.get(creation_ability_id, 0), unit.build_progress ) return abilities_amount, max_build_progress @@ -473,7 +478,7 @@ def _worker_orders(self) -> Counter[AbilityId]: @final def do( - self, + self: BotAI, action: UnitCommand, subtract_cost: bool = False, subtract_supply: bool = False, @@ -542,7 +547,7 @@ def do( return True @final - async def synchronous_do(self, action: UnitCommand): + async def synchronous_do(self: BotAI, action: UnitCommand): """ Not recommended. Use self.do instead to reduce lag. This function is only useful for realtime=True in the first frame of the game to instantly produce a worker @@ -554,7 +559,7 @@ async def synchronous_do(self, action: UnitCommand): if not self.can_afford(action.ability): logger.warning(f"Cannot afford action {action}") return ActionResult.Error - r = await self.client.actions(action) + r: ActionResult = await self.client.actions(action) # pyrefly: ignore if not r: # success cost = self.game_data.calculate_ability_cost(action.ability) self.minerals -= cost.minerals @@ -594,11 +599,14 @@ def prevent_double_actions(action: UnitCommand) -> bool: # Different action, return True return True with suppress(AttributeError): - if current_action.target == action.target.tag: + if current_action.target == action.target.tag: # pyrefly: ignore # Same action, remove action if same target unit return False with suppress(AttributeError): - if action.target.x == current_action.target.x and action.target.y == current_action.target.y: + if ( + # pyrefly: ignore + action.target.x == current_action.target.x and action.target.y == current_action.target.y + ): # Same action, remove action if same target position return False return True @@ -649,7 +657,7 @@ def _prepare_first_step(self) -> None: self._time_before_step: float = time.perf_counter() @final - def _prepare_step(self, state: GameState, proto_game_info: sc_pb.Response) -> None: + def _prepare_step(self: BotAI, state: GameState, proto_game_info: sc_pb.Response) -> None: """ :param state: :param proto_game_info: @@ -690,7 +698,7 @@ def _prepare_step(self, state: GameState, proto_game_info: sc_pb.Response) -> No self.enemy_race = Race(self.all_enemy_units.first.race) @final - def _prepare_units(self) -> None: + def _prepare_units(self: BotAI) -> None: # Set of enemy units detected by own sensor tower, as blips have less unit information than normal visible units self.blips: set[Blip] = set() self.all_units: Units = Units([], self) @@ -816,7 +824,7 @@ async def _after_step(self) -> int: return self.state.game_loop @final - async def _advance_steps(self, steps: int) -> None: + async def _advance_steps(self: BotAI, steps: int) -> None: """Advances the game loop by amount of 'steps'. This function is meant to be used as a debugging and testing tool only. If you are using this, please be aware of the consequences, e.g. 'self.units' will be filled with completely new data.""" await self._after_step() @@ -829,7 +837,7 @@ async def _advance_steps(self, steps: int) -> None: await self.issue_events() @final - async def issue_events(self) -> None: + async def issue_events(self: BotAI) -> None: """This function will be automatically run from main.py and triggers the following functions: - on_unit_created - on_unit_destroyed @@ -844,7 +852,7 @@ async def issue_events(self) -> None: await self._issue_vision_events() @final - async def _issue_unit_added_events(self) -> None: + async def _issue_unit_added_events(self: BotAI) -> None: for unit in self.units: if unit.tag not in self._units_previous_map and unit.tag not in self._unit_tags_seen_this_game: self._unit_tags_seen_this_game.add(unit.tag) @@ -861,14 +869,14 @@ async def _issue_unit_added_events(self) -> None: await self.on_unit_type_changed(unit, previous_frame_unit.type_id) @final - async def _issue_upgrade_events(self) -> None: + async def _issue_upgrade_events(self: BotAI) -> None: difference = self.state.upgrades - self._previous_upgrades for upgrade_completed in difference: await self.on_upgrade_complete(upgrade_completed) self._previous_upgrades = self.state.upgrades @final - async def _issue_building_events(self) -> None: + async def _issue_building_events(self: BotAI) -> None: for structure in self.structures: if structure.tag not in self._structures_previous_map: if structure.build_progress < 1: @@ -900,7 +908,7 @@ async def _issue_building_events(self) -> None: await self.on_building_construction_complete(structure) @final - async def _issue_vision_events(self) -> None: + async def _issue_vision_events(self: BotAI) -> None: # Call events for enemy unit entered vision for enemy_unit in self.enemy_units: if enemy_unit.tag not in self._enemy_units_previous_map: @@ -918,7 +926,7 @@ async def _issue_vision_events(self) -> None: await self.on_enemy_unit_left_vision(enemy_structure_tag) @final - async def _issue_unit_dead_events(self) -> None: + async def _issue_unit_dead_events(self: BotAI) -> None: for unit_tag in self.state.dead_units & set(self._all_units_previous_map): await self.on_unit_destroyed(unit_tag) diff --git a/sc2/cache.py b/sc2/cache.py index b1682fc5..ad06ce99 100644 --- a/sc2/cache.py +++ b/sc2/cache.py @@ -32,17 +32,18 @@ class property_cache_once_per_frame(property): # noqa: N801 def __init__(self, func: Callable[[BotAI], T], name: str | None = None) -> None: self.__name__ = name or func.__name__ self.__frame__ = f"__frame__{self.__name__}" + # pyrefly: ignore self.func = func def __set__(self, obj: BotAI, value: T) -> None: obj.cache[self.__name__] = value - # pyre-ignore[16] + obj.cache[self.__frame__] = obj.state.game_loop # pyre-fixme[34] def __get__(self, obj: BotAI, _type=None) -> T: value = obj.cache.get(self.__name__, None) - # pyre-ignore[16] + bot_frame = obj.state.game_loop if value is None or obj.cache[self.__frame__] < bot_frame: value = self.func(obj) diff --git a/sc2/client.py b/sc2/client.py index 8971b86f..c1dc6394 100644 --- a/sc2/client.py +++ b/sc2/client.py @@ -1,9 +1,8 @@ -# pyre-ignore-all-errors[6, 9, 16, 29, 58] from __future__ import annotations from collections.abc import Iterable from pathlib import Path -from typing import Any +from typing import Any, Literal from aiohttp import ClientWebSocketResponse from loguru import logger @@ -37,8 +36,10 @@ def __init__(self, ws: ClientWebSocketResponse, save_replay_path: str | None = N # How many frames will be waited between iterations before the next one is called self.game_step: int = 4 self.save_replay_path: str | None = save_replay_path - self._player_id = None - self._game_result = None + # The following will be set on join_game() + self._player_id: int = None # pyrefly: ignore + # The following will be set on leave() + self._game_result: dict[int, Result] = None # pyrefly: ignore # Store a hash value of all the debug requests to prevent sending the same ones again if they haven't changed last frame self._debug_hash_tuple_last_iteration: tuple[int, int, int, int] = (0, 0, 0, 0) self._debug_draw_last_frame = False @@ -89,26 +90,26 @@ async def join_game( if race is None: assert isinstance(observed_player_id, int), f"observed_player_id is of type {type(observed_player_id)}" # join as observer - req = sc_pb.RequestJoinGame(observed_player_id=observed_player_id, options=ifopts) + request = sc_pb.RequestJoinGame(observed_player_id=observed_player_id, options=ifopts) else: assert isinstance(race, Race) - req = sc_pb.RequestJoinGame(race=race.value, options=ifopts) + request = sc_pb.RequestJoinGame(race=race.value, options=ifopts) if portconfig: - req.server_ports.game_port = portconfig.server[0] - req.server_ports.base_port = portconfig.server[1] + request.server_ports.game_port = portconfig.server[0] + request.server_ports.base_port = portconfig.server[1] for ppc in portconfig.players: - p = req.client_ports.add() + p = request.client_ports.add() # pyrefly: ignore p.game_port = ppc[0] p.base_port = ppc[1] if name is not None: assert isinstance(name, str), f"name is of type {type(name)}" - req.player_name = name + request.player_name = name - result = await self._execute(join_game=req) - self._game_result = None + result = await self._execute(join_game=request) + self._game_result = None # pyrefly: ignore self._player_id = result.join_game.player_id return result.join_game.player_id @@ -150,7 +151,7 @@ async def observation(self, game_loop: int | None = None): result = await self._execute(observation=sc_pb.RequestObservation()) assert result.observation.player_result - player_id_to_result = {} + player_id_to_result = dict[int, Result]() for pr in result.observation.player_result: player_id_to_result[pr.player_id] = Result(pr.result) self._game_result = player_id_to_result @@ -211,16 +212,21 @@ async def actions(self, actions: list[UnitCommand], return_successes: bool = Fal # On realtime=True, might get an error here: sc2.protocol.ProtocolError: ['Not in a game'] try: - res = await self._execute( - action=sc_pb.RequestAction(actions=(sc_pb.Action(action_raw=a) for a in combine_actions(actions))) + response = await self._execute( + action=sc_pb.RequestAction( + # pyrefly: ignore + actions=(sc_pb.Action(action_raw=action) for action in combine_actions(actions)) + ) ) except ProtocolError: return [] if return_successes: - return [ActionResult(r) for r in res.action.result] - return [ActionResult(r) for r in res.action.result if ActionResult(r) != ActionResult.Success] + return [ActionResult(result) for result in response.action.result] + return [ + ActionResult(result) for result in response.action.result if ActionResult(result) != ActionResult.Success + ] - async def query_pathing(self, start: Unit | Point2 | Point3, end: Point2 | Point3) -> int | float | None: + async def query_pathing(self, start: Unit | Point2 | Point3, end: Point2 | Point3) -> float | None: """Caution: returns "None" when path not found Try to combine queries with the function below because the pathing query is generally slow. @@ -238,25 +244,25 @@ async def query_pathing(self, start: Unit | Point2 | Point3, end: Point2 | Point return None return distance - async def query_pathings(self, zipped_list: list[list[Unit | Point2 | Point3]]) -> list[float]: + async def query_pathings(self, zipped_list: list[tuple[Unit | Point2 | Point3, Point2 | Point3]]) -> list[float]: """Usage: await self.query_pathings([[unit1, target2], [unit2, target2]]) -> returns [distance1, distance2] Caution: returns 0 when path not found :param zipped_list: """ - assert zipped_list, "No zipped_list" - assert isinstance(zipped_list, list), f"{type(zipped_list)}" - assert isinstance(zipped_list[0], list), f"{type(zipped_list[0])}" - assert len(zipped_list[0]) == 2, f"{len(zipped_list[0])}" - assert isinstance(zipped_list[0][0], (Point2, Unit)), f"{type(zipped_list[0][0])}" - assert isinstance(zipped_list[0][1], Point2), f"{type(zipped_list[0][1])}" - if isinstance(zipped_list[0][0], Point2): - path = ( - query_pb.RequestQueryPathing(start_pos=p1.as_Point2D, end_pos=p2.as_Point2D) for p1, p2 in zipped_list + assert zipped_list, "No entry in zipped_list" + path = ( + query_pb.RequestQueryPathing( + # pyrefly: ignore + unit_tag=p1.tag if isinstance(p1, Unit) else None, + # pyrefly: ignore + start_pos=None if isinstance(p1, Unit) else p1.as_Point2D, + end_pos=p2.as_Point2D, ) - else: - path = (query_pb.RequestQueryPathing(unit_tag=p1.tag, end_pos=p2.as_Point2D) for p1, p2 in zipped_list) + for p1, p2 in zipped_list + ) + # pyrefly: ignore results = await self._execute(query=query_pb.RequestQuery(pathing=path)) return [float(d.distance) for d in results.query.pathing] @@ -272,6 +278,7 @@ async def _query_building_placement_fast( """ result = await self._execute( query=query_pb.RequestQuery( + # pyrefly: ignore placements=( query_pb.RequestQueryBuildingPlacement(ability_id=ability.value, target_pos=position.as_Point2D) for position in positions @@ -297,6 +304,7 @@ async def query_building_placement( assert isinstance(ability, AbilityData) result = await self._execute( query=query_pb.RequestQuery( + # pyrefly: ignore placements=( query_pb.RequestQueryBuildingPlacement(ability_id=ability.id.value, target_pos=position.as_Point2D) for position in positions @@ -320,13 +328,14 @@ async def query_available_abilities( assert units result = await self._execute( query=query_pb.RequestQuery( + # pyrefly: ignore abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units), ignore_resource_requirements=ignore_resource_requirements, ) ) """ Fix for bots that only query a single unit, may be removed soon """ if not input_was_a_list: - # pyre-fixme[7] + # pyrefly: ignore return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities][0] return [[AbilityId(a.ability_id) for a in b.abilities] for b in result.query.abilities] @@ -334,9 +343,9 @@ async def query_available_abilities_with_tag( self, units: list[Unit] | Units, ignore_resource_requirements: bool = False ) -> dict[int, set[AbilityId]]: """Query abilities of multiple units""" - result = await self._execute( query=query_pb.RequestQuery( + # pyrefly: ignore abilities=(query_pb.RequestQueryAvailableAbilities(unit_tag=unit.tag) for unit in units), ignore_resource_requirements=ignore_resource_requirements, ) @@ -368,7 +377,9 @@ async def toggle_autocast(self, units: list[Unit] | Units, ability: AbilityId) - sc_pb.Action( action_raw=raw_pb.ActionRaw( toggle_autocast=raw_pb.ActionRawToggleAutocast( - ability_id=ability.value, unit_tags=(u.tag for u in units) + ability_id=ability.value, + # pyrefly: ignore + unit_tags=(u.tag for u in units), ) ) ) @@ -376,22 +387,18 @@ async def toggle_autocast(self, units: list[Unit] | Units, ability: AbilityId) - ) ) - async def debug_create_unit(self, unit_spawn_commands: list[list[UnitTypeId | int | Point2 | Point3]]) -> None: + async def debug_create_unit( + self, unit_spawn_commands: list[tuple[UnitTypeId, int, Point2 | Point3, Literal[1, 2]]] + ) -> None: """Usage example (will spawn 5 marines in the center of the map for player ID 1): await self._client.debug_create_unit([[UnitTypeId.MARINE, 5, self._game_info.map_center, 1]]) :param unit_spawn_commands:""" - assert isinstance(unit_spawn_commands, list) - assert unit_spawn_commands - assert isinstance(unit_spawn_commands[0], list) - assert len(unit_spawn_commands[0]) == 4 - assert isinstance(unit_spawn_commands[0][0], UnitTypeId) - assert unit_spawn_commands[0][1] > 0 # careful, in realtime=True this function may create more units - assert isinstance(unit_spawn_commands[0][2], (Point2, Point3)) - assert 1 <= unit_spawn_commands[0][3] <= 2 + assert unit_spawn_commands, "List is empty" await self._execute( debug=sc_pb.RequestDebug( + # pyrefly: ignore debug=( debug_pb.DebugCommand( create_unit=debug_pb.DebugCreateUnit( @@ -417,6 +424,7 @@ async def debug_kill_unit(self, unit_tags: Unit | Units | list[int] | set[int]) assert unit_tags await self._execute( + # pyrefly: ignore debug=sc_pb.RequestDebug(debug=[debug_pb.DebugCommand(kill_unit=debug_pb.DebugKillUnit(tag=unit_tags))]) ) @@ -636,15 +644,19 @@ async def _send_debug(self) -> None: debug=[ debug_pb.DebugCommand( draw=debug_pb.DebugDraw( + # pyrefly: ignore text=[text.to_proto() for text in self._debug_texts] if self._debug_texts else None, + # pyrefly: ignore lines=[line.to_proto() for line in self._debug_lines] if self._debug_lines else None, + # pyrefly: ignore boxes=[box.to_proto() for box in self._debug_boxes] if self._debug_boxes else None, + # pyrefly: ignore spheres=[sphere.to_proto() for sphere in self._debug_spheres] if self._debug_spheres else None, @@ -666,6 +678,7 @@ async def _send_debug(self) -> None: await self._execute( debug=sc_pb.RequestDebug( debug=[ + # pyrefly: ignore debug_pb.DebugCommand(draw=debug_pb.DebugDraw(text=None, lines=None, boxes=None, spheres=None)) ] ) @@ -697,6 +710,7 @@ async def debug_set_unit_value( assert value >= 0, "Value can't be negative" await self._execute( debug=sc_pb.RequestDebug( + # pyrefly: ignore debug=( debug_pb.DebugCommand( unit_value=debug_pb.DebugSetUnitValue( @@ -787,12 +801,12 @@ def to_debug_color(color: tuple[float, float, float] | list[float] | Point3 | No return debug_pb.Color(r=255, g=255, b=255) # Need to check if not of type Point3 because Point3 inherits from tuple if isinstance(color, (tuple, list)) or isinstance(color, Point3) and len(color) == 3: - return debug_pb.Color(r=color[0], g=color[1], b=color[2]) + return debug_pb.Color(r=int(color[0]), g=int(color[1]), b=int(color[2])) # In case color is of type Point3 r = getattr(color, "r", getattr(color, "x", 255)) g = getattr(color, "g", getattr(color, "y", 255)) b = getattr(color, "b", getattr(color, "z", 255)) - # pyre-ignore[20] + if max(r, g, b) <= 1: r *= 255 g *= 255 @@ -819,6 +833,7 @@ def to_proto(self): color=self.to_debug_color(self._color), text=self._text, virtual_pos=self._start_point.to3.as_Point, + # pyrefly: ignore world_pos=None, size=self._font_size, ) @@ -830,20 +845,21 @@ def __hash__(self) -> int: class DrawItemWorldText(DrawItem): def __init__( self, - start_point: Point3 = None, - color: tuple[float, float, float] | list[float] | Point3 | None = None, + start_point: Point3, + color: tuple[float, float, float] | list[float] | Point3 | None, text: str = "", font_size: int = 8, ) -> None: - self._start_point: Point3 = start_point - self._color: Point3 = color - self._text: str = text - self._font_size: int = font_size + self._start_point = start_point + self._color = color + self._text = text + self._font_size = font_size def to_proto(self): return debug_pb.DebugText( color=self.to_debug_color(self._color), text=self._text, + # pyrefly: ignore virtual_pos=None, world_pos=self._start_point.as_Point, size=self._font_size, @@ -856,13 +872,13 @@ def __hash__(self) -> int: class DrawItemLine(DrawItem): def __init__( self, - start_point: Point3 = None, - end_point: Point3 = None, + start_point: Point3, + end_point: Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: - self._start_point: Point3 = start_point - self._end_point: Point3 = end_point - self._color: Point3 = color + self._start_point = start_point + self._end_point = end_point + self._color = color def to_proto(self): return debug_pb.DebugLine( @@ -877,13 +893,13 @@ def __hash__(self) -> int: class DrawItemBox(DrawItem): def __init__( self, - start_point: Point3 = None, - end_point: Point3 = None, + start_point: Point3, + end_point: Point3, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: - self._start_point: Point3 = start_point - self._end_point: Point3 = end_point - self._color: Point3 = color + self._start_point = start_point + self._end_point = end_point + self._color = color def to_proto(self): return debug_pb.DebugBox( @@ -899,13 +915,13 @@ def __hash__(self) -> int: class DrawItemSphere(DrawItem): def __init__( self, - start_point: Point3 = None, - radius: float = None, + start_point: Point3, + radius: float, color: tuple[float, float, float] | list[float] | Point3 | None = None, ) -> None: - self._start_point: Point3 = start_point - self._radius: float = radius - self._color: Point3 = color + self._start_point = start_point + self._radius = radius + self._color = color def to_proto(self): return debug_pb.DebugSphere( diff --git a/sc2/constants.py b/sc2/constants.py index 6478ff01..a3be87c3 100644 --- a/sc2/constants.py +++ b/sc2/constants.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] from __future__ import annotations from collections import defaultdict @@ -494,7 +493,7 @@ def return_NOTAUNIT() -> UnitTypeId: UnitTypeId.EXTRACTOR, UnitTypeId.EXTRACTORRICH, } -# pyre-ignore[11] + DAMAGE_BONUS_PER_UPGRADE: dict[UnitTypeId, dict[int, Any]] = { # # Protoss diff --git a/sc2/controller.py b/sc2/controller.py index e068aa3f..45b7dec5 100644 --- a/sc2/controller.py +++ b/sc2/controller.py @@ -26,18 +26,23 @@ def running(self) -> bool: async def create_game(self, game_map, players, realtime: bool, random_seed=None, disable_fog=None): req = sc_pb.RequestCreateGame( - local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)), realtime=realtime, disable_fog=disable_fog + local_map=sc_pb.LocalMap(map_path=str(game_map.relative_path)), + realtime=realtime, + # pyrefly: ignore + disable_fog=disable_fog, ) if random_seed is not None: req.random_seed = random_seed for player in players: + # pyrefly: ignore p = req.player_setup.add() p.type = player.type.value if isinstance(player, Computer): p.race = player.race.value p.difficulty = player.difficulty.value - p.ai_build = player.ai_build.value + if player.ai_build is not None: + p.ai_build = player.ai_build.value logger.info("Creating new game") logger.info(f"Map: {game_map.name}") diff --git a/sc2/data.py b/sc2/data.py index d376138b..e36bddd0 100644 --- a/sc2/data.py +++ b/sc2/data.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16, 19] """For the list of enums, see here https://github.com/Blizzard/s2client-proto/tree/bff45dae1fc685e6acbaae084670afb7d1c0832c/s2clientprotocol @@ -38,7 +37,7 @@ ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items()) -# pyre-ignore[11] + race_worker: dict[Race, UnitTypeId] = { Race.Protoss: UnitTypeId.PROBE, Race.Terran: UnitTypeId.SCV, diff --git a/sc2/expiring_dict.py b/sc2/expiring_dict.py index 7c6cc2c0..d80ffa8a 100644 --- a/sc2/expiring_dict.py +++ b/sc2/expiring_dict.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14, 15, 58] from __future__ import annotations from collections import OrderedDict @@ -42,7 +41,6 @@ def __init__(self, bot: BotAI, max_age_frames: int = 1) -> None: @property def frame(self) -> int: - # pyre-ignore[16] return self.bot.state.game_loop def __contains__(self, key: Hashable) -> bool: @@ -75,7 +73,7 @@ def __setitem__(self, key: Hashable, value: Any) -> None: def __repr__(self) -> str: """Printable version of the dict instead of getting memory adress""" - print_list = [] + print_list: list[str] = [] with self.lock: for key, value in OrderedDict.items(self): if self.frame - value[1] < self.max_age: diff --git a/sc2/game_data.py b/sc2/game_data.py index 4be84ee2..4283892c 100644 --- a/sc2/game_data.py +++ b/sc2/game_data.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[29] from __future__ import annotations from bisect import bisect_left @@ -49,7 +48,6 @@ def calculate_ability_cost(self, ability: AbilityData | AbilityId | UnitCommand) if not AbilityData.id_exists(unit.creation_ability.id.value): continue - # pyre-ignore[16] if unit.creation_ability.is_free_morph: continue @@ -265,9 +263,7 @@ def morph_cost(self) -> Cost | None: self._game_data.units[tech_alias.value].cost.minerals for tech_alias in self.tech_alias ) tech_alias_cost_vespene = max( - self._game_data.units[tech_alias.value].cost.vespene - # pyre-ignore[16] - for tech_alias in self.tech_alias + self._game_data.units[tech_alias.value].cost.vespene for tech_alias in self.tech_alias ) return Cost( self._proto.mineral_cost - tech_alias_cost_minerals, diff --git a/sc2/game_info.py b/sc2/game_info.py index aab025d5..f68daf95 100644 --- a/sc2/game_info.py +++ b/sc2/game_info.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 11, 16, 58] from __future__ import annotations import heapq @@ -31,7 +30,7 @@ def y_offset(self) -> float: return 0.5 @cached_property - def _height_map(self): + def _height_map(self) -> PixelMap: return self.game_info.terrain_height @cached_property @@ -146,6 +145,7 @@ def barracks_can_fit_addon(self) -> bool: """Test if a barracks can fit an addon at natural ramp""" # https://i.imgur.com/4b2cXHZ.png if len(self.upper2_for_ramp_wall) == 2: + # pyrefly: ignore return self.barracks_in_middle.x + 1 > max(self.corner_depots, key=lambda depot: depot.x).x raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") @@ -173,8 +173,9 @@ def protoss_wall_pylon(self) -> Point2 | None: raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") middle = self.depot_in_middle # direction up the ramp + # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) - # pyre-ignore[7] + return middle + 6 * direction @cached_property @@ -188,12 +189,14 @@ def protoss_wall_buildings(self) -> frozenset[Point2]: if len(self.upper2_for_ramp_wall) == 2: middle = self.depot_in_middle # direction up the ramp + # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) # sort depots based on distance to start to get wallin orientation sorted_depots = sorted( self.corner_depots, key=lambda depot: depot.distance_to(self.game_info.player_start_location) ) wall1: Point2 = sorted_depots[1].offset(direction) + # pyrefly: ignore wall2 = middle + direction + (middle - wall1) / 1.5 return frozenset([wall1, wall2]) @@ -211,6 +214,7 @@ def protoss_wall_warpin(self) -> Point2 | None: raise Exception("Not implemented. Trying to access a ramp that has a wrong amount of upper points.") middle = self.depot_in_middle # direction up the ramp + # pyrefly: ignore direction = self.barracks_in_middle.negative_offset(middle) # sort depots based on distance to start to get wallin orientation sorted_depots = sorted(self.corner_depots, key=lambda x: x.distance_to(self.game_info.player_start_location)) @@ -223,7 +227,7 @@ def __init__(self, proto: sc2api_pb2.ResponseGameInfo) -> None: self.players: list[Player] = [Player.from_proto(p) for p in self._proto.player_info] self.map_name: str = self._proto.map_name self.local_map_path: str = self._proto.local_map_path - # pyre-ignore[8] + self.map_size: Size = Size.from_proto(self._proto.start_raw.map_size) # self.pathing_grid[point]: if 0, point is not pathable, if 1, point is pathable @@ -234,9 +238,9 @@ def __init__(self, proto: sc2api_pb2.ResponseGameInfo) -> None: self.placement_grid: PixelMap = PixelMap(self._proto.start_raw.placement_grid, in_bits=True) self.playable_area = Rect.from_proto(self._proto.start_raw.playable_area) self.map_center = self.playable_area.center - # pyre-ignore[8] + # pyrefly: ignore self.map_ramps: list[Ramp] = None # Filled later by BotAI._prepare_first_step - # pyre-ignore[8] + # pyrefly: ignore self.vision_blockers: frozenset[Point2] = None # Filled later by BotAI._prepare_first_step self.player_races: dict[int, int] = { p.player_id: p.race_actual or p.race_requested for p in self._proto.player_info @@ -244,7 +248,7 @@ def __init__(self, proto: sc2api_pb2.ResponseGameInfo) -> None: self.start_locations: list[Point2] = [ Point2.from_proto(sl).round(decimals=1) for sl in self._proto.start_raw.start_locations ] - # pyre-ignore[8] + # pyrefly: ignore self.player_start_location: Point2 = None # Filled later by BotAI._prepare_first_step def _find_ramps_and_vision_blockers(self) -> tuple[list[Ramp], frozenset[Point2]]: @@ -270,10 +274,10 @@ def equal_height_around(tile): # divide points into ramp points and vision blockers ramp_points = [point for point in points if not equal_height_around(point)] vision_blockers = frozenset(point for point in points if equal_height_around(point)) - ramps = [Ramp(group, self) for group in self._find_groups(ramp_points)] + ramps = [Ramp(frozenset(group), self) for group in self._find_groups(ramp_points)] return ramps, vision_blockers - def _find_groups(self, points: frozenset[Point2], minimum_points_per_group: int = 8) -> Iterable[frozenset[Point2]]: + def _find_groups(self, points: Iterable[Point2], minimum_points_per_group: int = 8) -> Iterable[frozenset[Point2]]: """ From a set of points, this function will try to group points together by painting clusters of points in a rectangular map using flood fill algorithm. @@ -287,6 +291,7 @@ def _find_groups(self, points: frozenset[Point2], minimum_points_per_group: int picture: list[list[int]] = [[-2 for _ in range(map_width)] for _ in range(map_height)] def paint(pt: Point2) -> None: + # pyrefly: ignore picture[pt.y][pt.x] = current_color nearby: list[tuple[int, int]] = [(a, b) for a in [-1, 0, 1] for b in [-1, 0, 1] if a != 0 or b != 0] @@ -310,6 +315,7 @@ def paint(pt: Point2) -> None: # Do we ever reach out of map bounds? if not (0 <= px < map_width and 0 <= py < map_height): continue + # pyrefly: ignore if picture[py][px] != NOT_COLORED_YET: continue point: Point2 = Point2((px, py)) diff --git a/sc2/game_state.py b/sc2/game_state.py index 7ff4bb89..fd7af3db 100644 --- a/sc2/game_state.py +++ b/sc2/game_state.py @@ -1,9 +1,9 @@ -# pyre-ignore-all-errors[11, 16] from __future__ import annotations from dataclasses import dataclass from functools import cached_property from itertools import chain +from s2clientprotocol.raw_pb2 import Effect, Unit from loguru import logger @@ -46,7 +46,7 @@ def is_visible(self) -> bool: return self._proto.display_type == DisplayType.Visible.value @property - def alliance(self) -> Alliance: + def alliance(self) -> int: return self._proto.alliance @property @@ -97,7 +97,7 @@ def __init__(self, proto: raw_pb2.Effect | raw_pb2.Unit, fake: bool = False) -> :param proto: :param fake: """ - self._proto = proto + self._proto: Effect | Unit = proto self.fake = fake @property @@ -133,7 +133,7 @@ def owner(self) -> int: @property def radius(self) -> float: - if self.fake: + if isinstance(self._proto, Unit): return FakeEffectRadii[self._proto.unit_type] return self._proto.radius @@ -149,6 +149,8 @@ class ChatMessage: @dataclass class AbilityLookupTemplateClass: + ability_id: int + @property def exact_id(self) -> AbilityId: return AbilityId(self.ability_id) @@ -164,7 +166,6 @@ def generic_id(self) -> AbilityId: @dataclass class ActionRawUnitCommand(AbilityLookupTemplateClass): game_loop: int - ability_id: int unit_tags: list[int] queue_command: bool target_world_space_pos: Point2 | None @@ -174,7 +175,6 @@ class ActionRawUnitCommand(AbilityLookupTemplateClass): @dataclass class ActionRawToggleAutocast(AbilityLookupTemplateClass): game_loop: int - ability_id: int unit_tags: list[int] @@ -185,7 +185,6 @@ class ActionRawCameraMove: @dataclass class ActionError(AbilityLookupTemplateClass): - ability_id: int unit_tag: int # See here for the codes of 'result': https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/error.proto#L6 result: int @@ -212,7 +211,7 @@ def __init__( self.common: Common = Common(self.observation.player_common) # Area covered by Pylons and Warpprisms - self.psionic_matrix: PsionicMatrix = PsionicMatrix.from_proto(self.observation_raw.player.power_sources) + self.psionic_matrix: PsionicMatrix = PsionicMatrix.from_proto(list(self.observation_raw.player.power_sources)) # 22.4 per second on faster game speed self.game_loop: int = self.observation.game_loop @@ -259,7 +258,7 @@ def alerts(self) -> list[int]: """ if self.previous_observation is not None: return list(chain(self.previous_observation.observation.alerts, self.observation.alerts)) - return self.observation.alerts + return list(self.observation.alerts) @cached_property def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | ActionRawCameraMove]: @@ -283,7 +282,7 @@ def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | Actio ActionRawUnitCommand( game_loop, raw_unit_command.ability_id, - raw_unit_command.unit_tags, + list(raw_unit_command.unit_tags), raw_unit_command.queue_command, Point2.from_proto(raw_unit_command.target_world_space_pos), ) @@ -294,7 +293,7 @@ def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | Actio ActionRawUnitCommand( game_loop, raw_unit_command.ability_id, - raw_unit_command.unit_tags, + list(raw_unit_command.unit_tags), raw_unit_command.queue_command, None, raw_unit_command.target_unit_tag, @@ -307,7 +306,7 @@ def actions(self) -> list[ActionRawUnitCommand | ActionRawToggleAutocast | Actio ActionRawToggleAutocast( game_loop, raw_toggle_autocast_action.ability_id, - raw_toggle_autocast_action.unit_tags, + list(raw_toggle_autocast_action.unit_tags), ) ) else: @@ -321,7 +320,7 @@ def actions_unit_commands(self) -> list[ActionRawUnitCommand]: List of successful unit actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L185-L193 """ - # pyre-ignore[7] + return list(filter(lambda action: isinstance(action, ActionRawUnitCommand), self.actions)) @cached_property @@ -330,7 +329,7 @@ def actions_toggle_autocast(self) -> list[ActionRawToggleAutocast]: List of successful autocast toggle actions since last frame. See https://github.com/Blizzard/s2client-proto/blob/01ab351e21c786648e4c6693d4aad023a176d45c/s2clientprotocol/raw.proto#L199-L202 """ - # pyre-ignore[7] + return list(filter(lambda action: isinstance(action, ActionRawToggleAutocast), self.actions)) @cached_property diff --git a/sc2/generate_ids.py b/sc2/generate_ids.py index 1fd4e7d6..af0b5fe1 100644 --- a/sc2/generate_ids.py +++ b/sc2/generate_ids.py @@ -25,7 +25,7 @@ def __init__( self.game_version = game_version self.verbose = verbose - self.HEADER = f"""# pyre-ignore-all-errors[14] + self.HEADER = f""" from __future__ import annotations # DO NOT EDIT! # This file was automatically generated by "{Path(__file__).name}" @@ -194,8 +194,8 @@ def _missing_(cls, value: int) -> {class_name}: f.write(f'ID_VERSION_STRING = "{self.game_version}"\n') def update_ids_from_stableid_json(self) -> None: - if self.game_version is None or ID_VERSION_STRING is None or self.game_version != ID_VERSION_STRING: - if self.verbose and self.game_version is not None and ID_VERSION_STRING is not None: + if self.game_version is None or self.game_version != ID_VERSION_STRING: + if self.verbose and self.game_version is not None: logger.info( f"Game version is different (Old: {self.game_version}, new: {ID_VERSION_STRING}. Updating ids to match game version" ) diff --git a/sc2/ids/__init__.py b/sc2/ids/__init__.py index 24b6bc2a..a69ff863 100644 --- a/sc2/ids/__init__.py +++ b/sc2/ids/__init__.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/ids/ability_id.py b/sc2/ids/ability_id.py index 955be493..d895fa90 100644 --- a/sc2/ids/ability_id.py +++ b/sc2/ids/ability_id.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/ids/buff_id.py b/sc2/ids/buff_id.py index 5a7345a8..632965b1 100644 --- a/sc2/ids/buff_id.py +++ b/sc2/ids/buff_id.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/ids/effect_id.py b/sc2/ids/effect_id.py index 77aea24b..7bef1546 100644 --- a/sc2/ids/effect_id.py +++ b/sc2/ids/effect_id.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/ids/unit_typeid.py b/sc2/ids/unit_typeid.py index ec74ebe8..ab0facdf 100644 --- a/sc2/ids/unit_typeid.py +++ b/sc2/ids/unit_typeid.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/ids/upgrade_id.py b/sc2/ids/upgrade_id.py index 4be6cbfe..0f411d5e 100644 --- a/sc2/ids/upgrade_id.py +++ b/sc2/ids/upgrade_id.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14] from __future__ import annotations # DO NOT EDIT! diff --git a/sc2/main.py b/sc2/main.py index 8d07314e..cb18c0d3 100644 --- a/sc2/main.py +++ b/sc2/main.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 11, 16, 21, 29] from __future__ import annotations import asyncio @@ -25,7 +24,7 @@ from sc2.game_state import GameState from sc2.maps import Map from sc2.observer_ai import ObserverAI -from sc2.player import AbstractPlayer, Bot, BotProcess, Human +from sc2.player import AbstractPlayer, Bot, BotProcess, Computer, Human from sc2.portconfig import Portconfig from sc2.protocol import ConnectionAlreadyClosedError, ProtocolError from sc2.proxy import Proxy @@ -56,7 +55,12 @@ class GameMatch: def __post_init__(self) -> None: # avoid players sharing names - if len(self.players) > 1 and self.players[0].name is not None and self.players[0].name == self.players[1].name: + if ( + len(self.players) > 1 + and self.players[0].name is not None + and self.players[1].name is not None + and self.players[0].name == self.players[1].name + ): self.players[1].name += "2" if self.sc2_config is not None: @@ -107,7 +111,8 @@ async def _play_game_human(client, player_id, realtime, game_time_limit): async def _play_game_ai( client: Client, player_id: int, ai: BotAI, realtime: bool, game_time_limit: int | None ) -> Result: - gs: GameState | None = None + # pyrefly: ignore + gs: GameState = None async def initialize_first_step() -> Result | None: nonlocal gs @@ -205,10 +210,10 @@ async def run_bot_iteration(iteration: int): async def _play_game( - player: AbstractPlayer, + player: Human | Bot, client: Client, realtime: bool, - portconfig: Portconfig, + portconfig: Portconfig | None = None, game_time_limit: int | None = None, rgb_render_config: dict[str, Any] | None = None, ) -> Result: @@ -336,7 +341,7 @@ async def _setup_host_game( async def _host_game( map_settings: Map, - players: list[AbstractPlayer], + players: list[Human | Bot | Computer] | list[Human | Bot], realtime: bool = False, portconfig: Portconfig | None = None, save_replay_as: str | None = None, @@ -349,6 +354,7 @@ async def _host_game( assert players, "Can't create a game without players" assert any((isinstance(p, (Human, Bot))) for p in players) + assert isinstance(players[0], (Human, Bot)), "First player needs to be a Human or a Bot" async with SC2Process( fullscreen=players[0].fullscreen, render=rgb_render_config is not None, sc2_version=sc2_version @@ -359,7 +365,7 @@ async def _host_game( server, map_settings, players, realtime, random_seed, disable_fog, save_replay_as ) # Bot can decide if it wants to launch with 'raw_affects_selection=True' - if not isinstance(players[0], Human) and getattr(players[0].ai, "raw_affects_selection", None) is not None: + if isinstance(players[0], Bot) and getattr(players[0].ai, "raw_affects_selection", None) is not None: client.raw_affects_selection = players[0].ai.raw_affects_selection result = await _play_game(players[0], client, realtime, portconfig, game_time_limit, rgb_render_config) @@ -378,7 +384,7 @@ async def _host_game_aiter( map_settings, players, realtime, - portconfig=None, + portconfig, save_replay_as=None, game_time_limit=None, ): @@ -417,9 +423,9 @@ def _host_game_iter(*args, **kwargs): async def _join_game( - players: list[AbstractPlayer], - realtime: bool = False, - portconfig: Portconfig | None = None, + players: list[Human | Bot], + realtime: bool, + portconfig: Portconfig, save_replay_as: str | None = None, game_time_limit: int | None = None, sc2_version: str | None = None, @@ -465,6 +471,7 @@ def get_replay_version(replay_path: str | Path) -> tuple[str, str]: replay_io.write(replay_data) replay_io.seek(0) archive = mpyq.MPQArchive(replay_io).extract() + # pyrefly: ignore metadata = json.loads(archive[b"replay.gamemetadata.json"].decode("utf-8")) return metadata["BaseBuild"], metadata["DataVersion"] @@ -472,7 +479,7 @@ def get_replay_version(replay_path: str | Path) -> tuple[str, str]: # TODO Deprecate run_game function in favor of run_multiple_games def run_game( map_settings: Map, - players: list[AbstractPlayer], + players: list[Human | Bot | Computer], realtime: bool, portconfig: Portconfig | None = None, save_replay_as: str | None = None, @@ -486,14 +493,16 @@ def run_game( Returns a single Result enum if the game was against the built-in computer. Returns a list of two Result enums if the game was "Human vs Bot" or "Bot vs Bot". """ + result: Result | list[Result | None] if sum(isinstance(p, (Human, Bot)) for p in players) > 1: portconfig = Portconfig() + players_non_computer: list[Human | Bot] = [p for p in players if isinstance(p, (Human, Bot))] async def run_host_and_join(): return await asyncio.gather( _host_game( map_settings, - players, + players_non_computer, realtime=realtime, portconfig=portconfig, save_replay_as=save_replay_as, @@ -504,7 +513,7 @@ async def run_host_and_join(): disable_fog=disable_fog, ), _join_game( - players, + players_non_computer, realtime=realtime, portconfig=portconfig, save_replay_as=save_replay_as, @@ -514,11 +523,12 @@ async def run_host_and_join(): return_exceptions=True, ) - result: list[Result] = asyncio.run(run_host_and_join()) + # pyrefly: ignore + result = asyncio.run(run_host_and_join()) assert isinstance(result, list) assert all(isinstance(r, Result) for r in result) else: - result: Result = asyncio.run( + result = asyncio.run( _host_game( map_settings, players, @@ -551,9 +561,9 @@ def run_replay(ai: ObserverAI, replay_path: Path | str, realtime: bool = False, async def play_from_websocket( ws_connection: str | ClientWebSocketResponse, - player: AbstractPlayer, - realtime: bool = False, - portconfig: Portconfig | None = None, + player: Human | Bot, + realtime: bool, + portconfig: Portconfig, save_replay_as: str | None = None, game_time_limit: int | None = None, should_close: bool = True, @@ -569,6 +579,7 @@ async def play_from_websocket( try: if isinstance(ws_connection, str): session = ClientSession() + # pyrefly: ignore ws_connection = await session.ws_connect(ws_connection, timeout=120) should_close = True client = Client(ws_connection) @@ -592,7 +603,7 @@ async def run_match(controllers: list[Controller], match: GameMatch, close_ws: b # Setup portconfig beforehand, so all players use the same ports startport = None - portconfig = None + portconfig: Portconfig = None # pyrefly: ignore if match.needed_sc2_count > 1: if any(isinstance(player, BotProcess) for player in match.players): portconfig = Portconfig.contiguous_ports() @@ -624,12 +635,12 @@ async def run_match(controllers: list[Controller], match: GameMatch, close_ws: b async_results = await asyncio.gather(*coros, return_exceptions=True) - if not isinstance(async_results, list): - async_results = [async_results] for i, a in enumerate(async_results): if isinstance(a, Exception): logger.error(f"Exception[{a}] thrown by {[p for p in match.players if p.needs_sc2][i]}") + # TODO async_results may contain exceptions + # pyrefly: ignore return process_results(match.players, async_results) @@ -644,9 +655,10 @@ def process_results(players: list[AbstractPlayer], async_results: list[Result]) else: result[player] = Result.Undecided i += 1 - else: # computer + else: + # Computer other_result = async_results[0] - result[player] = None + result[player] = Result.Undecided if other_result in opp_res: result[player] = opp_res[other_result] @@ -659,12 +671,14 @@ async def maintain_SCII_count(count: int, controllers: list[Controller], proc_ar if controllers: to_remove = [] alive = await asyncio.wait_for( - asyncio.gather(*(c.ping() for c in controllers if not c._ws.closed), return_exceptions=True), timeout=20 + # pyrefly: ignore + asyncio.gather(*(c.ping() for c in controllers if not c._ws.closed), return_exceptions=True), + timeout=20, ) i = 0 # for alive for controller in controllers: if controller._ws.closed: - if not controller._process._session.closed: + if controller._process._session is not None and not controller._process._session.closed: await controller._process._session.close() to_remove.append(controller) else: @@ -698,12 +712,14 @@ async def maintain_SCII_count(count: int, controllers: list[Controller], proc_ar else: # Doesnt seem to work on linux: starting 2 clients nearly at the same time new_controllers = await asyncio.wait_for( + # pyrefly: ignore asyncio.gather(*[sc.__aenter__() for sc in extra], return_exceptions=True), timeout=50, ) controllers.extend(c for c in new_controllers if isinstance(c, Controller)) if len(controllers) == count: + # pyrefly: ignore await asyncio.wait_for(asyncio.gather(*(c.ping() for c in controllers)), timeout=20) break extra = [ @@ -739,8 +755,8 @@ async def a_run_multiple_games(matches: list[GameMatch]) -> list[dict[AbstractPl if not matches: return [] - results = [] - controllers = [] + results: list[dict[AbstractPlayer, Result]] = [] + controllers: list[Controller] = [] for m in matches: result = None dont_restart = m.needed_sc2_count == 2 @@ -755,7 +771,8 @@ async def a_run_multiple_games(matches: list[GameMatch]) -> list[dict[AbstractPl finally: if dont_restart: # Keeping them alive after a non-computer match can cause crashes await maintain_SCII_count(0, controllers, m.sc2_config) - results.append(result) + if result is not None: + results.append(result) KillSwitch.kill_all() return results @@ -772,7 +789,7 @@ async def a_run_multiple_games_nokill(matches: list[GameMatch]) -> list[dict[Abs return [] # Start the matches - results = [] + results: list[dict[AbstractPlayer, Result]] = [] controllers: list[Controller] = [] for m in matches: logger.info(f"Starting match {1 + len(results)} / {len(matches)}: {m}") @@ -795,10 +812,11 @@ async def a_run_multiple_games_nokill(matches: list[GameMatch]) -> list[dict[Abs logger.exception(f"Caught unknown exception: {e}") if not (isinstance(e, ProtocolError) and e.is_game_over_error): logger.info(f"controller {c.__dict__} threw {e}") - - results.append(result) + if result is not None: + results.append(result) # Fire the killswitch manually, instead of letting the winning player fire it. + # pyrefly: ignore await asyncio.wait_for(asyncio.gather(*(c._process._close_connection() for c in controllers)), timeout=50) KillSwitch.kill_all() signal.signal(signal.SIGINT, signal.SIG_DFL) diff --git a/sc2/observer_ai.py b/sc2/observer_ai.py index 7cd23c99..814fe455 100644 --- a/sc2/observer_ai.py +++ b/sc2/observer_ai.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 11, 16] """ This class is very experimental and probably not up to date and needs to be refurbished. If it works, you can watch replays with it. diff --git a/sc2/paths.py b/sc2/paths.py index 10ec26bb..86eac639 100644 --- a/sc2/paths.py +++ b/sc2/paths.py @@ -55,7 +55,7 @@ def platform_detect(): return pf -PF = platform_detect() +PF: str = platform_detect() def get_home(): @@ -68,12 +68,12 @@ def get_home(): def get_user_sc2_install(): """Attempts to find a user's SC2 install if their OS has ExecuteInfo.txt""" if USERPATH[PF]: - einfo = str(get_home() / Path(USERPATH[PF])) + einfo = str(get_home() / Path(USERPATH[PF])) # pyrefly: ignore if Path(einfo).is_file(): with Path(einfo).open() as f: content = f.read() if content: - base = re.search(r" = (.*)Versions", content).group(1) + base = re.search(r" = (.*)Versions", content).group(1) # pyrefly: ignore if PF in {"WSL1", "WSL2"}: base = str(wsl.win_path_to_wsl_path(base)) @@ -88,8 +88,9 @@ def get_env() -> None: def get_runner_args(cwd): - if "WINE" in os.environ: - runner_file = Path(os.environ.get("WINE")) + wine_path = os.environ.get("WINE") + if wine_path is not None: + runner_file = Path(wine_path) runner_file = runner_file if runner_file.is_file() else runner_file / "wine" """ TODO Is converting linux path really necessary? @@ -133,16 +134,16 @@ def __setup(cls): try: base = os.environ.get("SC2PATH") or get_user_sc2_install() or BASEDIR[PF] - cls.BASE = Path(base).expanduser() + cls.BASE = Path(base).expanduser() # pyrefly: ignore cls.EXECUTABLE = latest_executeble(cls.BASE / "Versions") - cls.CWD = cls.BASE / CWD[PF] if CWD[PF] else None + cls.CWD = cls.BASE / CWD[PF] if CWD[PF] else None # pyrefly: ignore - cls.REPLAYS = cls.BASE / "Replays" + cls.REPLAYS = cls.BASE / "Replays" # pyrefly: ignore if (cls.BASE / "maps").exists(): - cls.MAPS = cls.BASE / "maps" + cls.MAPS = cls.BASE / "maps" # pyrefly: ignore else: - cls.MAPS = cls.BASE / "Maps" + cls.MAPS = cls.BASE / "Maps" # pyrefly: ignore except FileNotFoundError as e: logger.critical(f"SC2 installation not found: File '{e.filename}' does not exist.") sys.exit(1) diff --git a/sc2/pixel_map.py b/sc2/pixel_map.py index c6925d80..4b4af8f0 100644 --- a/sc2/pixel_map.py +++ b/sc2/pixel_map.py @@ -6,7 +6,7 @@ import numpy as np from s2clientprotocol.common_pb2 import ImageData -from sc2.position import Point2 +from sc2.position import Point2, _PointLike class PixelMap: @@ -43,13 +43,14 @@ def bits_per_pixel(self) -> int: def bytes_per_pixel(self) -> int: return self._proto.bits_per_pixel // 8 - def __getitem__(self, pos: tuple[int, int]) -> int: + def __getitem__(self, pos: _PointLike) -> int: """Example usage: is_pathable = self._game_info.pathing_grid[Point2((20, 20))] != 0""" assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" + # pyrefly: ignore return int(self.data_numpy[pos[1], pos[0]]) - def __setitem__(self, pos: tuple[int, int], value: int) -> None: + def __setitem__(self, pos: _PointLike, value: int) -> None: """Example usage: self._game_info.pathing_grid[Point2((20, 20))] = 255""" assert 0 <= pos[0] < self.width, f"x is {pos[0]}, self.width is {self.width}" assert 0 <= pos[1] < self.height, f"y is {pos[1]}, self.height is {self.height}" @@ -57,6 +58,7 @@ def __setitem__(self, pos: tuple[int, int], value: int) -> None: f"value is {value}, it should be between 0 and {254 * self._in_bits + 1}" ) assert isinstance(value, int), f"value is of type {type(value)}, it should be an integer" + # pyrefly: ignore self.data_numpy[pos[1], pos[0]] = value def is_set(self, p: tuple[int, int]) -> bool: @@ -70,7 +72,8 @@ def copy(self) -> PixelMap: def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> set[Point2]: nodes: set[Point2] = set() - queue: list[Point2] = [start_point] + # pyrefly: ignore + queue: list[tuple[int, int]] = [start_point] while queue: x, y = queue.pop() @@ -83,7 +86,7 @@ def flood_fill(self, start_point: Point2, pred: Callable[[int], bool]) -> set[Po if pred(self[x, y]): nodes.add(Point2((x, y))) - queue += [Point2((x + a, y + b)) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)] + queue += [(x + a, y + b) for a in [-1, 0, 1] for b in [-1, 0, 1] if not (a == 0 and b == 0)] return nodes def flood_fill_all(self, pred: Callable[[int], bool]) -> set[frozenset[Point2]]: diff --git a/sc2/player.py b/sc2/player.py index bd1410a5..e624d41c 100644 --- a/sc2/player.py +++ b/sc2/player.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[6, 11, 16, 29] from __future__ import annotations from abc import ABC @@ -68,7 +67,7 @@ def __init__(self, race: Race, ai: BotAI, name: str | None = None, fullscreen: b """ assert isinstance(ai, BotAI) or ai is None, f"ai is of type {type(ai)}, inherit BotAI from bot_ai.py" super().__init__(PlayerType.Participant, race, name=name, fullscreen=fullscreen) - self.ai = ai + self.ai: BotAI = ai def __str__(self) -> str: if self.name is not None: @@ -83,7 +82,9 @@ def __init__( super().__init__(PlayerType.Computer, race, difficulty=difficulty, ai_build=ai_build) def __str__(self) -> str: - return f"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})" + if self.ai_build is not None: + return f"Computer {self.difficulty._name_}({self.race._name_}, {self.ai_build.name})" + return f"Computer {self.difficulty._name_}({self.race._name_})" class Observer(AbstractPlayer): @@ -99,7 +100,8 @@ def __init__( self, player_id: int, p_type: PlayerType, - requested_race: Race, + # None in case of observer + requested_race: Race | None, difficulty: Difficulty | None = None, actual_race: Race | None = None, name: str | None = None, diff --git a/sc2/portconfig.py b/sc2/portconfig.py index 9041b90f..ca022c16 100644 --- a/sc2/portconfig.py +++ b/sc2/portconfig.py @@ -26,20 +26,20 @@ class Portconfig: """ def __init__( - self, guests: int = 1, server_ports: list[int] | None = None, player_ports: list[int] | None = None + self, guests: int = 1, server_ports: list[int] | None = None, player_ports: list[list[int]] | None = None ) -> None: self.shared = None self._picked_ports: list[int] = [] if server_ports: - self.server = server_ports + self.server: list[int] = server_ports else: self.server = [portpicker.pick_unused_port() for _ in range(2)] self._picked_ports.extend(self.server) if player_ports: - self.players = player_ports + self.players: list[list[int]] = player_ports else: self.players = [[portpicker.pick_unused_port() for _ in range(2)] for _ in range(guests)] - self._picked_ports.extend(port for player in self.players for port in player) + self._picked_ports.extend([port for player in self.players for port in player]) def clean(self) -> None: while self._picked_ports: diff --git a/sc2/position.py b/sc2/position.py index f3d9bd70..9ba15f93 100644 --- a/sc2/position.py +++ b/sc2/position.py @@ -43,8 +43,9 @@ def distance_to(self, target: _PosLike) -> float: """Calculate a single distance from a point or unit to another point or unit :param target:""" - p: tuple[float, ...] = target if isinstance(target, tuple) else target.position - return math.hypot(self[0] - p[0], self[1] - p[1]) + # pyrefly: ignore + position: tuple[float, ...] = target if isinstance(target, tuple) else target.position + return math.hypot(self[0] - position[0], self[1] - position[1]) def distance_to_point2(self, p: _PointLike) -> float: """Same as the function above, but should be a bit faster because of the dropped asserts @@ -82,6 +83,7 @@ def distance_to_closest(self, ps: Iterable[_TPosLike]) -> float: assert ps, "ps is empty" closest_distance = math.inf for p in ps: + # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position distance = self.distance_to_point2(p2) if distance <= closest_distance: @@ -103,6 +105,7 @@ def distance_to_furthest(self, ps: Iterable[_PosLike]) -> float: assert ps, "ps is empty" furthest_distance = -math.inf for p in ps: + # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position distance = self.distance_to_point2(p2) if distance >= furthest_distance: @@ -130,6 +133,7 @@ def towards(self: T, p: _PosLike, distance: float = 1, limit: bool = False) -> T :param distance: :param limit: """ + # pyrefly: ignore p2: tuple[float, ...] = p if isinstance(p, tuple) else p.position # assert self != p, f"self is {self}, p is {p}" # TODO test and fix this if statement @@ -284,7 +288,7 @@ def neighbors8(self: T) -> set[T]: def negative_offset(self: T, other: Point2) -> T: return self.__class__((self[0] - other[0], self[1] - other[1])) - def __add__(self, other: Point2) -> Point2: # pyright: ignore[reportIncompatibleMethodOverride] + def __add__(self, other: Point2) -> Point2: return self.offset(other) def __sub__(self, other: Point2) -> Point2: @@ -299,12 +303,12 @@ def __abs__(self) -> float: def __bool__(self) -> bool: return self[0] != 0 or self[1] != 0 - def __mul__(self, other: _PointLike | float) -> Point2: # pyright: ignore[reportIncompatibleMethodOverride] + def __mul__(self, other: _PointLike | float) -> Point2: if isinstance(other, (int, float)): return Point2((self[0] * other, self[1] * other)) return Point2((self[0] * other[0], self[1] * other[1])) - def __rmul__(self, other: _PointLike | float) -> Point2: # pyright: ignore[reportIncompatibleMethodOverride] + def __rmul__(self, other: _PointLike | float) -> Point2: return self.__mul__(other) def __truediv__(self, other: float | Point2) -> Point2: @@ -338,7 +342,7 @@ def center(points: list[Point2]) -> Point2: class Point3(Point2): @classmethod - def from_proto(cls, data: common_pb.Point | Point3) -> Point3: # pyright: ignore[reportIncompatibleMethodOverride] + def from_proto(cls, data: common_pb.Point | Point3) -> Point3: """ :param data: """ @@ -387,7 +391,7 @@ def height(self) -> float: class Rect(Point2): @classmethod - def from_proto(cls, data: common_pb.RectangleI) -> Rect: # pyright: ignore[reportIncompatibleMethodOverride] + def from_proto(cls, data: common_pb.RectangleI) -> Rect: """ :param data: """ @@ -425,7 +429,7 @@ def size(self) -> Size: return Size((self[2], self[3])) @property - def center(self) -> Point2: # pyright: ignore[reportIncompatibleMethodOverride] + def center(self) -> Point2: return Point2((self.x + self.width / 2, self.y + self.height / 2)) def offset(self, p: _PointLike) -> Rect: diff --git a/sc2/protocol.py b/sc2/protocol.py index 2722abe0..d2fae1aa 100644 --- a/sc2/protocol.py +++ b/sc2/protocol.py @@ -113,7 +113,7 @@ async def _execute(self, debug: sc_pb.RequestDebug) -> sc_pb.Response: ... async def _execute(self, **kwargs) -> sc_pb.Response: assert len(kwargs) == 1, "Only one request allowed by the API" - response = await self.__request(sc_pb.Request(**kwargs)) + response: sc_pb.Response = await self.__request(sc_pb.Request(**kwargs)) new_status = Status(response.status) if new_status != self._status: diff --git a/sc2/proxy.py b/sc2/proxy.py index 340570cd..4ea563f5 100644 --- a/sc2/proxy.py +++ b/sc2/proxy.py @@ -1,10 +1,10 @@ -# pyre-ignore-all-errors[16, 29] from __future__ import annotations import asyncio import os import platform import subprocess +import sys import time import traceback from pathlib import Path @@ -59,7 +59,8 @@ async def parse_request(self, msg) -> None: elif self.controller._status == Status.ended: await self.get_response() elif request.HasField("join_game") and not request.join_game.HasField("player_name"): - request.join_game.player_name = self.player.name + if self.player.name is not None: + request.join_game.player_name = self.player.name await self.controller._ws.send_bytes(request.SerializeToString()) # TODO Catching too general exception Exception (broad-except) @@ -175,8 +176,9 @@ async def play_with_proxy(self, startport): subproc_args = {"cwd": str(self.player.path), "stderr": subprocess.STDOUT} if platform.system() == "Linux": + # pyrefly: ignore subproc_args["preexec_fn"] = os.setpgrp - elif platform.system() == "Windows": + elif platform.system() == "Windows" and sys.platform == "win32": subproc_args["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP player_command_line = self.player.cmd_line(self.port, startport, self.controller._process._host, self.realtime) diff --git a/sc2/renderer.py b/sc2/renderer.py index 17e3599e..7b7d1165 100644 --- a/sc2/renderer.py +++ b/sc2/renderer.py @@ -1,5 +1,6 @@ from __future__ import annotations + import datetime from typing import TYPE_CHECKING @@ -9,23 +10,26 @@ if TYPE_CHECKING: from sc2.client import Client + from pyglet.image import ImageData + from pyglet.text import Label + from pyglet.window import Window class Renderer: def __init__(self, client: Client, map_size: tuple[float, float], minimap_size: tuple[float, float]) -> None: self._client = client - self._window = None + self._window: Window = None # pyrefly: ignore self._map_size = map_size - self._map_image = None + self._map_image: ImageData = None # pyrefly: ignore self._minimap_size = minimap_size - self._minimap_image = None + self._minimap_image: ImageData = None # pyrefly: ignore self._mouse_x, self._mouse_y = None, None - self._text_supply = None - self._text_vespene = None - self._text_minerals = None - self._text_score = None - self._text_time = None + self._text_supply: Label = None # pyrefly: ignore + self._text_vespene: Label = None # pyrefly: ignore + self._text_minerals: Label = None # pyrefly: ignore + self._text_score: Label = None # pyrefly: ignore + self._text_time: Label = None # pyrefly: ignore async def render(self, observation: ResponseObservation) -> None: render_data = observation.observation.render_data @@ -47,11 +51,11 @@ async def render(self, observation: ResponseObservation) -> None: from pyglet.window import Window self._window = Window(width=map_width, height=map_height) - # pyre-fixme[16] + # pyrefly: ignore self._window.on_mouse_press = self._on_mouse_press - # pyre-fixme[16] + # pyrefly: ignore self._window.on_mouse_release = self._on_mouse_release - # pyre-fixme[16] + # pyrefly: ignore self._window.on_mouse_drag = self._on_mouse_drag self._map_image = ImageData(map_width, map_height, "RGB", map_data, map_pitch) self._minimap_image = ImageData(minimap_width, minimap_height, "RGB", minimap_data, minimap_pitch) @@ -114,6 +118,7 @@ async def render(self, observation: ResponseObservation) -> None: self._text_vespene.text = str(observation.observation.player_common.vespene) self._text_minerals.text = str(observation.observation.player_common.minerals) if observation.observation.HasField("score"): + # pyrefly: ignore self._text_score.text = f"{score_pb._SCORE_SCORETYPE.values_by_number[observation.observation.score.score_type].name} score: {observation.observation.score.score}" await self._update_window() diff --git a/sc2/sc2process.py b/sc2/sc2process.py index 391d30b1..0017c01d 100644 --- a/sc2/sc2process.py +++ b/sc2/sc2process.py @@ -132,9 +132,9 @@ def versions(self): def find_data_hash(self, target_sc2_version: str) -> str | None: """Returns the data hash from the matching version string.""" - version: dict for version in self.versions: if version["label"] == target_sc2_version: + # pyrefly: ignore return version["data-hash"] return None @@ -154,7 +154,7 @@ def _launch(self): else: executable = str(Paths.EXECUTABLE) - if self._port is None: + if self._port == -1: self._port = portpicker.pick_unused_port() self._used_portpicker = True args = paths.get_runner_args(Paths.CWD) + [ @@ -222,6 +222,7 @@ async def _connect(self) -> ClientWebSocketResponse: await asyncio.sleep(1) try: self._session = aiohttp.ClientSession() + # pyrefly: ignore ws = await self._session.ws_connect(self.ws_url, timeout=120) # FIXME fix deprecation warning in for future aiohttp version # ws = await self._session.ws_connect( @@ -229,8 +230,9 @@ async def _connect(self) -> ClientWebSocketResponse: # ) logger.debug("Websocket connection ready") return ws - except aiohttp.client_exceptions.ClientConnectorError: - await self._session.close() + except aiohttp.ClientConnectorError: + if self._session is not None: + await self._session.close() if i > 15: logger.debug("Connection refused (startup not complete (yet))") @@ -266,6 +268,7 @@ def _clean(self, verbose: bool = True) -> None: self._process.wait() logger.error("KILLED") # Try to kill wineserver on linux + # pyrefly: ignore if paths.PF in {"Linux", "WineLinux"}: # Command wineserver not detected with suppress(FileNotFoundError), subprocess.Popen(["wineserver", "-k"]) as p: @@ -278,6 +281,6 @@ def _clean(self, verbose: bool = True) -> None: self._ws = None if self._used_portpicker and self._port is not None: portpicker.return_port(self._port) - self._port = None + self._port = -1 if verbose: logger.info("Cleanup complete") diff --git a/sc2/score.py b/sc2/score.py index aba9c8ff..82e8a823 100644 --- a/sc2/score.py +++ b/sc2/score.py @@ -13,7 +13,7 @@ def __init__(self, proto: score_pb2.Score) -> None: self._proto = proto.score_details @property - def summary(self) -> list[list[int | float]]: + def summary(self) -> list[list[float]]: """ TODO this is super ugly, how can we improve this summary? Print summary to file with: @@ -105,6 +105,7 @@ def summary(self) -> list[list[int | float]]: "current_apm", "current_effective_apm", ] + # pyrefly: ignore return [[value, getattr(self, value)] for value in values] @property diff --git a/sc2/unit.py b/sc2/unit.py index 236116f9..73c519b0 100644 --- a/sc2/unit.py +++ b/sc2/unit.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[11, 16, 29] from __future__ import annotations import math @@ -53,7 +52,7 @@ UNIT_PHOTONCANNON, transforming, ) -from sc2.data import Alliance, Attribute, CloakState, Race, Target, race_gas, warpgate_abilities +from sc2.data import Attribute, CloakState, Race, Target, race_gas, warpgate_abilities from sc2.ids.ability_id import AbilityId from sc2.ids.buff_id import BuffId from sc2.ids.unit_typeid import UnitTypeId @@ -63,7 +62,6 @@ if TYPE_CHECKING: from sc2.bot_ai import BotAI - from sc2.bot_ai_internal import BotAIInternal from sc2.game_data import AbilityData, UnitTypeData @@ -109,7 +107,7 @@ class Unit(HasPosition2D): def __init__( self, proto_data: raw_pb2.Unit, - bot_object: BotAI | BotAIInternal, + bot_object: BotAI, distance_calculation_index: int = -1, base_build: int = -1, ) -> None: @@ -287,7 +285,7 @@ def air_range(self) -> float: return 0 @cached_property - def bonus_damage(self) -> tuple[int, str] | None: + def bonus_damage(self) -> tuple[float, str] | None: """Returns a tuple of form '(bonus damage, armor type)' if unit does 'bonus damage' against 'armor type'. Possible armor typs are: 'Light', 'Armored', 'Biological', 'Mechanical', 'Psionic', 'Massive', 'Structure'.""" # TODO: Consider units with ability attacks (Oracle, Baneling) or multiple attacks (Thor). @@ -472,7 +470,8 @@ def is_snapshot(self) -> bool: if self.base_build >= 82457: return self._proto.display_type == IS_SNAPSHOT # TODO: Fixed in version 5.0.4, remove if a new linux binary is released: https://github.com/Blizzard/s2client-proto/issues/167 - position = self.position.rounded + # pyrefly: ignore + position: tuple[int, int] = self.position.rounded return self._bot_object.state.visibility.data_numpy[position[1], position[0]] != 2 @cached_property @@ -504,7 +503,7 @@ def is_placeholder(self) -> bool: return self._proto.display_type == IS_PLACEHOLDER @property - def alliance(self) -> Alliance: + def alliance(self) -> int: """Returns the team the unit belongs to.""" return self._proto.alliance @@ -529,7 +528,7 @@ def position_tuple(self) -> tuple[float, float]: return self._proto.pos.x, self._proto.pos.y @cached_property - def position(self) -> Point2: # pyright: ignore[reportIncompatibleMethodOverride] + def position(self) -> Point2: """Returns the 2d position of the unit.""" return Point2.from_proto(self._proto.pos) @@ -594,9 +593,7 @@ def in_ability_cast_range(self, ability_id: AbilityId, target: Unit | Point2, bo <= (cast_range + self.radius + target.radius + bonus_distance) ** 2 ) # For casting abilities on the ground, like queen creep tumor, ravager bile, HT storm - if ability_target_type in {Target.Point.value, Target.PointOrUnit.value} and isinstance( - target, (Point2, tuple) - ): + if ability_target_type in {Target.Point.value, Target.PointOrUnit.value} and isinstance(target, Point2): return ( self._bot_object._distance_pos_to_pos(self.position_tuple, target) <= cast_range + self.radius + bonus_distance @@ -1028,6 +1025,7 @@ def buff_duration_max(self) -> int: def orders(self) -> list[UnitOrder]: """Returns the a list of the current orders.""" # TODO: add examples on how to use unit orders + # pyrefly: ignore return [UnitOrder.from_proto(order, self._bot_object) for order in self._proto.orders] @cached_property @@ -1154,6 +1152,7 @@ def add_on_position(self) -> Point2: @cached_property def passengers(self) -> set[Unit]: """Returns the units inside a Bunker, CommandCenter, PlanetaryFortress, Medivac, Nydus, Overlord or WarpPrism.""" + # pyrefly: ignore return {Unit(unit, self._bot_object) for unit in self._proto.passengers} @cached_property @@ -1259,8 +1258,12 @@ def train( :param queue: :param can_afford_check: """ + creation_ability = self._bot_object.game_data.units[unit.value].creation_ability + if creation_ability is None: + return False + return self( - self._bot_object.game_data.units[unit.value].creation_ability.id, + creation_ability.id, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, @@ -1279,7 +1282,7 @@ def build( SCV.build(COMMANDCENTER, position) hatchery.build(UnitTypeId.LAIR) # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position - SCV.build(REFINERY, target_vespene_geysir) + SCV.build(REFINERY, target_vespene_geyser) :param unit: :param position: @@ -1290,8 +1293,11 @@ def build( assert isinstance(position, Unit), ( "When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir." ) + creation_ability = self._bot_object.game_data.units[unit.value].creation_ability + if creation_ability is None: + return False return self( - self._bot_object.game_data.units[unit.value].creation_ability.id, + creation_ability.id, target=position, queue=queue, subtract_cost=True, @@ -1308,7 +1314,7 @@ def build_gas( Usage:: # Target for refinery, assimilator and extractor needs to be the vespene geysir unit, not its position - SCV.build_gas(target_vespene_geysir) + SCV.build_gas(target_vespene_geyser) :param target_geysir: :param queue: @@ -1318,8 +1324,11 @@ def build_gas( assert isinstance(target_geysir, Unit), ( "When building the gas structure, the target needs to be a unit (the vespene geysir) not the position of the vespene geysir." ) + creation_ability = self._bot_object.game_data.units[gas_structure_type_id.value].creation_ability + if creation_ability is None: + return False return self( - self._bot_object.game_data.units[gas_structure_type_id.value].creation_ability.id, + creation_ability.id, target=target_geysir, queue=queue, subtract_cost=True, @@ -1339,8 +1348,11 @@ def research( :param queue: :param can_afford_check: """ + research_ability = self._bot_object.game_data.upgrades[upgrade.value].research_ability + if research_ability is None: + return False return self( - self._bot_object.game_data.upgrades[upgrade.value].research_ability.exact_id, + research_ability.exact_id, queue=queue, subtract_cost=True, can_afford_check=can_afford_check, @@ -1358,9 +1370,11 @@ def warp_in( :param queue: :param can_afford_check: """ - normal_creation_ability = self._bot_object.game_data.units[unit.value].creation_ability.id + creation_ability = self._bot_object.game_data.units[unit.value].creation_ability + if creation_ability is None: + return False return self( - warpgate_abilities[normal_creation_ability], + warpgate_abilities[creation_ability.id], target=position, subtract_cost=True, subtract_supply=True, diff --git a/sc2/units.py b/sc2/units.py index 1871dfc6..3e58b1d2 100644 --- a/sc2/units.py +++ b/sc2/units.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[14, 15, 16] from __future__ import annotations import random diff --git a/sc2/versions.py b/sc2/versions.py index 96c2f35f..5292f233 100644 --- a/sc2/versions.py +++ b/sc2/versions.py @@ -1,4 +1,6 @@ -VERSIONS = [ +from __future__ import annotations + +VERSIONS: list[dict[str, int | str]] = [ { "base-version": 52910, "data-hash": "8D9FEF2E1CF7C6C9CBE4FBCA830DDE1C", diff --git a/sc2/wsl.py b/sc2/wsl.py index 4f2a3cdd..dcdfecec 100644 --- a/sc2/wsl.py +++ b/sc2/wsl.py @@ -93,10 +93,11 @@ def detect() -> str | None: # Unix-style newlines for safety's sake. lines = re.sub(r"\000|\r", "", wsl_proc.stdout.decode("utf-8")).split("\n") - def line_has_proc(ln): - return re.search("^\\s*[*]?\\s+" + wsl_name, ln) + def line_has_proc(ln: str): + if wsl_name is not None: + return re.search("^\\s*[*]?\\s+" + wsl_name, ln) - def line_version(ln): + def line_version(ln: str): return re.sub("^.*\\s+(\\d+)\\s*$", "\\1", ln) versions = [line_version(ln) for ln in lines if line_has_proc(ln)] diff --git a/test/autotest_bot.py b/test/autotest_bot.py index a6a0dc23..7d3ebbbf 100644 --- a/test/autotest_bot.py +++ b/test/autotest_bot.py @@ -216,7 +216,7 @@ async def test_botai_actions4(self): async def test_botai_actions5(self): # Test BotAI action: self.expand_now() which tests for get_next_expansion, select_build_worker, can_place, find_placement, build and can_afford # Wait till worker has started construction of CC - while 1: + while True: if self.can_afford(UnitTypeId.COMMANDCENTER): await self.get_next_expansion() await self.expand_now() @@ -241,9 +241,9 @@ async def test_botai_actions6(self): # Test if reaper grenade shows up in effects center = self.game_info.map_center - while 1: + while True: if self.units(UnitTypeId.REAPER).amount < 10: - await self.client.debug_create_unit([[UnitTypeId.REAPER, 10, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.REAPER, 10, center, 1)]) for reaper in self.units(UnitTypeId.REAPER): reaper(AbilityId.KD8CHARGE_KD8CHARGE, center) @@ -266,9 +266,9 @@ async def test_botai_actions6(self): async def test_botai_actions7(self): # Test ravager effects center = self.game_info.map_center - while 1: + while True: if self.units(UnitTypeId.RAVAGER).amount < 10: - await self.client.debug_create_unit([[UnitTypeId.RAVAGER, 10, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.RAVAGER, 10, center, 1)]) for ravager in self.units(UnitTypeId.RAVAGER): ravager(AbilityId.EFFECT_CORROSIVEBILE, center) @@ -291,15 +291,15 @@ async def test_botai_actions8(self): # Test if train function works on hatchery, lair, hive center = self.game_info.map_center if not self.structures(UnitTypeId.HIVE): - await self.client.debug_create_unit([[UnitTypeId.HIVE, 1, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.HIVE, 1, center, 1)]) if not self.structures(UnitTypeId.LAIR): - await self.client.debug_create_unit([[UnitTypeId.LAIR, 1, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.LAIR, 1, center, 1)]) if not self.structures(UnitTypeId.HATCHERY): - await self.client.debug_create_unit([[UnitTypeId.HATCHERY, 1, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.HATCHERY, 1, center, 1)]) if not self.structures(UnitTypeId.SPAWNINGPOOL): - await self.client.debug_create_unit([[UnitTypeId.SPAWNINGPOOL, 1, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.SPAWNINGPOOL, 1, center, 1)]) - while 1: + while True: townhalls = self.structures.of_type({UnitTypeId.HIVE, UnitTypeId.LAIR, UnitTypeId.HATCHERY}) if townhalls.amount == 3 and self.minerals >= 450 and not self.already_pending(UnitTypeId.QUEEN): self.train(UnitTypeId.QUEEN, amount=3) @@ -324,14 +324,14 @@ async def test_botai_actions9(self): center = self.game_info.map_center await self.client.debug_create_unit( [ - [UnitTypeId.HIGHTEMPLAR, 1, center, 1], - [UnitTypeId.DARKTEMPLAR, 1, center + Point2((5, 0)), 1], + (UnitTypeId.HIGHTEMPLAR, 1, center, 1), + (UnitTypeId.DARKTEMPLAR, 1, center + Point2((5, 0)), 1), ] ) await self._advance_steps(4) assert self.already_pending(UnitTypeId.ARCHON) == 0 - while 1: + while True: for templar in self.units.of_type({UnitTypeId.HIGHTEMPLAR, UnitTypeId.DARKTEMPLAR}): templar(AbilityId.MORPH_ARCHON) @@ -365,7 +365,7 @@ async def test_botai_actions10(self): center = self.game_info.map_center target_amount = 400 - while 1: + while True: bane_nests = self.structures(UnitTypeId.BANELINGNEST) lings = self.units(UnitTypeId.ZERGLING) banes = self.units(UnitTypeId.BANELING) @@ -377,10 +377,10 @@ async def test_botai_actions10(self): # Spawn units if not bane_nests: - await self.client.debug_create_unit([[UnitTypeId.BANELINGNEST, 1, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.BANELINGNEST, 1, center, 1)]) current_amount = banes.amount + bane_cocoons.amount + lings.amount if current_amount < target_amount: - await self.client.debug_create_unit([[UnitTypeId.ZERGLING, target_amount - current_amount, center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.ZERGLING, target_amount - current_amount, center, 1)]) if lings.amount >= target_amount and self.minerals >= 10_000 and self.vespene >= 10_000: for ling in lings: @@ -408,11 +408,11 @@ async def test_botai_actions11(self): map_center = self.game_info.map_center while not self.units(UnitTypeId.RAVEN): - await self.client.debug_create_unit([[UnitTypeId.RAVEN, 1, map_center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.RAVEN, 1, map_center, 1)]) await self._advance_steps(2) while not self.enemy_units(UnitTypeId.INFESTOR): - await self.client.debug_create_unit([[UnitTypeId.INFESTOR, 1, map_center, 2]]) + await self.client.debug_create_unit([(UnitTypeId.INFESTOR, 1, map_center, 2)]) await self._advance_steps(2) raven = self.units(UnitTypeId.RAVEN)[0] @@ -421,7 +421,7 @@ async def test_botai_actions11(self): await self._advance_steps(4) enemy = self.enemy_units(UnitTypeId.INFESTOR)[0] - while 1: + while True: raven = self.units(UnitTypeId.RAVEN)[0] raven(AbilityId.EFFECT_ANTIARMORMISSILE, enemy) await self._advance_steps(2) @@ -440,7 +440,7 @@ async def test_botai_actions12(self): await self.client.debug_all_resources() await self._advance_steps(2) - while 1: + while True: # Once depot is under construction: debug kill scv -> advance simulation: should now match the test case if self.structures(UnitTypeId.SUPPLYDEPOT).not_ready.amount == 1: construction_scvs: Units = self.workers.filter(lambda worker: worker.is_constructing_scv) @@ -484,7 +484,6 @@ async def on_start(self): await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): - # pyre-ignore[16] map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: diff --git a/test/battery_overcharge_bot.py b/test/battery_overcharge_bot.py index d948244a..22bb9add 100644 --- a/test/battery_overcharge_bot.py +++ b/test/battery_overcharge_bot.py @@ -18,9 +18,9 @@ async def on_start(self): """Spawn requires structures.""" await self.client.debug_create_unit( [ - [UnitTypeId.PYLON, 1, self.start_location.towards(self.game_info.map_center, 5), 1], - [UnitTypeId.SHIELDBATTERY, 1, self.start_location.towards(self.game_info.map_center, 5), 1], - [UnitTypeId.CYBERNETICSCORE, 1, self.start_location.towards(self.game_info.map_center, 5), 1], + (UnitTypeId.PYLON, 1, self.start_location.towards(self.game_info.map_center, 5), 1), + (UnitTypeId.SHIELDBATTERY, 1, self.start_location.towards(self.game_info.map_center, 5), 1), + (UnitTypeId.CYBERNETICSCORE, 1, self.start_location.towards(self.game_info.map_center, 5), 1), ] ) diff --git a/test/benchmark_distance_two_points.py b/test/benchmark_distance_two_points.py index 9527a107..dbfe10af 100644 --- a/test/benchmark_distance_two_points.py +++ b/test/benchmark_distance_two_points.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[21] from __future__ import annotations import math @@ -47,7 +46,7 @@ def distance_numpy_linalg_norm(p1, p2): def distance_sum_squared_sqrt(p1, p2) -> int | float: """Distance calculation using numpy""" - return np.sqrt(np.sum((p1 - p2) ** 2)) + return np.sqrt(np.sum((p1 - p2) ** 2)) # pyrefly: ignore def distance_sum_squared(p1, p2) -> int | float: diff --git a/test/benchmark_distances_cdist.py b/test/benchmark_distances_cdist.py index fdcfd7b8..8d02d083 100644 --- a/test/benchmark_distances_cdist.py +++ b/test/benchmark_distances_cdist.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[21] import random import numpy as np @@ -57,7 +56,7 @@ def distance_matrix_scipy_cdist_mahalanobis(ps): def distance_matrix_scipy_cdist_matching(ps): # Calculate distances between each of the points - return cdist(ps, ps, "matching") + return cdist(ps, ps, "matching") # pyrefly: ignore # def distance_matrix_scipy_cdist_minkowski(ps): diff --git a/test/benchmark_distances_points_to_point.py b/test/benchmark_distances_points_to_point.py index cd36c8d8..f04d5849 100644 --- a/test/benchmark_distances_points_to_point.py +++ b/test/benchmark_distances_points_to_point.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[21] from __future__ import annotations import math diff --git a/test/benchmark_distances_units.py b/test/benchmark_distances_units.py index 11d81462..c281045a 100644 --- a/test/benchmark_distances_units.py +++ b/test/benchmark_distances_units.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[21] import math import random diff --git a/test/damagetest_bot.py b/test/damagetest_bot.py index 962c2bd8..6d0f8318 100644 --- a/test/damagetest_bot.py +++ b/test/damagetest_bot.py @@ -231,8 +231,6 @@ def do_some_unit_property_tests(attacker: Unit, defender: Unit): await self.clean_up_center() - attacker: Unit - defender: Unit for upgrade_level in upgrade_levels: if upgrade_level != 0: await self.client.debug_upgrade() @@ -252,7 +250,7 @@ def do_some_unit_property_tests(attacker: Unit, defender: Unit): # Spawn units await self.client.debug_create_unit( - [[attacker_type, 1, map_center, 1], [defender_type, 1, map_center, 2]] + [(attacker_type, 1, map_center, 1), (defender_type, 1, map_center, 2)] ) await self._advance_steps(1) @@ -292,8 +290,10 @@ def do_some_unit_property_tests(attacker: Unit, defender: Unit): await self._advance_steps(1) # Unsure why I have to recalculate this here again but it prevents a bug attacker, defender = get_attacker_and_defender() + # pyrefly: ignore expected_damage: float = max(expected_damage, attacker.calculate_damage_vs_target(defender)[0]) real_damage = math.ceil( + # pyrefly: ignore defender.health_max + defender.shield_max - defender.health - defender.shield ) # logger.info( @@ -304,6 +304,7 @@ def do_some_unit_property_tests(attacker: Unit, defender: Unit): f"Step limit reached. Test timed out for attacker {attacker_type} and defender {defender_type}" ) assert expected_damage == real_damage, ( + # pyrefly: ignore f"Expected damage does not match real damage: Unit type {attacker_type} (attack upgrade: {attacker.attack_upgrade_level}) deals {real_damage} damage against {defender_type} (armor upgrade: {defender.armor_upgrade_level} and shield upgrade: {defender.shield_upgrade_level}) but calculated damage was {expected_damage}, attacker weapons: \n{attacker._weapons}" ) @@ -321,7 +322,6 @@ async def on_start(self): await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): - # pyre-ignore[16] map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: diff --git a/test/generate_pickle_files_bot.py b/test/generate_pickle_files_bot.py index 3b4158ee..d8b3296d 100644 --- a/test/generate_pickle_files_bot.py +++ b/test/generate_pickle_files_bot.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] """ This "bot" will loop over several available ladder maps and generate the pickle file in the "/test/pickle_data/" subfolder. These will then be used to run tests from the test script "test_pickled_data.py" @@ -26,7 +25,7 @@ class ExporterBot(BotAI): def __init__(self): BotAI.__init__(self) - self.map_name: str = None + self.map_name: str = None # pyrefly: ignore async def on_step(self, iteration): pass @@ -45,7 +44,7 @@ def get_combat_file_path(self) -> Path: file_path = folder_path / subfolder_name / file_name return file_path - async def store_data_to_file(self, file_path: str): + async def store_data_to_file(self, file_path: Path): # Grab all raw data from observation raw_game_data = await self.client._execute( data=sc_pb.RequestData(ability_id=True, unit_type_id=True, upgrade_id=True, buff_id=True, effect_id=True) @@ -87,10 +86,10 @@ async def on_start(self): } # Create units for self - await self.client.debug_create_unit([[valid_unit, 1, self.start_location, 1] for valid_unit in valid_units]) + await self.client.debug_create_unit([(valid_unit, 1, self.start_location, 1) for valid_unit in valid_units]) # Create units for enemy await self.client.debug_create_unit( - [[valid_unit, 1, self.enemy_start_locations[0], 2] for valid_unit in valid_units] + [(valid_unit, 1, self.enemy_start_locations[0], 2) for valid_unit in valid_units] ) await self._advance_steps(2) diff --git a/test/queries_test_bot.py b/test/queries_test_bot.py index 72c3c7f8..bb029e84 100644 --- a/test/queries_test_bot.py +++ b/test/queries_test_bot.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] """ This testbot's purpose is to test the query behavior of the API. These query functions are: @@ -48,7 +47,7 @@ async def clear_map_center(self): map_center = self.game_info.map_center # Spawn observer to be able to see enemy invisible units - await self.client.debug_create_unit([[UnitTypeId.OBSERVER, 1, map_center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.OBSERVER, 1, map_center, 1)]) await self._advance_steps(10) # Remove everything close to map center @@ -72,7 +71,7 @@ async def spawn_unit(self, unit_type: UnitTypeId | list[UnitTypeId]): if not isinstance(unit_type, list): unit_type = [unit_type] for i in unit_type: - await self.client.debug_create_unit([[i, 1, self.game_info.map_center, 1]]) + await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 1)]) async def spawn_unit_enemy(self, unit_type: UnitTypeId | list[UnitTypeId]): await self._advance_steps(10) @@ -80,9 +79,9 @@ async def spawn_unit_enemy(self, unit_type: UnitTypeId | list[UnitTypeId]): unit_type = [unit_type] for i in unit_type: if i == UnitTypeId.CREEPTUMOR: - await self.client.debug_create_unit([[i, 1, self.game_info.map_center + Point2((5, 5)), 2]]) + await self.client.debug_create_unit([(i, 1, self.game_info.map_center + Point2((5, 5)), 2)]) else: - await self.client.debug_create_unit([[i, 1, self.game_info.map_center, 2]]) + await self.client.debug_create_unit([(i, 1, self.game_info.map_center, 2)]) async def run_can_place(self) -> bool: result = await self.can_place(AbilityId.TERRANBUILD_COMMANDCENTER, [self.game_info.map_center]) @@ -195,7 +194,7 @@ async def test_rally_points_with_rally_ability(self): map_center = self.game_info.map_center barracks_spawn_point = map_center.offset(Point2((10, 10))) await self.client.debug_create_unit( - [[UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1], [UnitTypeId.FACTORY, 2, barracks_spawn_point, 1]] + [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)] ) await self._advance_steps(10) @@ -221,7 +220,7 @@ async def test_rally_points_with_smart_ability(self): map_center = self.game_info.map_center barracks_spawn_point = map_center.offset(Point2((10, 10))) await self.client.debug_create_unit( - [[UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1], [UnitTypeId.FACTORY, 2, barracks_spawn_point, 1]] + [(UnitTypeId.BARRACKS, 2, barracks_spawn_point, 1), (UnitTypeId.FACTORY, 2, barracks_spawn_point, 1)] ) await self._advance_steps(10) diff --git a/test/real_time_worker_production.py b/test/real_time_worker_production.py index d272e0ea..062362ce 100644 --- a/test/real_time_worker_production.py +++ b/test/real_time_worker_production.py @@ -68,16 +68,16 @@ async def on_step(self, iteration): continue if self.enemy_structures.closer_than(10, expansion_location): continue - await self.client.debug_create_unit([[UnitTypeId.NEXUS, 1, expansion_location, 1]]) + await self.client.debug_create_unit([(UnitTypeId.NEXUS, 1, expansion_location, 1)]) logger.info( f"{self.time_formatted} {self.state.game_loop} Spawning a nexus {self.supply_used} / {self.supply_cap}" ) made_nexus = True - break + continue # Spawn new pylon in map center if no more expansions are available if self.supply_left == 0 and not made_nexus: - await self.client.debug_create_unit([[UnitTypeId.PYLON, 1, self.game_info.map_center, 1]]) + await self.client.debug_create_unit([(UnitTypeId.PYLON, 1, self.game_info.map_center, 1)]) # Don't get disturbed during this test if self.enemy_units: @@ -92,7 +92,6 @@ async def on_building_construction_complete(self, unit: Unit): if unit.is_structure: unit(AbilityId.RALLY_WORKERS, self.mineral_field.closest_to(unit)) - # pyre-ignore[11] async def on_end(self, game_result: Result): global on_end_was_called on_end_was_called = True diff --git a/test/run_example_bots_vs_computer.py b/test/run_example_bots_vs_computer.py index 9cb15576..3291c13e 100644 --- a/test/run_example_bots_vs_computer.py +++ b/test/run_example_bots_vs_computer.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] """ This script makes sure to run all bots in the examples folder to check if they can launch. """ @@ -115,10 +114,9 @@ # Run example bots for bot_info in bot_infos: - # pyre-ignore[11] - bot_race: Race = bot_info["race"] - bot_path: str = bot_info["path"] - bot_class_name: str = bot_info["bot_class_name"] + bot_race: Race = bot_info["race"] # pyrefly: ignore + bot_path: str = bot_info["path"] # pyrefly: ignore + bot_class_name: str = bot_info["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class: type[BotAI] = getattr(module, bot_class_name) diff --git a/test/run_example_bots_vs_each_other.py b/test/run_example_bots_vs_each_other.py index b70f3ccb..59aefe14 100644 --- a/test/run_example_bots_vs_each_other.py +++ b/test/run_example_bots_vs_each_other.py @@ -1,4 +1,3 @@ -# pyre-ignore-all-errors[16] """ This script makes sure to run all bots in the examples folder to check if they can launch against each other. """ @@ -96,16 +95,15 @@ # Run bots against each other for bot_info1, bot_info2 in combinations(bot_infos, 2): - # pyre-ignore[11] - bot_race1: Race = bot_info1["race"] - bot_path: str = bot_info1["path"] - bot_class_name: str = bot_info1["bot_class_name"] + bot_race1: Race = bot_info1["race"] # pyrefly: ignore + bot_path: str = bot_info1["path"] # pyrefly: ignore + bot_class_name: str = bot_info1["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class1: type[BotAI] = getattr(module, bot_class_name) - bot_race2: Race = bot_info2["race"] - bot_path: str = bot_info2["path"] - bot_class_name: str = bot_info2["bot_class_name"] + bot_race2: Race = bot_info2["race"] # pyrefly: ignore + bot_path: str = bot_info2["path"] # pyrefly: ignore + bot_class_name: str = bot_info2["bot_class_name"] # pyrefly: ignore module = import_module(bot_path) bot_class2: type[BotAI] = getattr(module, bot_class_name) diff --git a/test/test_expiring_dict.py b/test/test_expiring_dict.py index 9fc6daa3..55324021 100644 --- a/test/test_expiring_dict.py +++ b/test/test_expiring_dict.py @@ -18,7 +18,7 @@ def increment(self, value=1): test_dict = {"hello": "its me mario", "does_this_work": "yes it works", "another_test": "yep this one also worked"} bot = BotAI() - test = ExpiringDict(bot, max_age_frames=10) + test = ExpiringDict(bot, max_age_frames=10) # pyrefly: ignore for key, value in test_dict.items(): test[key] = value @@ -50,8 +50,8 @@ def increment(self, value=1): assert key in test assert test[key] == item, (key, item) assert test.get(key) == item - assert test.get(key, with_age=True)[0] == item - assert test.get(key, with_age=True)[1] in {0, 1} + assert test.get(key, with_age=True)[0] == item # pyrefly: ignore + assert test.get(key, with_age=True)[1] in {0, 1} # pyrefly: ignore c = 0 for _key in test: @@ -65,7 +65,7 @@ def increment(self, value=1): # Update from another dict updater_dict = {"new_key": "my_new_value"} - test.update(updater_dict) + test.update(updater_dict) # pyrefly: ignore assert "does_this_work" in test assert "new_key" in test diff --git a/test/test_pickled_data.py b/test/test_pickled_data.py index 8a6c5e69..cf831c70 100644 --- a/test/test_pickled_data.py +++ b/test/test_pickled_data.py @@ -59,7 +59,7 @@ def build_bot_object_from_pickle_data(raw_game_data, raw_game_info, raw_observat game_info = GameInfo(raw_game_info.game_info) game_state = GameState(raw_observation) bot._initialize_variables() - client = Client(True) + client = Client(True) # pyrefly: ignore bot._prepare_start(client=client, player_id=1, game_info=game_info, game_data=game_data) bot._prepare_step(state=game_state, proto_game_info=raw_game_info) return bot @@ -124,7 +124,7 @@ def test_bot_ai(): assert bot.time == 0 assert bot.time_formatted in {"0:00", "00:00"} assert bot.start_location is None # Is populated by main.py - bot.game_info.player_start_location = bot.townhalls.random.position + bot.game_info.player_start_location = bot.townhalls.random.position # pyrefly: ignore assert bot.townhalls.random.position not in bot.enemy_start_locations assert bot.enemy_units == Units([], bot) assert bot.enemy_structures == Units([], bot) @@ -152,13 +152,13 @@ def test_bot_ai(): # Store old values for minerals, vespene old_values = bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used - bot.vespene = 50 + bot.vespene = 50 # pyrefly: ignore assert bot.can_afford(UpgradeId.WARPGATERESEARCH) assert bot.can_afford(AbilityId.RESEARCH_WARPGATE) - bot.minerals = 150 - bot.supply_cap = 15 - bot.supply_left = -1 - bot.supply_used = 16 + bot.minerals = 150 # pyrefly: ignore + bot.supply_cap = 15 # pyrefly: ignore + bot.supply_left = -1 # pyrefly: ignore + bot.supply_used = 16 # pyrefly: ignore # Confirm that units that don't cost supply can be built while at negative supply using can_afford function assert bot.can_afford(UnitTypeId.GATEWAY) assert bot.can_afford(UnitTypeId.PYLON) @@ -166,6 +166,7 @@ def test_bot_ai(): assert bot.can_afford(UnitTypeId.BANELING) assert not bot.can_afford(UnitTypeId.ZERGLING) assert not bot.can_afford(UnitTypeId.MARINE) + # pyrefly: ignore bot.minerals, bot.vespene, bot.supply_cap, bot.supply_left, bot.supply_used = old_values worker = bot.workers.random @@ -291,13 +292,15 @@ def test_bot_ai(): def calc_cost(item_id) -> Cost: if isinstance(item_id, AbilityId): - # pyre-ignore[16] return bot.game_data.calculate_ability_cost(item_id) elif isinstance(item_id, UpgradeId): return bot.game_data.upgrades[item_id.value].cost elif isinstance(item_id, UnitTypeId): - creation_ability: AbilityId = bot.game_data.units[item_id.value].creation_ability.exact_id - return bot.game_data.calculate_ability_cost(creation_ability) + creation_ability = bot.game_data.units[item_id.value].creation_ability + if creation_ability is None: + return Cost(0, 0) + creation_ability_id = creation_ability.exact_id + return bot.game_data.calculate_ability_cost(creation_ability_id) return Cost(0, 0) def assert_cost(item_id, real_cost: Cost): @@ -490,11 +493,11 @@ def test_pixelmap(): pathing_grid: PixelMap = bot.game_info.pathing_grid assert pathing_grid.bits_per_pixel assert pathing_grid.bytes_per_pixel == pathing_grid.bits_per_pixel // 8 - assert not pathing_grid.is_set(Point2((0, 0))) - assert pathing_grid.is_empty(Point2((0, 0))) + assert not pathing_grid.is_set((0, 0)) + assert pathing_grid.is_empty((0, 0)) pathing_grid[Point2((0, 0))] = 123 - assert pathing_grid.is_set(Point2((0, 0))) - assert not pathing_grid.is_empty(Point2((0, 0))) + assert pathing_grid.is_set((0, 0)) + assert not pathing_grid.is_empty((0, 0)) pathing_grid.flood_fill_all(lambda i: True) pathing_grid.copy() pathing_grid.print() @@ -954,10 +957,12 @@ def test_exact_creation_ability(): UnitTypeId.REFINERYRICH, ]: with test_case.assertRaises(AttributeError): + # pyrefly: ignore _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id continue try: + # pyrefly: ignore _creation_ability = bot.game_data.units[unit_type.value].creation_ability.exact_id except AttributeError: if unit_type not in CREATION_ABILITY_FIX: @@ -974,12 +979,10 @@ def test_dicts(): bot: BotAI = get_map_specific_bot(random.choice(MAPS)) - unit_id: UnitTypeId - data: dict - for unit_id, data in RESEARCH_INFO.items(): + for data in RESEARCH_INFO.values(): upgrade_id: UpgradeId for upgrade_id, upgrade_data in data.items(): - research_ability_correct: AbilityId = upgrade_data["ability"] + research_ability_correct: AbilityId = upgrade_data["ability"] # pyrefly: ignore research_ability_data_from_api = bot.game_data.upgrades[upgrade_id.value].research_ability if research_ability_data_from_api is None: continue @@ -996,12 +999,12 @@ def test_dicts(): @given( - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=500) def test_position_pointlike(x1, y1, x2, y2, x3, y3): @@ -1063,10 +1066,10 @@ def test_position_pointlike(x1, y1, x2, y2, x3, y3): @given( - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=500) def test_position_point2(x1, y1, x2, y2): @@ -1121,9 +1124,9 @@ def test_position_point2(x1, y1, x2, y2): @given( - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=10) def test_position_point3(x1, y1, z1): @@ -1132,7 +1135,16 @@ def test_position_point3(x1, y1, z1): assert pos1.to3 == pos1 -@given(st.integers(min_value=-1e5, max_value=1e5), st.integers(min_value=-1e5, max_value=1e5)) +@given( + st.integers( + min_value=-1e5, # pyrefly: ignore + max_value=1e5, # pyrefly: ignore + ), + st.integers( + min_value=-1e5, # pyrefly: ignore + max_value=1e5, # pyrefly: ignore + ), +) @settings(max_examples=20) def test_position_size(w, h): size = Size((w, h)) @@ -1141,10 +1153,10 @@ def test_position_size(w, h): @given( - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), - st.integers(min_value=-1e5, max_value=1e5), + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore + st.integers(min_value=-1e5, max_value=1e5), # pyrefly: ignore ) @settings(max_examples=20) def test_position_rect(x, y, w, h): diff --git a/test/test_pickled_ramp.py b/test/test_pickled_ramp.py index 0695fe65..33161eff 100644 --- a/test/test_pickled_ramp.py +++ b/test/test_pickled_ramp.py @@ -14,7 +14,6 @@ from loguru import logger from sc2.game_info import Ramp -from sc2.ids.unit_typeid import UnitTypeId from sc2.position import Point2 from sc2.unit import Unit from sc2.units import Units @@ -25,6 +24,7 @@ def pytest_generate_tests(metafunc): idlist = [] argvalues = [] + argnames = [] for scenario in metafunc.cls.scenarios: idlist.append(scenario[0]) items = scenario[1].items() @@ -37,11 +37,11 @@ class TestClass: # Load all pickle files and convert them into bot objects from raw data (game_data, game_info, game_state) scenarios = [(map_path.name, {"map_path": map_path}) for map_path in MAPS] - MAPS_WITH_ODD_EXPANSION_COUNT: set[UnitTypeId] = {"Persephone AIE", "StargazersAIE", "Stasis LE"} + MAPS_WITH_ODD_EXPANSION_COUNT = {"Persephone AIE", "StargazersAIE", "Stasis LE"} def test_main_base_ramp(self, map_path: Path): bot = get_map_specific_bot(map_path) - # pyre-ignore[16] + bot.game_info.map_ramps, bot.game_info.vision_blockers = bot.game_info._find_ramps_and_vision_blockers() # Test if main ramp works for all spawns @@ -107,7 +107,7 @@ def test_bot_ai(self, map_path: Path): ) # On N player maps, it is expected that there are N*X bases because of symmetry, at least for maps designed for 1vs1 # Those maps in the list have an un-even expansion count - # pyre-ignore[16] + expect_even_expansion_count = 1 if bot.game_info.map_name in self.MAPS_WITH_ODD_EXPANSION_COUNT else 0 assert ( len(bot.expansion_locations_list) % (len(bot.enemy_start_locations) + 1) == expect_even_expansion_count @@ -120,7 +120,7 @@ def test_bot_ai(self, map_path: Path): for location in bot.enemy_start_locations: assert location in set(bot.expansion_locations_list), f"{location}, {bot.expansion_locations_list}" # Each expansion is supposed to have at least one geysir and 6-12 minerals - # pyre-ignore[16] + for expansion, resource_positions in bot.expansion_locations_dict.items(): assert isinstance(expansion, Point2) assert isinstance(resource_positions, Units) diff --git a/test/travis_test_script.py b/test/travis_test_script.py index 973141d6..5fde8cb5 100644 --- a/test/travis_test_script.py +++ b/test/travis_test_script.py @@ -49,34 +49,32 @@ # Break as the bot run was successful break - # pyre-ignore[16] - if process.returncode is not None: + if process is not None and process.returncode is not None and result is not None: # Reformat the output into a list - # pyre-ignore[16] - logger.info_output = result + linebreaks = [ - # pyre-ignore[16] - ["\r\n", logger.info_output.count("\r\n")], - ["\r", logger.info_output.count("\r")], - ["\n", logger.info_output.count("\n")], + ("\r\n", result.count("\r\n")), + ("\r", result.count("\r")), + ("\n", result.count("\n")), ] most_linebreaks_type = max(linebreaks, key=lambda x: x[1]) linebreak_type, linebreak_count = most_linebreaks_type - # pyre-ignore[16] - output_as_list = logger.info_output.split(linebreak_type) + + output_as_list = result.split(linebreak_type) logger.info("Travis test script, bot output:\r\n{}\r\nEnd of bot output".format("\r\n".join(output_as_list))) time_taken = time.time() - t0 # Bot was not successfully run in time, returncode will be None - if process.returncode is None or process.returncode != 0: + if process is not None and (process.returncode is None or process.returncode != 0): logger.info( f"Exiting with exit code 5, error: Attempted to launch script {sys.argv[1]} timed out after {time_taken} seconds. Retries completed: {i}" ) sys.exit(5) # process.returncode will always return 0 if the game was run successfully or if there was a python error (in this case it returns as defeat) - logger.info(f"Returncode: {process.returncode}") + if process is not None and process.returncode is not None: + logger.info(f"Returncode: {process.returncode}") logger.info(f"Game took {round(time.time() - t0, 1)} real time seconds") if process is not None and process.returncode == 0: for line in output_as_list: diff --git a/test/upgradestest_bot.py b/test/upgradestest_bot.py index 6267866b..5b0ae987 100644 --- a/test/upgradestest_bot.py +++ b/test/upgradestest_bot.py @@ -102,19 +102,21 @@ async def test_botai_actions1(self): if "TECHLAB" in structure_type.name: continue + # pyrefly: ignore structure_upgrade_types: dict[UpgradeId, dict[str, AbilityId]] = RESEARCH_INFO[structure_type] data: dict[str, AbilityId] for upgrade_id, data in structure_upgrade_types.items(): # Collect data to spawn - research_ability: AbilityId = data.get("ability", None) - requires_power: bool = data.get("requires_power", False) - required_building: UnitTypeId = data.get("required_building", None) + research_ability: AbilityId = data.get("ability", None) # pyrefly: ignore + requires_power: bool = data.get("requires_power", False) # pyrefly: ignore + required_building: UnitTypeId = data.get("required_building", None) # pyrefly: ignore # Prevent linux crash if ( research_ability.value not in self.game_data.abilities or upgrade_id.value not in self.game_data.upgrades or self.game_data.upgrades[upgrade_id.value].research_ability is None + # pyrefly: ignore or self.game_data.upgrades[upgrade_id.value].research_ability.exact_id != research_ability ): logger.info( @@ -130,7 +132,7 @@ async def test_botai_actions1(self): if required_building: spawn_structures.append(required_building) - await self.client.debug_create_unit([[structure, 1, map_center, 1] for structure in spawn_structures]) + await self.client.debug_create_unit([(structure, 1, map_center, 1) for structure in spawn_structures]) logger.info( f"Spawning {structure_type} to research upgrade {upgrade_id} via research ability {research_ability}" ) @@ -152,7 +154,7 @@ async def test_botai_actions1(self): assert self.structures(structure_type), f"Structure {structure_type} has not been spawned in time" # Try to research the upgrade - while 1: + while True: upgrader_structures: Units = self.structures(structure_type) # Upgrade has been researched, break if upgrader_structures: @@ -174,7 +176,6 @@ async def on_start(self): await self.client.debug_kill_unit(self.units) async def on_step(self, iteration: int): - # pyre-ignore[16] map_center = self.game_info.map_center enemies = self.enemy_units | self.enemy_structures if enemies: diff --git a/uv.lock b/uv.lock index e7ee29bc..4c78b3e5 100644 --- a/uv.lock +++ b/uv.lock @@ -304,7 +304,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pyglet", specifier = ">=2.0.20" }, { name = "pylint", specifier = ">=3.3.2" }, - { name = "pyrefly", specifier = ">=0.21.0" }, + { name = "pyrefly", specifier = ">=0.46.3" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.25.0" }, { name = "pytest-benchmark", specifier = ">=5.1.0" }, @@ -2522,18 +2522,18 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/3f/8a30ed93cb027a18080d7e670b2bbf14135b031fe74443eaa850494d9aa8/pyrefly-0.21.0.tar.gz", hash = "sha256:e05a083047dcba25e730c7e0c70b3dc48ba420f17ef73265f169bc95f487a99d", size = 1056016, upload-time = "2025-06-23T17:45:22.033Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/dd/5b1a4a3a713be65e2af02563f8baa70ea69d594d681cb1c36b38319eee90/pyrefly-0.21.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b44e6172421de12fa14d380bcd416fa64d474aec9f1829e6985b18471b1fcee1", size = 5820971, upload-time = "2025-06-23T17:45:04.928Z" }, - { url = "https://files.pythonhosted.org/packages/5b/40/df55322e761b798903c951ad5699585046f9e926e6b3b6686cb0056024a4/pyrefly-0.21.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0d7ea3b86ac3c8680b290389ca3706cbbb046899247a963ac7384c57880e6f2f", size = 5404314, upload-time = "2025-06-23T17:45:07.007Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a5/b3d526bf75ab8708cc85cd0db571af0179b566964fe2c9aae717f3a03090/pyrefly-0.21.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:101ecb84b0442e85a92bcb15a2dfd844f7a6abae8f980661849c71102447443e", size = 5611017, upload-time = "2025-06-23T17:45:08.989Z" }, - { url = "https://files.pythonhosted.org/packages/f6/02/4d4b0ddade7e2980a13e14062770535666a84dfab3293c33e154dfff6ae1/pyrefly-0.21.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b934004ddcdcbe55efeb732d0c0155a781a184e374fec9cd031e819ac1b92eff", size = 6285992, upload-time = "2025-06-23T17:45:11.991Z" }, - { url = "https://files.pythonhosted.org/packages/29/7c/2c3922ee3bdd82a827a893a80aeddf119e38ce8406035a6eda6ae480885e/pyrefly-0.21.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c66446bc5f7e912dab923adf93f636307c834ab53ddd9422e56d101910d960a", size = 6066455, upload-time = "2025-06-23T17:45:13.587Z" }, - { url = "https://files.pythonhosted.org/packages/e1/c8/c8d391f3535046cb79154d4dfcc01b0d022d5af2701627cbe9a518dd50bc/pyrefly-0.21.0-py3-none-win32.whl", hash = "sha256:a4cb8acf2dc831759cb43fa0326e07085cb21da7202212e70d9478bfc40b7a28", size = 5569924, upload-time = "2025-06-23T17:45:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/85/79/58c94192acbc234ff9290c22063bf8f11d65a7b61eb61f9260fa6fb4b1f3/pyrefly-0.21.0-py3-none-win_amd64.whl", hash = "sha256:765d19f2b48d5dd3dae0752676e2d6e388025fa6337947031ff628160b8cf568", size = 5946646, upload-time = "2025-06-23T17:45:17.433Z" }, - { url = "https://files.pythonhosted.org/packages/75/6e/d476584e93e3c63609dde26a57bc23f732c22b0e9bd17462282268aec758/pyrefly-0.21.0-py3-none-win_arm64.whl", hash = "sha256:a3fc4fffb625a5610b68fc3bf07e4d33b5b1c279512e0251b3d3d4f7c3ed1541", size = 5595515, upload-time = "2025-06-23T17:45:19.4Z" }, +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/c2/f92ab45699f4e2ce2291fd81ec183a3f96ec6c72a1d03056644fdf4aa702/pyrefly-0.46.3.tar.gz", hash = "sha256:6aeb90698b587bba38ec870a515cf3499756fc81d73852fd11eaa10abda0fea6", size = 4769402, upload-time = "2025-12-30T23:58:16.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/ae/8257e96b2e6396880280e96b1ccda0242f19bbedbcc933443d6f56f81843/pyrefly-0.46.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5626d345aa829dc17311c77c8019dfc7b9b6dd8da102b66d943d47862af2be59", size = 11662076, upload-time = "2025-12-30T23:57:55.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/aba0dd3b0f9cb50fb3b39992960c1e04ae3498346c448ae041bff0d26337/pyrefly-0.46.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:13b553090ec9821a781198389564bcb3cb020679189272b1e88fddba9d613879", size = 11269927, upload-time = "2025-12-30T23:57:58.362Z" }, + { url = "https://files.pythonhosted.org/packages/17/82/2a3ad9107229893207979d25b5eb98d8b71d3f43b28f349286b3b6630514/pyrefly-0.46.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ad8612c2b8eaa36f3953ab3be8635bd91fd8179ddbdd9eb8bb4f1ad513ff3f0", size = 31501305, upload-time = "2025-12-30T23:58:00.998Z" }, + { url = "https://files.pythonhosted.org/packages/11/10/b5f2c1eea63bee42f45480a17a72c31ca60b6c15024f1085459f4cd5d638/pyrefly-0.46.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:873d0c6a71cdd0399e1c6de1a3edbab34c7a9ae48e7a9e9ac327b1010d8686a1", size = 33721653, upload-time = "2025-12-30T23:58:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/fe81b2c7e9e7edfe5ea9db00c79439e31c7110faad021ab451734722525e/pyrefly-0.46.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd55f0d864ead658607dc3be6649019cfb5d1c72ceb0818b4b3501e0eb81472", size = 34766036, upload-time = "2025-12-30T23:58:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c7/eff2558569533a11d1a21d73a366a85483bd38d80a12af1c7381218a6b62/pyrefly-0.46.3-py3-none-win32.whl", hash = "sha256:28e11eca9461fc892b19bdad799e280ab81cc00f5a712f89f7bc6b2e41a70194", size = 10738486, upload-time = "2025-12-30T23:58:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/be/84/8f545321cc0bb555992d4e4af0905dc907ef3e9e864d68c5504a46560bc6/pyrefly-0.46.3-py3-none-win_amd64.whl", hash = "sha256:1eb510fa62960bc30137e39360ef68b7c691eb28a6c958560088bc99595d63be", size = 11426143, upload-time = "2025-12-30T23:58:11.546Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1d/5f9b3f6eba2a90a7859dc21905248f78f23d2bdd9e13cec791acb544a4b4/pyrefly-0.46.3-py3-none-win_arm64.whl", hash = "sha256:bb5db31d7781edc3a590fec380d50832faecfc57171c054512009c8254d5ad0f", size = 10977207, upload-time = "2025-12-30T23:58:13.812Z" }, ] [[package]]