Skip to content

[WIP] Build order support #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 82 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
824bdef
Add inline condition to prevent converting of 'None' to string for no…
Jan 26, 2018
9ea30f7
Merge branch 'master' into playable
Jan 26, 2018
cf0483c
Initial implementation of build order support
Jan 26, 2018
d6e168d
Merge branch 'master' into build-order-support
reypader Jan 26, 2018
c8c0d5e
Move build order related files to its own submodule
reypader Jan 26, 2018
5c80134
Move can_afford conditions into the actions of the build order
reypader Jan 26, 2018
96ff76e
Add `expand` build order action
reypader Jan 26, 2018
4ba7ac6
Wrap cwd expression in parentheses
reypader Jan 26, 2018
13e8bcd
Merge remote-tracking branch 'remotes/upstream/master'
reypader Jan 26, 2018
f45d613
Add return resource command to units
reypader Jan 26, 2018
cb6ff95
Initialize geysers for each step
reypader Jan 26, 2018
9096d22
Conver `expansion_locations` to a dict of Point2D: Mineral Fields
reypader Jan 26, 2018
2fd008b
Implement distribution of workers
reypader Jan 26, 2018
79655e0
Add sample script to showcase worker distribution in action
reypader Jan 26, 2018
2ecb34b
Change `BotAI.already_pending` to return integers to allow counting
reypader Jan 27, 2018
f01042e
Add support for morhping instead of training for Zerg
reypader Jan 27, 2018
e68e686
rename unit_count arguments
reypader Jan 27, 2018
63505df
Introduce Intent class and make worker creation a parameter
reypader Jan 27, 2018
156fcad
Allow build order actions to be executed indefinitely as long as it's…
reypader Jan 27, 2018
f69a91d
Add supply, geyser, townhall, and worker type properties to BotAI
Jan 28, 2018
3c1beec
Move state conditions into separate submodule
Jan 28, 2018
c05f930
Add auto-add-supply feature to build order
Jan 28, 2018
78cf389
Partial fix for #8 - already_pending returns count instead of boolean
Jan 28, 2018
a919b05
Move commands into a different submodule for clear separation.
Jan 28, 2018
a34faa9
Wrap the response of `BotAI.can_afford` to return a wrapper
Jan 28, 2018
c81dffe
Add `action_result` property to CanAffordWrapper
Jan 28, 2018
95c3f62
Merge branch 'worker-distribution' into build-order-support
Jan 28, 2018
b0d7a64
Merge branch 'can_afford-wrapper' into build-order-support
Jan 28, 2018
0aadf21
Convert get_owned_expansions to owned_expansions property
Jan 28, 2018
ea1545d
Merge branch 'worker-distribution' into build-order-support
Jan 28, 2018
28850b2
Add support for rich mineral fields
Jan 28, 2018
e45bea9
Merge branch 'worker-distribution' into build-order-support
Jan 28, 2018
e0e89c9
Utilize `can_afford` wrapper
Jan 28, 2018
bd5a716
Allow prioritization of build order actions
Jan 28, 2018
5a59654
Adjust priority of build commands in build orders
Jan 28, 2018
ffb3ea8
Assign idle workers to gather before distribution
Jan 28, 2018
3fc94f6
Merge branch 'worker-distribution' into build-order-support
Jan 28, 2018
4da9af8
Optimize mineral field selection during transfer
Jan 28, 2018
1c29052
Merge branch 'worker-distribution' into build-order-support
Jan 28, 2018
63cf2af
Add unit count conditions
Jan 28, 2018
329ce67
Tweak sample scripts
Jan 28, 2018
495ace3
Queue gather command when transferring workers with resource
Jan 28, 2018
85c0581
Merge branch 'worker-distribution' into build-order-support
Jan 29, 2018
a519808
Add terran build order sample - marine rush
Jan 29, 2018
c9e8f01
Make auto-adding supply to be done when there's at least 1 supply lef…
Jan 30, 2018
e37848c
Rename commands to avoid confusion with core SC2 methods
Jan 30, 2018
e5518ac
Use the installed StarCraft 2 stableid.json for ids
Jan 30, 2018
8f705ab
Update header in id files
Jan 30, 2018
b2778d1
Use abilities with index 0
Jan 30, 2018
8be993c
Add support for getting the available abilities of a unit
Jan 30, 2018
1ac55cd
Merge branch 'can-cast-support' into build-order-support
Jan 30, 2018
e988a5f
Merge branch 'use-stableid-json' into build-order-support
Jan 30, 2018
983e52c
remove PyCharm files
Jan 30, 2018
2324062
Manually add SMART ability
Jan 30, 2018
5a64a3d
Convert usage of enums to new enum names
Jan 30, 2018
6eb9c0e
Merge branch 'use-stableid-json' into build-order-support
Jan 30, 2018
fe048c1
Tweak naming extraction to use friendly name whenever possible
Jan 30, 2018
9f0407e
Revert to using globals in examples
Jan 31, 2018
7c4f654
Adjust variable naming in zerg_rush.py
Jan 31, 2018
bea5073
Revert to using globals but add assertion for duplicate enums
Jan 31, 2018
41eaed4
Revert to some global usage in examples
Jan 31, 2018
dd1a85a
Merge remote-tracking branch 'upstream/master' into worker-distribution
Jan 31, 2018
910d2e9
Remove usage of state parameter in sample
Jan 31, 2018
e3159cc
Merge remote-tracking branch 'upstream/master' into can-cast-support
Jan 31, 2018
9471626
Merge remote-tracking branch 'upstream/master' into use-stableid-json
Jan 31, 2018
f199af1
Merge branch 'master' of github.com:Dentosal/python-sc2
Feb 1, 2018
071be7f
Merge branch 'can-cast-support'
Feb 1, 2018
0d90df9
Merge branch 'already-pending-enhancement'
Feb 1, 2018
e98cd55
Merge branch 'worker-distribution'
Feb 1, 2018
ebcf1ff
Merge branch 'use-stableid-json'
Feb 1, 2018
ad9d314
Ignore pycharm files
Feb 1, 2018
1620f16
Merge branch 'master' into build-order-support
Feb 1, 2018
9c60adc
Fix state parameters being passed
Feb 1, 2018
902b877
Update example for chronoboost cast
Feb 1, 2018
a8ceab2
Fix enum names changed from new ID generation script
Feb 1, 2018
d3d4ec9
Implement warpgate support using find_placement and available_abilities
Feb 1, 2018
e75cb65
Merge branch 'warpgate-support' into build-order-support
Feb 1, 2018
c0ba90f
Merge remote-tracking branch 'upstream/master' into use-stableid-json
Feb 1, 2018
32b422a
Rever BotAI find placement default step to 2
Feb 1, 2018
1726531
Remove local path in generated files
Feb 1, 2018
ae55f67
Merge branch 'use-stableid-json' into build-order-support
Feb 1, 2018
c5f2b26
Merge branch 'master' of github.com:Dentosal/python-sc2 into build-or…
Feb 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ maps/
mini_games/

*.SC2Replay

.idea
*.iml
64 changes: 64 additions & 0 deletions examples/terran_marine_rush_build_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import sc2
from sc2 import run_game, maps, Race, Difficulty
from sc2.build_orders.build_order import BuildOrder, train_unit
from sc2.build_orders.commands import construct, expand, add_supply
from sc2.constants import *
from sc2.player import Bot, Computer
from sc2.state_conditions.conditions import all_of, supply_at_least, minerals_at_least, unit_count


def first_barracks(bot):
return bot.units(UnitTypeId.BARRACKS).first


class TerranBuildOrderBot(sc2.BotAI):
def __init__(self):
build_order = [
(supply_at_least(13), add_supply(prioritize=True)),
(supply_at_least(15), construct(UnitTypeId.BARRACKS, prioritize=True)),
(supply_at_least(16), construct(UnitTypeId.BARRACKS, prioritize=True)),
(supply_at_least(16), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(17), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(18), construct(UnitTypeId.BARRACKS, prioritize=True)),
(supply_at_least(18), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(19), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(20), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(21), add_supply(prioritize=True)),
(supply_at_least(21), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(22), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(23), construct(UnitTypeId.BARRACKS, prioritize=True)),
(supply_at_least(23), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(24), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(25), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(26), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS)),
(supply_at_least(27), construct(UnitTypeId.BARRACKS, prioritize=True)),
(all_of(supply_at_least(27), unit_count(UnitTypeId.BARRACKS, 5, include_pending=True)), add_supply(prioritize=True)),
(unit_count(UnitTypeId.BARRACKS, 5), train_unit(UnitTypeId.MARINE, on_building=UnitTypeId.BARRACKS, repeatable=True))
]
self.attack = False
self.build_order = BuildOrder(self, build_order, worker_count=16)

async def on_step(self, iteration):
await self.distribute_workers()
await self.build_order.execute_build()

if self.units(UnitTypeId.MARINE).amount >= 15 or self.attack:
self.attack = True
for unit in self.units(UnitTypeId.MARINE).idle:
await self.do(unit.attack(self.enemy_start_locations[0]))
if self.known_enemy_structures.exists:
enemy = self.known_enemy_structures.first
await self.do(unit.attack(enemy.position.to2, queue=True))
return


def main():
run_game(maps.get("Abyssal Reef LE"), [
Bot(Race.Terran, TerranBuildOrderBot()),
# Bot(Race.Terran, TerranBuildOrderBot()),
Computer(Race.Random, Difficulty.Harder)
], realtime=False)

if __name__ == '__main__':
main()

2 changes: 1 addition & 1 deletion examples/threebase_voidray.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sc2.player import Bot, Computer

class ThreebaseVoidrayBot(sc2.BotAI):
def select_target(self, state):
def select_target(self):
if self.known_enemy_structures.exists:
return random.choice(self.known_enemy_structures)

Expand Down
72 changes: 72 additions & 0 deletions examples/zerg_rush_build_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import sc2
from sc2 import Race, Difficulty
from sc2.build_orders.build_order import train_unit, BuildOrder, morph
from sc2.build_orders.commands import add_gas, construct, expand, add_supply
from sc2.constants import *
from sc2.player import Bot, Computer
from sc2.state_conditions.conditions import supply_at_least, all_of, unit_count, gas_less_than, unit_count_less_than, \
unit_count_at_least


def research(building, upgrade):
async def research_spec(bot):
sp = bot.units(building).ready
if sp.exists and bot.can_afford(upgrade) and not bot.already_pending(upgrade):
await bot.do(sp.first(upgrade))

return research_spec

class ZergRushBot(sc2.BotAI):

def __init__(self):
self.attack = False
self.mboost_started = False
build_order = [
(all_of(supply_at_least(13), unit_count(UnitTypeId.OVERLORD, 1, include_pending=True)), add_supply(prioritize=True)),
(all_of(supply_at_least(17), unit_count(UnitTypeId.EXTRACTOR, 0, include_pending=True)), add_gas()),
(all_of(supply_at_least(17), unit_count(UnitTypeId.SPAWNINGPOOL, 0, include_pending=True)), construct(UnitTypeId.SPAWNINGPOOL)),
(all_of(supply_at_least(17), unit_count(UnitTypeId.HATCHERY, 1, include_pending=True)), expand()),
(supply_at_least(18), morph(UnitTypeId.ZERGLING)),
(supply_at_least(19), train_unit(UnitTypeId.QUEEN, on_building=UnitTypeId.HATCHERY, prioritize=True)),
(all_of(supply_at_least(21), unit_count(UnitTypeId.OVERLORD, 2, include_pending=True)), add_supply(prioritize=True)),
(all_of(supply_at_least(21), unit_count(UnitTypeId.ROACHWARREN, 0, include_pending=True)), construct(UnitTypeId.ROACHWARREN)),
(all_of(supply_at_least(20), unit_count(UnitTypeId.OVERLORD, 3, include_pending=True)), add_supply(prioritize=True)),
(unit_count_at_least(UnitTypeId.ROACH, 7), morph(UnitTypeId.ZERGLING, repeatable=True)),
(unit_count(UnitTypeId.ROACHWARREN, 1), morph(UnitTypeId.ROACH, repeatable=True))
]

self.build_order = BuildOrder(self, build_order, worker_count=35)

async def on_step(self, iteration):
await self.distribute_workers()

if self.vespene >= 100:
sp = self.units(UnitTypeId.SPAWNINGPOOL).ready
if sp.exists and self.minerals >= 100 and not self.mboost_started:
await self.do(sp.first(AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST))
self.mboost_started = True

await self.build_order.execute_build()

for queen in self.units(UnitTypeId.QUEEN).idle:
if queen.energy >= 25: # Hard coded, since this is not (yet) available
hatchery = self.townhalls.closest_to(queen.position.to2)
await self.do(queen(AbilityId.EFFECT_INJECTLARVA, hatchery))

if (self.units(UnitTypeId.ROACH).amount >= 7 and self.units(UnitTypeId.ZERGLING).amount >= 10) or self.attack:
self.attack = True
for unit in self.units(UnitTypeId.ZERGLING).ready.idle | self.units(UnitTypeId.ROACH).ready.idle:
await self.do(unit.attack(self.enemy_start_locations[0]))
if self.known_enemy_structures.exists:
enemy = self.known_enemy_structures.first
await self.do(unit.attack(enemy.position.to2, queue=True))
return

def main():
sc2.run_game(sc2.maps.get("Abyssal Reef LE"), [
Bot(Race.Zerg, ZergRushBot()),
Computer(Race.Random, Difficulty.Hard)
], realtime=False)

if __name__ == '__main__':
main()
12 changes: 9 additions & 3 deletions sc2/bot_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from .constants import EGG

from .position import Point2, Point3
from .data import Race, ActionResult, Attribute, race_worker, race_townhalls, race_gas
from .data import Race, ActionResult, Attribute, race_worker, race_townhalls, race_gas, race_supply, \
race_basic_townhalls
from .unit import Unit
from .cache import property_cache_forever
from .game_data import AbilityData, Cost
Expand All @@ -28,6 +29,11 @@ def _prepare_start(self, client, player_id, game_info, game_data):
self.player_id = player_id
self.race = Race(self._game_info.player_races[self.player_id])

self.worker_type = race_worker[self.race]
self.basic_townhall_type = race_basic_townhalls[self.race]
self.geyser_type = race_gas[self.race]
self.supply_type = race_supply[self.race]

@property
def game_info(self):
return self._game_info
Expand Down Expand Up @@ -79,7 +85,7 @@ async def get_available_abilities(self, unit):

async def expand_now(self, building=None, max_distance=10, location=None):
if not building:
building = self.townhalls.first.type_id
building = self.basic_townhall_type

assert isinstance(building, UnitTypeId)

Expand Down Expand Up @@ -282,7 +288,7 @@ async def chat_send(self, message):
def _prepare_step(self, state):
self.state = state
self.units = state.units.owned
self.workers = self.units(race_worker[self.race])
self.workers = self.units(self.worker_type)
self.townhalls = self.units(race_townhalls[self.race])
self.geysers = self.units(race_gas[self.race])

Expand Down
Empty file added sc2/build_orders/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions sc2/build_orders/build_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from sc2 import Race, race_worker, ActionResult, race_townhalls
from sc2.build_orders.commands import add_supply, morph, train_unit
from sc2.state_conditions.conditions import always_true


class BuildOrder(object):
def __init__(self, bot, build, worker_count=0, auto_add_supply=True):
self.build = build
self.bot = bot
self.worker_count = worker_count
self.auto_add_supply = auto_add_supply

async def execute_build(self):
bot = self.bot
if bot.supply_left <= ((bot.supply_cap+50) / 50) and not bot.already_pending(bot.supply_type) \
and self.auto_add_supply:
return await add_supply().execute(bot)

for index, item in enumerate(self.build):
condition, command = item
condition = item[0] if item[0] else always_true
if condition(bot) and not command.is_done:
e = await command.execute(bot)
if command.is_done:
return e
else:
# Save up to be able to do this command and hold worker creation.
if command.is_priority and e == ActionResult.NotEnoughMinerals:
return e

if e == ActionResult.NotEnoughFood and self.auto_add_supply \
and not bot.already_pending(bot.supply_type):
return await add_supply().execute(bot)
continue

if bot.workers.amount < self.worker_count:
if bot.race == Race.Zerg:
return await morph(race_worker[Race.Zerg]).execute(bot)
else:
return await train_unit(race_worker[bot.race], race_townhalls[self.bot.race]).execute(bot)
return None
122 changes: 122 additions & 0 deletions sc2/build_orders/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from sc2 import ActionResult, Race
from sc2.constants import *


class Command(object):
def __init__(self, action, repeatable=False, priority=False):
self.action = action
self.is_done = False
self.is_repeatable = repeatable
self.is_priority = priority

async def execute(self, bot):
e = await self.action(bot)
if not e and not self.is_repeatable:
self.is_done = True

return e

def allow_repeat(self):
self.is_repeatable = True
self.is_done = False
return self


def expand(prioritize=False, repeatable=True):
async def do_expand(bot):
building = bot.basic_townhall_type
can_afford = bot.can_afford(building)
if can_afford:
return await bot.expand_now(building=building)
else:
return can_afford.action_result

return Command(do_expand, priority=prioritize, repeatable=repeatable)


def train_unit(unit, on_building, prioritize=False, repeatable=False):
async def do_train(bot):
buildings = bot.units(on_building).ready.noqueue
if buildings.exists:
selected = buildings.first
can_afford = bot.can_afford(unit)
if can_afford:
print("Training {}".format(unit))
return await bot.do(selected.train(unit))
else:
return can_afford.action_result
else:
return ActionResult.Error

return Command(do_train, priority=prioritize, repeatable=repeatable)


def morph(unit, prioritize=False, repeatable=False):
async def do_morph(bot):
larvae = bot.units(UnitTypeId.LARVA)
if larvae.exists:
selected = larvae.first
can_afford = bot.can_afford(unit)
if can_afford:
print("Morph {}".format(unit))
return await bot.do(selected.train(unit))
else:
return can_afford.action_result
else:
return ActionResult.Error

return Command(do_morph, priority=prioritize, repeatable=repeatable)


def construct(building, placement=None, prioritize=True, repeatable=False):
async def do_build(bot):

if not placement:
location = bot.townhalls.first.position.towards(bot.game_info.map_center, 5)
else:
location = placement

can_afford = bot.can_afford(building)
if can_afford:
print("Building {}".format(building))
return await bot.build(building, near=location)
else:
return can_afford.action_result

return Command(do_build, priority=prioritize, repeatable=repeatable)


def add_supply(prioritize=True, repeatable=False):
async def supply_spec(bot):
can_afford = bot.can_afford(bot.supply_type)
if can_afford:
if bot.race == Race.Zerg:
return await morph(bot.supply_type).execute(bot)
else:
return await construct(bot.supply_type).execute(bot)
else:
return can_afford.action_result

return Command(supply_spec, priority=prioritize, repeatable=repeatable)


def add_gas(prioritize=True, repeatable=False):
async def do_add_gas(bot):
can_afford = bot.can_afford(bot.geyser_type)
if not can_afford:
return can_afford.action_result

owned_expansions = bot.owned_expansions
for location, th in owned_expansions.items():
vgs = bot.state.vespene_geyser.closer_than(20.0, th)
for vg in vgs:
worker = bot.select_build_worker(vg.position)
if worker is None:
break

if not bot.units(bot.geyser_type).closer_than(1.0, vg).exists:
return await bot.do(worker.build(bot.geyser_type, vg))

return ActionResult.Error

return Command(do_add_gas, priority=prioritize, repeatable=repeatable)
14 changes: 13 additions & 1 deletion sc2/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .ids.unit_typeid import COMMANDCENTER, ORBITALCOMMAND, PLANETARYFORTRESS
from .ids.unit_typeid import HATCHERY, LAIR, HIVE
from .ids.unit_typeid import ASSIMILATOR, REFINERY, EXTRACTOR

from .ids.unit_typeid import PYLON, OVERLORD, SUPPLYDEPOT
from .ids.ability_id import (
GATEWAYTRAIN_ZEALOT,
GATEWAYTRAIN_STALKER,
Expand Down Expand Up @@ -47,12 +47,24 @@

ActionResult = enum.Enum("ActionResult", error_pb.ActionResult.items())

race_supply = {
Race.Protoss: PYLON,
Race.Terran: SUPPLYDEPOT,
Race.Zerg: OVERLORD
}

race_worker = {
Race.Protoss: PROBE,
Race.Terran: SCV,
Race.Zerg: DRONE
}

race_basic_townhalls = {
Race.Protoss: NEXUS,
Race.Terran: COMMANDCENTER,
Race.Zerg: HATCHERY
}

race_townhalls = {
Race.Protoss: {NEXUS},
Race.Terran: {COMMANDCENTER, ORBITALCOMMAND, PLANETARYFORTRESS},
Expand Down
Empty file.
Loading