Skip to content

Commit

Permalink
Merge pull request #99 from AresSC2/feat/production-controller
Browse files Browse the repository at this point in the history
feat: production controller
  • Loading branch information
raspersc2 authored Dec 9, 2023
2 parents 2da5502 + 84b2d04 commit 2a601a2
Show file tree
Hide file tree
Showing 16 changed files with 763 additions and 129 deletions.
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,141 @@ avoiding preconceived choices out of the box. In fact when initiating a project
resembles starting with a blank `python-sc2` bot! You can write standard `python-sc2` logic and call upon
`ares` functionality as required.

## Features

- Calculated production formation for every expansion location on game start for Terran and Protoss,
use convenience behavior `BuildStructure` for easy usage.
```python
from ares.behaviors.macro import BuildStructure
from sc2.ids.unit_typeid import UnitTypeId

self.register_behavior(
BuildStructure(
base_location=self.start_location,
structure_id=UnitTypeId.BARRACKS
)
)
```
![formation](https://github.com/raspersc2/oops/assets/63355562/946686eb-cc75-4271-ae1e-3b9f5c424e47)

- Curate custom combat maneuvers with our plug and play behavior system. Mix and match your own
behaviors to truly create some unique play styles!
```python
from ares import AresBot
from ares.behaviors.combat import CombatManeuver
from ares.behaviors.combat.individual import (
DropCargo,
KeepUnitSafe,
PathUnitToTarget,
PickUpCargo,
)
from sc2.unit import Unit
from sc2.units import Units
import numpy as np

class MyBot(AresBot):
async def on_step(self, iteration: int) -> None:
# retrieve medivac and mines_to_pickup and pass to method
# left out here for clarity
# mines would require their own behavior
self.do_medivac_mine_drop(medivac, mines_to_pickup)

def do_medivac_mine_drop(
self,
medivac: Unit,
mines_to_pickup: Units
) -> None:
# initialize a new CombatManeuver
mine_drop: CombatManeuver = CombatManeuver()
# get a grid for the medivac to path on
air_grid: np.ndarray = self.mediator.get_air_grid
# first priority is picking up units
mine_drop.add(
PickUpCargo(
unit=medivac,
grid=air_grid,
pickup_targets=mines_to_pickup)
)
# if there is cargo, path to target and drop them off
if medivac.has_cargo:
# path
mine_drop.add(
PathUnitToTarget(
unit=medivac,
grid=air_grid,
target=self.enemy_start_locations[0],
)
)
# drop off the mines
mine_drop.add(
DropCargo(unit=medivac, target=medivac.position)
)
# no cargo and no units to pick up, stay safe
else:
mine_drop.add(
KeepUnitSafe(unit=medivac, grid=air_grid)
)

# finally register this maneuver to be executed
self.register_behavior(mine_drop)
```
- Convenient production management via `SpawnController` and `ProductionController` behaviors.
- Fast `cython` alternatives to some common functionality, see [the docs](https://aressc2.github.io/ares-sc2/api_reference/cython_extensions/index.html)
- [MapAnalyzer](https://github.com/spudde123/SC2MapAnalysis) library available and used throughout `ares-sc2`,
access the library yourself via `self.mediator.get_map_data_object`

- [CombatSim](https://github.com/danielvschoor/sc2-helper) helper method available via `self.mediator.can_win_fight`
- Opt in Build runner system, easily curate new builds via a yml config file.

- Use `KDTree` for fast distance checks on batches of units, example:
```python
from ares.consts import UnitTreeQueryType

from sc2.ids.unit_typeid import UnitTypeId
from sc2.unit import Unit
from sc2.units import Units

reapers: list[Unit] = self.mediator.get_own_units_dict[UnitTypeId.REAPER]
all_ground_near_reapers: dict[int, Units] = self.mediator.get_units_in_range(
start_points=reapers,
distances=15,
query_tree=UnitTreeQueryType.EnemyGround,
return_as_dict=True,
)

for reaper in reapers:
near_ground: Units = all_ground_near_reapers[reaper.tag]
```

- `ares-sc2` works quietly behind the scenes, yet at any moment access to a wealth of information
is available via the `mediator`, some examples:

Retrieve a ground pathing grid already containing enemy influence:

`grid: np.ndarray = self.mediator.get_ground_grid`

Use this grid to make a pathing call:

`move_to: Point2 = self.mediator.find_path_next_point(start=unit.position, target=self.target, grid=grid)`

Optimally select a worker and assign a new `UnitRole`:
```python
from ares.consts import UnitRole
from sc2.ids.unit_typeid import UnitTypeId
from sc2.units import Units

if worker := self.mediator.select_worker(target_position=self.main_base_ramp.top_center):
self.mediator.assign_role(tag=worker.tag, role=UnitRole.DEFENDING)

# retrieve `UnitRole.DEFENDING` workers
defending_workers: Units = self.mediator.get_units_from_role(
role=UnitRole.DEFENDING, unit_type=UnitTypeId.SCV
)
```

See [Manager Mediator docs](https://aressc2.github.io/ares-sc2/api_reference/manager_mediator.html) for all
available methods.

## Setting up `ares-sc2`

To setup a full development environment:
Expand Down
5 changes: 5 additions & 0 deletions docs/api_reference/behaviors/combat_behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
show_root_heading: false
show_root_toc_entry: false

::: ares.behaviors.combat.individual.place_predictive_aoe
options:
show_root_heading: false
show_root_toc_entry: false

::: ares.behaviors.combat.individual.shoot_target_in_range
options:
show_root_heading: false
Expand Down
5 changes: 5 additions & 0 deletions docs/api_reference/behaviors/macro_behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class MyBot(AresBot):
show_root_heading: false
show_root_toc_entry: false

::: ares.behaviors.macro.production_controller
options:
show_root_heading: false
show_root_toc_entry: false

::: ares.behaviors.macro.spawn_controller
options:
show_root_heading: false
Expand Down
5 changes: 5 additions & 0 deletions docs/api_reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ Many of these functions exhibit multiple-fold speed enhancements compared to the
many of which serve as alternatives to `python-sc2` functions.
- `Manager mediator` - Seamlessly orchestrating numerous managers in the background, the mediator serves
as the recommended approach to access data and functions within these managers.

Convenient methods globally available:
::: ares.main
options:
show_root_heading: false
6 changes: 5 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ resembles starting with a blank `python-sc2` bot! You can write standard `python
## Bots made with `ares-sc2`
Feel free to add your own bot here

- [Phobos (T)](https://github.com/AresSC2/phobos)
[//]: # ( - [Phobos (T)](https://github.com/AresSC2/phobos))
- [Aristaeus (P)](https://github.com/august-k/Aristaeus)
- [QueenBot (Z)](https://github.com/AresSC2/QueenBot)
- [oops (R) micro ladder bot](https://github.com/raspersc2/oops)

## Features

- Highly customizable and extendable behavior system. Curate custom combat maneuvers and macro plans.
- `ManagerMediator` to facilitate communication and retrieve information from managers in `ares`,
see [docs here](./api_reference/manager_mediator.html)
- Manage production by declaring army compositions, less repetitive boilerplate code in your bot. See
[tutorial here](./tutorials/managing_production.html)
- Memory units tracking by default. Track units that have recently entered fog of war.
- Pre-calculated building formation for all maps and bases (Terran and Protoss only).
- [MapAnalyzer](https://github.com/spudde123/SC2MapAnalysis/tree/develop) library available
Expand Down
75 changes: 75 additions & 0 deletions docs/tutorials/managing_production.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

Dealing with production logic for a bot often means wrestling with the same old repetitive code headaches.
Imagine starting off with a bot geared towards a stalker and immortal combo, finely tuning your logic around
that. Now, say you want to shake things up and switch to a late-game skytoss setup – suddenly, you're
stuck adding more code for production facilities, tech structures, and handling those late-game units.

We get it, this kind of redundancy is a pain. We didn't just
want to make transitioning between army compositions smoother; we wanted to kick out all that extra,
unnecessary code clutter. Whether you're rocking a stalker and immortal crew or going all-in on a late-game
skytoss spectacle, our approach offers a more straightforward and adaptable development process.

Furthermore, this system is split into two main behaviors, the `SpawnController` to manage army production,
and `ProductionController` to add production and tech up. Depending on your needs you can use one, both or
none at all!

## Defining army compositions
Both controllers rely on an army composition dictionary, for example:
```python
from sc2.ids.unit_typeid import UnitTypeId as UnitID

@property
def viking_tank(self) -> dict:
return {
UnitID.MARINE: {"proportion": 0.69, "priority": 4},
UnitID.SIEGETANK: {"proportion": 0.13, "priority": 0},
UnitID.VIKINGFIGHTER: {"proportion": 0.16, "priority": 3},
UnitID.RAVEN: {"proportion": 0.02, "priority": 1},
}

```
Things to note:

- The `proportion` values should add up to 1.0 (0.69 + 0.13 + 0.16 + 0.02 = 1.0)
- Each declared unit should be given a priority, where 0 is the highest. This allows resources
to be saved for important units.

## Running the controllers
Let's create a simple bot, demonstrating how to run these controllers.
```python
from ares import AresBot
from ares.behaviors.macro import ProductionController, SpawnController

from sc2.ids.unit_typeid import UnitTypeId as UnitID

class TestBot(AresBot):

@property
def viking_tank(self) -> dict:
return {
UnitID.MARINE: {"proportion": 0.69, "priority": 4},
UnitID.SIEGETANK: {"proportion": 0.13, "priority": 0},
UnitID.VIKINGFIGHTER: {"proportion": 0.16, "priority": 3},
UnitID.RAVEN: {"proportion": 0.02, "priority": 1},
}

async def on_step(self, iteration: int) -> None:
await super(TestBot, self).on_step(iteration)

production_location = self.start_location

# production controller
self.register_behavior(
ProductionController(self.viking_tank, production_location)
)

# spawn controller
self.register_behavior(
SpawnController(self.viking_tank)
)
```

These behaviors can be further customized through arguments,
please refer to the [API docs](../api_reference/behaviors/macro_behaviors.html)


1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ nav:
- Creating Custom Behaviors: tutorials/custom_behaviors.md
- Config File: tutorials/config_file.md
- Installation: tutorials/installation.md
- Managing Production: tutorials/managing_production.md
- Tips and Tricks: tutorials/tips_and_tricks.md
- API Reference:
- api_reference/index.md
Expand Down
3 changes: 1 addition & 2 deletions src/ares/behaviors/combat/individual/place_predictive_aoe.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
class PlacePredictiveAoE(CombatBehavior):
"""Predict an enemy position and fire AoE accordingly.
Warning: Use this at your own risk. It works but making is a
work in progress.
Warning: Use this at your own risk. Work in progress.
TODO: Guess where the enemy is going based on how it's been moving.
Cythonize this.
Expand Down
2 changes: 2 additions & 0 deletions src/ares/behaviors/macro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
from ares.behaviors.macro.auto_supply import AutoSupply
from ares.behaviors.macro.build_structure import BuildStructure
from ares.behaviors.macro.macro_behavior import MacroBehavior
from ares.behaviors.macro.macro_plan import MacroPlan
from ares.behaviors.macro.mining import Mining
from ares.behaviors.macro.production_controller import ProductionController
from ares.behaviors.macro.spawn_controller import SpawnController
10 changes: 8 additions & 2 deletions src/ares/behaviors/macro/mining.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> boo
continue

# do we have record of this worker? If so mine from the relevant resource
if assigned_mineral_patch or assigned_gas_building:
if ai.townhalls and (assigned_mineral_patch or assigned_gas_building):
# we are far away, path to min field to avoid enemies
if dist_to_resource > 6.0 and not worker.is_carrying_resource:
worker.move(
Expand Down Expand Up @@ -201,7 +201,7 @@ def execute(self, ai: "AresBot", config: dict, mediator: ManagerMediator) -> boo
self._do_standard_mining(ai, worker, resource)

# nowhere for this worker to go, long distance mining
elif self.long_distance_mine and ai.minerals:
elif self.long_distance_mine and ai.minerals and ai.townhalls:
self._long_distance_mining(
ai,
mediator,
Expand Down Expand Up @@ -456,6 +456,12 @@ def _do_mining_boost(
cy_towards(target_position, worker_position, TOWNHALL_TARGET)
)

if not target:
ai.mediator.remove_mineral_field(mineral_field_tag=target.tag)
ai.mediator.remove_worker_from_mineral(worker_tag=worker.tag)
elif not target.is_mineral_field and not target.vespene_contents:
ai.mediator.remove_gas_building(gas_building_tag=target.tag)

closest_th: Unit = cy_closest_to(worker_position, ai.townhalls)

# fix realtime bug where worker is stuck with a move command but already
Expand Down
Loading

0 comments on commit 2a601a2

Please sign in to comment.