diff --git a/Steamhammer/Source/BOSimulator.cpp b/Steamhammer/Source/BOSimulator.cpp new file mode 100644 index 0000000..88f39df --- /dev/null +++ b/Steamhammer/Source/BOSimulator.cpp @@ -0,0 +1,165 @@ +#include "BOSimulator.h" + +using namespace UAlbertaBot; + +// TODO unfinished! this is work in progress + +// Simulate a given build order to estimate its duration and its final gas and minerals. +// This is meant to be a low-level tool to compare build orders, to help in making +// dynamic decisions during games. + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +// Minerals mined over the given (short) duration. The worker count is constant. +// NOTE This is a simplified estimate. The same is used in WorkerManager. +int BOSimulator::mineralsMined(int duration) const +{ + return int(std::round(duration * nWorkers * mineralRate)); +} + +// TODO unimplemented +int BOSimulator::gasMined(int duration) const +{ + return 0; +} + +// When will we have enough resources to produce the item? +// NOTE This doesn't check supply or prerequisites, only resources. +int BOSimulator::findItemFrame(const MacroAct & act) const +{ + if (act.isUnit()) + { + int mineralsNeeded = act.getUnitType().mineralPrice() - minerals; + int gasNeeded = act.getUnitType().gasPrice() - gas; + + if (mineralsNeeded > 0) + { + return frame + int(std::round(mineralsNeeded / (nWorkers * mineralRate))); + } + // Otherwise we already have enough and can fall through. + } + + return frame; +} + +// The next in-progress item is now completing. +void BOSimulator::doInProgressItem() +{ + int nextFrame = inProgress.top().first; + const MacroAct * act = inProgress.top().second; // do not alter inProgress until the end + + int duration = nextFrame - frame; + minerals += mineralsMined(duration); + gas += gasMined(duration); + + if (act->isUnit()) + { + BWAPI::UnitType type = act->getUnitType(); + completedUnits[type] += 1; + supply += type.supplyProvided(); + if (type.isWorker()) + { + ++nWorkers; + } + } + + frame = nextFrame; + inProgress.pop(); +} + +void BOSimulator::doBuildItem(int nextFrame) +{ + int duration = nextFrame - frame; + minerals += mineralsMined(duration); + gas += gasMined(duration); + + const MacroAct & act = buildOrder[boindex]; + if (act.isUnit()) + { + BWAPI::UnitType type = act.getUnitType(); + minerals -= type.mineralPrice(); + gas -= type.gasPrice(); + inProgress.push(std::pair(nextFrame + type.buildTime(), &act)); + } + + UAB_ASSERT(minerals >= 0 && gas >= 0, "resources out of bounds"); + + ++boindex; +} + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +BOSimulator::BOSimulator(const std::vector & bo) + : buildOrder(bo) + , boindex(0) + , frame(BWAPI::Broodwar->getFrameCount()) + , startFrame(BWAPI::Broodwar->getFrameCount()) + , nWorkers(4) // start of game + , minerals(BWAPI::Broodwar->self()->minerals()) + , gas(BWAPI::Broodwar->self()->gas()) + , supply(BWAPI::Broodwar->self()->supplyTotal()) +{ + // TODO find pending research, etc. + + run(); +} + +bool BOSimulator::done() const +{ + return + deadlock || + boindex >= buildOrder.size() && inProgress.empty(); +} + +bool BOSimulator::deadlocked() const +{ + return deadlock; +} + +void BOSimulator::step() +{ + UAB_ASSERT(!done(), "simulation over"); + + // 1. Is the next item a build order item, or the completion of an in-progress item? + bool nextIsInProgress; + int boFrame = -1; + + if (boindex >= buildOrder.size()) + { + nextIsInProgress = true; + } + else + { + const MacroAct & act = buildOrder.at(boindex); + boFrame = findItemFrame(act); + if (inProgress.empty()) + { + nextIsInProgress = false; + } + else + { + // Within a frame, do in progress items before build items. + int inProgressFrame = inProgress.top().first; + nextIsInProgress = inProgressFrame <= boFrame; + } + } + + // 2. Execute the next item. + if (nextIsInProgress) + { + doInProgressItem(); + } + else + { + doBuildItem(boFrame); + } +} + + +void BOSimulator::run() +{ + while (!done()) + { + step(); + } +} diff --git a/Steamhammer/Source/BOSimulator.h b/Steamhammer/Source/BOSimulator.h new file mode 100644 index 0000000..8f0d77d --- /dev/null +++ b/Steamhammer/Source/BOSimulator.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include "MacroAct.h" + +namespace UAlbertaBot +{ + +class BOSimulator +{ +private: + const std::vector & buildOrder; + + size_t boindex; // how far into the simulation? + int frame; // simulated time + + int startFrame; + int nWorkers; + int minerals; + int gas; + int supply; + + bool deadlock; + + // Assumed rate at which 1 worker can mine minerals. + const double mineralRate = 0.045; + + // Completed items. + std::map completedUnits; + + // The finishing time of items that are started and take time to complete. + std::priority_queue< + std::pair, + std::vector< std::pair >, + std::greater< std::pair > + > inProgress; + + int mineralsMined(int duration) const; + int gasMined(int duration) const; + + bool canBuildItem(const MacroAct & act) const; + int findItemFrame(const MacroAct & act) const; + void doInProgressItem(); + void doBuildItem(int nextFrame); + +public: + BOSimulator(const std::vector & bo); + + int getStartFrame() const { return startFrame; }; + int getFrame() const { return frame; }; + int getMinerals() const { return minerals; }; + int getGas() const { return gas; }; + + int getDuration() const { return frame - startFrame; }; + + bool done() const; // the simulation is completed (or deadlocked) + bool deadlocked() const; // the simulation cannot continue (and is therefore done) + void step(); // execute one simulation step + void run(); // run the simulation to its end +}; + +} \ No newline at end of file diff --git a/Steamhammer/Source/Base.cpp b/Steamhammer/Source/Base.cpp index 6253534..8efa1d6 100644 --- a/Steamhammer/Source/Base.cpp +++ b/Steamhammer/Source/Base.cpp @@ -1,6 +1,8 @@ #include "Common.h" #include "Base.h" +#include "WorkerManager.h" + using namespace UAlbertaBot; // NOTE This depends on tilePosition, so the startingBase flag must be declared after tilePosition. @@ -137,6 +139,29 @@ int Base::getInitialGas() const return total; } +// How many workers to saturate the base? +// Two per mineral patch plus three per geyser. +// NOTE This doesn't account for mineral patches mining out, decreasing the maximum. +int Base::getMaxWorkers() const +{ + return 2 * minerals.size() + 3 * geysers.size(); +} + +// Two per mineral patch plus three per geyser. +int Base::getNumWorkers() const +{ + // The number of assigned mineral workers. + int nWorkers = WorkerManager::Instance().getNumWorkers(resourceDepot); + + // Add the assigned gas workers. + for (BWAPI::Unit geyser : geysers) + { + nWorkers += WorkerManager::Instance().getNumWorkers(geyser); + } + + return nWorkers; +} + // The mean offset of the base's mineral patches from the center of the resource depot. // This is used to tell what direction the minerals are in. BWAPI::Position Base::getMineralOffset() const diff --git a/Steamhammer/Source/Base.h b/Steamhammer/Source/Base.h index 65c9a40..158b349 100644 --- a/Steamhammer/Source/Base.h +++ b/Steamhammer/Source/Base.h @@ -19,7 +19,7 @@ class Base BWAPI::Unitset geysers; // the base's associated geysers BWAPI::Unitset blockers; // destructible neutral units that may be in the way GridDistances distances; // ground distances from tilePosition - bool startingBase; // one of the map's starting bases + bool startingBase; // one of the map's starting bases? bool reserved; // if this is a planned expansion bool workerDanger; // for our own bases only; false for others @@ -66,6 +66,10 @@ class Base int getInitialMinerals() const; int getInitialGas() const; + // Workers assigned to mine minerals or gas. + int getMaxWorkers() const; + int getNumWorkers() const; + BWAPI::Position getMineralOffset() const; // mean offset of minerals from base center BWAPI::Position getFrontPoint() const; // the "front" of the base, where static defense should go diff --git a/Steamhammer/Source/Bases.cpp b/Steamhammer/Source/Bases.cpp index 14abb45..aeb556b 100644 --- a/Steamhammer/Source/Bases.cpp +++ b/Steamhammer/Source/Bases.cpp @@ -585,8 +585,8 @@ void Bases::update() } // When a building is placed, we are told the desired and actual location of the building. -// Buildings are usually placed in the main base, so this can give us a hint when the main base -// is full and we need to choose a new one. +// Buildings are usually placed in the "main" base, so this can give us a hint when the main base +// is full and we need to choose a new main base. void Bases::checkBuildingPosition(const BWAPI::TilePosition & desired, const BWAPI::TilePosition & actual) { UAB_ASSERT(desired.isValid(), "bad location"); diff --git a/Steamhammer/Source/BuildingManager.cpp b/Steamhammer/Source/BuildingManager.cpp index a1b1707..4bb1b81 100644 --- a/Steamhammer/Source/BuildingManager.cpp +++ b/Steamhammer/Source/BuildingManager.cpp @@ -141,8 +141,6 @@ void BuildingManager::assignWorkersToUnassignedBuildings() } b.finalPosition = testLocation; - the.micro.Move(b.builderUnit, b.getCenter()); - ++b.buildersSent; // count workers ever assigned to build it //BWAPI::Broodwar->printf("assign builder %d to %s", b.builderUnit->getID(), UnitTypeName(b.type).c_str()); @@ -748,13 +746,17 @@ BWAPI::TilePosition BuildingManager::getBuildingLocation(const Building & b) if (b.type.isResourceDepot()) { BWAPI::TilePosition front = Bases::Instance().frontPoint(); - if (b.macroLocation == MacroLocation::Front && front.isValid()) + if (b.macroLocation == MacroLocation::Front && + front.isValid() && + !the.groundAttacks.inRange(b.type, front)) { // This means build an additional hatchery at our existing front base, wherever it is. return front; } Base * natural = Bases::Instance().myNaturalBase(); - if (b.macroLocation == MacroLocation::Natural && natural) + if (b.macroLocation == MacroLocation::Natural && + natural && + !the.groundAttacks.inRange(b.type, natural->getTilePosition())) { return natural->getTilePosition(); } diff --git a/Steamhammer/Source/BuildingPlacer.cpp b/Steamhammer/Source/BuildingPlacer.cpp index dcfa23f..b3f343f 100644 --- a/Steamhammer/Source/BuildingPlacer.cpp +++ b/Steamhammer/Source/BuildingPlacer.cpp @@ -129,10 +129,10 @@ bool BuildingPlacer::tileBlocksAddon(BWAPI::TilePosition position) const // The tile is free of permanent obstacles, including future ones from planned buildings. // There might be a unit passing through, though. // The caller must ensure that x and y are in range! -bool BuildingPlacer::freeTile(int x, int y) const -{ - UAB_ASSERT(BWAPI::TilePosition(x,y).isValid(), "bad tile"); - +bool BuildingPlacer::freeTile(int x, int y) const +{ + UAB_ASSERT(BWAPI::TilePosition(x,y).isValid(), "bad tile"); + if (!BWAPI::Broodwar->isBuildable(x, y, true) || _reserveMap[x][y]) { return false; @@ -141,9 +141,9 @@ bool BuildingPlacer::freeTile(int x, int y) const { return false; } - - return true; -} + + return true; +} // Check that nothing obstructs the top of the building, including the corners. // For example, if the building is o, then nothing must obstruct the tiles marked x: @@ -154,105 +154,105 @@ bool BuildingPlacer::freeTile(int x, int y) const // // Unlike canBuildHere(), the freeOn...() functions do not care if mobile units are on the tiles. // They only care that the tiles are buildable (which implies walkable) and not reserved for future buildings. -bool BuildingPlacer::freeOnTop(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const -{ - int x1 = tile.x - 1; - int x2 = tile.x + buildingType.tileWidth(); - int y = tile.y - 1; - if (y < 0 || x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth()) - { - return false; - } - - for (int x = x1; x <= x2; ++x) - { +bool BuildingPlacer::freeOnTop(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const +{ + int x1 = tile.x - 1; + int x2 = tile.x + buildingType.tileWidth(); + int y = tile.y - 1; + if (y < 0 || x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth()) + { + return false; + } + + for (int x = x1; x <= x2; ++x) + { if (!freeTile(x,y)) { return false; } - } - return true; -} - + } + return true; +} + // x // o o x // o o x // x -bool BuildingPlacer::freeOnRight(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const -{ - int x = tile.x + buildingType.tileWidth(); - int y1 = tile.y - 1; - int y2 = tile.y + buildingType.tileHeight(); - if (x >= BWAPI::Broodwar->mapWidth() || y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) - { - return false; - } - - for (int y = y1; y <= y2; ++y) - { +bool BuildingPlacer::freeOnRight(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const +{ + int x = tile.x + buildingType.tileWidth(); + int y1 = tile.y - 1; + int y2 = tile.y + buildingType.tileHeight(); + if (x >= BWAPI::Broodwar->mapWidth() || y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) + { + return false; + } + + for (int y = y1; y <= y2; ++y) + { if (!freeTile(x, y)) { return false; } - } - return true; -} - -// x + } + return true; +} + +// x +// x o o // x o o -// x o o -// x -bool BuildingPlacer::freeOnLeft(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const -{ - int x = tile.x - 1; - int y1 = tile.y - 1; - int y2 = tile.y + buildingType.tileHeight(); - if (x < 0 || y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) - { - return false; - } - - for (int y = y1; y <= y2; ++y) - { +// x +bool BuildingPlacer::freeOnLeft(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const +{ + int x = tile.x - 1; + int y1 = tile.y - 1; + int y2 = tile.y + buildingType.tileHeight(); + if (x < 0 || y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) + { + return false; + } + + for (int y = y1; y <= y2; ++y) + { if (!freeTile(x, y)) { return false; } - } - return true; -} - + } + return true; +} + +// o o // o o -// o o // x x x x -bool BuildingPlacer::freeOnBottom(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const -{ - int x1 = tile.x - 1; - int x2 = tile.x + buildingType.tileWidth(); - int y = tile.y + buildingType.tileHeight(); - if (y >= BWAPI::Broodwar->mapHeight() || x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth()) - { - return false; - } - - for (int x = x1; x <= x2; ++x) - { +bool BuildingPlacer::freeOnBottom(const BWAPI::TilePosition & tile, BWAPI::UnitType buildingType) const +{ + int x1 = tile.x - 1; + int x2 = tile.x + buildingType.tileWidth(); + int y = tile.y + buildingType.tileHeight(); + if (y >= BWAPI::Broodwar->mapHeight() || x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth()) + { + return false; + } + + for (int x = x1; x <= x2; ++x) + { if (!freeTile(x, y)) { return false; } - } - return true; -} - -bool BuildingPlacer::freeOnAllSides(BWAPI::Unit building) const -{ - return - freeOnTop(building->getTilePosition(), building->getType()) && - freeOnRight(building->getTilePosition(), building->getType()) && - freeOnLeft(building->getTilePosition(), building->getType()) && - freeOnBottom(building->getTilePosition(), building->getType()); -} + } + return true; +} + +bool BuildingPlacer::freeOnAllSides(BWAPI::Unit building) const +{ + return + freeOnTop(building->getTilePosition(), building->getType()) && + freeOnRight(building->getTilePosition(), building->getType()) && + freeOnLeft(building->getTilePosition(), building->getType()) && + freeOnBottom(building->getTilePosition(), building->getType()); +} // Can a building can be built here? // This does not check all conditions! Other code must check for possible overlaps @@ -266,7 +266,11 @@ bool BuildingPlacer::canBuildHere(BWAPI::TilePosition position, const Building & // A worker can reach the place. // NOTE This simplified check disallows building on islands! - Bases::Instance().connectedToStart(position); + Bases::Instance().connectedToStart(position) && + + // Enemy static defense cannot fire on the building location. + // This is part of the response to e.g. cannon rushes. + !the.groundAttacks.inRange(b.type, position); } // Can we build this building here with the specified amount of space around it? @@ -299,19 +303,19 @@ bool BuildingPlacer::canBuildWithSpace(BWAPI::TilePosition position, const Build int x2 = position.x + width + extraSpace - 1; int y2 = position.y + height + extraSpace - 1; - // The rectangle must fit on the map. - if (x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth() || - y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) - { - return false; - } - - if (boxOverlapsBase(x1, y1, x2, y2)) - { - return false; - } - - // Every tile must be buildable and unreserved. + // The rectangle must fit on the map. + if (x1 < 0 || x2 >= BWAPI::Broodwar->mapWidth() || + y1 < 0 || y2 >= BWAPI::Broodwar->mapHeight()) + { + return false; + } + + if (boxOverlapsBase(x1, y1, x2, y2)) + { + return false; + } + + // Every tile must be buildable and unreserved. for (int x = x1; x <= x2; ++x) { for (int y = y1; y <= y2; ++y) @@ -592,7 +596,6 @@ BWAPI::TilePosition BuildingPlacer::getBuildLocationNear(const Building & b, int } // Let Bases decide whether the change the main base to another base. - // Bases::Instance().checkBuildingPosition(b.desiredPosition, tile); return tile; // may be None @@ -653,11 +656,18 @@ BWAPI::TilePosition BuildingPlacer::getRefineryPosition() // have been canceled or destroyed: They become inaccessible. https://github.com/bwapi/bwapi/issues/697 for (const auto geyser : BWAPI::Broodwar->getGeysers()) { - // check to see if it's near one of our depots - for (const auto unit : BWAPI::Broodwar->self()->getUnits()) - { - if (unit->getType().isResourceDepot() && unit->getDistance(geyser) < 300) - { + // Check to see if the geyser is near one of our depots. + for (const auto unit : BWAPI::Broodwar->self()->getUnits()) + { + if (unit->getType().isResourceDepot() && unit->getDistance(geyser) < 300) + { + // Don't take a geyser which is in enemy static defense range. It'll just die. + // This is rare so we check it only after other checks succeed. + if (the.groundAttacks.inRange(geyser->getType(), geyser->getTilePosition())) + { + break; + } + int homeDistance = geyser->getDistance(homePosition); if (homeDistance < minGeyserDistanceFromHome) @@ -666,9 +676,9 @@ BWAPI::TilePosition BuildingPlacer::getRefineryPosition() closestGeyser = geyser->getTilePosition(); // BWAPI bug workaround by Arrak } break; - } - } - } + } + } + } return closestGeyser; } diff --git a/Steamhammer/Source/CombatCommander.cpp b/Steamhammer/Source/CombatCommander.cpp index f36267a..055c7bc 100644 --- a/Steamhammer/Source/CombatCommander.cpp +++ b/Steamhammer/Source/CombatCommander.cpp @@ -196,9 +196,13 @@ void CombatCommander::chooseScourgeTarget(const Squad & sourgeSquad) { const UnitInfo & ui(kv.second); - // Skip ground units and units known to have moved away some time ago. + // Skip inappropriate units and units known to have moved away some time ago. + // Also stay out of range of enemy static air defense. if (!ui.type.isFlyer() || - ui.goneFromLastPosition && BWAPI::Broodwar->getFrameCount() - ui.updateFrame < 5 * 24) + ui.type == BWAPI::UnitTypes::Protoss_Interceptor || + ui.type == BWAPI::UnitTypes::Zerg_Overlord || + ui.goneFromLastPosition && BWAPI::Broodwar->getFrameCount() - ui.updateFrame > 5 * 24 || + the.airAttacks.inRange(BWAPI::TilePosition(ui.lastPosition))) { continue; } @@ -212,7 +216,7 @@ void CombatCommander::chooseScourgeTarget(const Squad & sourgeSquad) // Each score increment is worth 2 tiles of distance. const int distance = center.getApproxDistance(ui.lastPosition); - score = 2 * score - distance / 32; + score -= distance / 16; if (score > bestScore) { bestTarget = ui.lastPosition; @@ -615,8 +619,8 @@ BWAPI::Position CombatCommander::getReconLocation() const // Form the ground squad and the flying squad, the main attack squads. // NOTE Arbiters and guardians go into the ground squad. // Devourers are flying squad if it exists, otherwise ground. -// Carriers are flying squad if it exists or the carrier count is high enough. -// Other air units always go into the flying squad. +// Carriers are flying squad if it already exists or if the carrier count is high enough. +// Other air units always go into the flying squad (except scourge, they are in their own squad). void CombatCommander::updateAttackSquads() { Squad & groundSquad = _squadData.getSquad("Ground"); @@ -1461,19 +1465,21 @@ void CombatCommander::cancelDyingItems() type == BWAPI::UnitTypes::Zerg_Egg || type == BWAPI::UnitTypes::Zerg_Lurker_Egg || type == BWAPI::UnitTypes::Zerg_Cocoon - ) && - ( unit->getHitPoints() < 30 || - type == BWAPI::UnitTypes::Zerg_Sunken_Colony && unit->getHitPoints() < 130 && unit->getRemainingBuildTime() < 24 )) { - if (unit->canCancelMorph()) - { - unit->cancelMorph(); - } - else if (unit->canCancelConstruction()) - { - the.micro.Cancel(unit); - } + const int timeSoFar = unit->getType().buildTime() - unit->getRemainingBuildTime(); // time under construction so far + if (unit->getHitPoints() < 30 && timeSoFar >= 2 * 24 || + type == BWAPI::UnitTypes::Zerg_Sunken_Colony && unit->getHitPoints() < 130 && unit->getRemainingBuildTime() < 24) + { + if (unit->canCancelMorph()) + { + unit->cancelMorph(); + } + else if (unit->canCancelConstruction()) + { + the.micro.Cancel(unit); + } + } } } } @@ -1597,7 +1603,7 @@ SquadOrder CombatCommander::getAttackOrder(const Squad * squad) return SquadOrder(SquadOrderTypes::Attack, getAttackLocation(squad), AttackRadius, "Attack enemy"); } -// Choose a point of attack for the given squad (which may be null). +// Choose a point of attack for the given squad (which may be null--no squad at all). // For a squad with ground units, ignore targets which are not accessible by ground. BWAPI::Position CombatCommander::getAttackLocation(const Squad * squad) { @@ -1656,7 +1662,8 @@ BWAPI::Position CombatCommander::getAttackLocation(const Squad * squad) } std::vector enemies; - InformationManager::Instance().getNearbyForce(enemies, base->getCenter(), BWAPI::Broodwar->enemy(), 384); + int enemyDefenseRange = InformationManager::Instance().enemyHasSiegeMode() ? 12 * 32 : 8 * 32; + InformationManager::Instance().getNearbyForce(enemies, base->getCenter(), BWAPI::Broodwar->enemy(), enemyDefenseRange); for (const auto & enemy : enemies) { // Count enemies that are buildings or slow-moving units good for defense. @@ -1702,6 +1709,7 @@ BWAPI::Position CombatCommander::getAttackLocation(const Squad * squad) // 2. We only know that a building is lifted while it is in sight. That can cause oscillatory // behavior--we move away, can't see it, move back because now we can attack it, see it is lifted, .... if (ui.type.isBuilding() && + !ui.type.isAddon() && ui.lastPosition.isValid() && !ui.goneFromLastPosition && (ui.type.isRefinery() || squadPartition == the.partitions.id(ui.lastPosition))) @@ -1754,7 +1762,26 @@ BWAPI::Position CombatCommander::getAttackLocation(const Squad * squad) } } - // 4. We can't see anything, so explore the map until we find something. + // 4. Attack the remembered locations of unseen enemy units which might still be there. + // Choose the one most recently seen. + int lastSeenFrame = 0; + BWAPI::Position lastSeenPos = BWAPI::Positions::None; + for (const auto & kv : InformationManager::Instance().getUnitData(BWAPI::Broodwar->enemy()).getUnits()) + { + const UnitInfo & ui(kv.second); + + if (!ui.goneFromLastPosition && ui.updateFrame > lastSeenFrame) + { + lastSeenFrame = ui.updateFrame; + lastSeenPos = ui.lastPosition; + } + } + if (lastSeenPos.isValid()) + { + return lastSeenPos; + } + + // 5. We can't see anything, so explore the map until we find something. return MapGrid::Instance().getLeastExplored(hasGround && !hasAir, squadPartition); } diff --git a/Steamhammer/Source/CombatSimulation.cpp b/Steamhammer/Source/CombatSimulation.cpp index 5318a6c..2aee3f3 100644 --- a/Steamhammer/Source/CombatSimulation.cpp +++ b/Steamhammer/Source/CombatSimulation.cpp @@ -60,6 +60,18 @@ CombatSimEnemies CombatSimulation::analyzeForEnemies(const BWAPI::Unitset units) return CombatSimEnemies::AllEnemies; } +bool CombatSimulation::allFlying(const BWAPI::Unitset units) const +{ + for (BWAPI::Unit unit : units) + { + if (!unit->isFlying()) + { + return false; + } + } + return true; +} + void CombatSimulation::drawWhichEnemies(const BWAPI::Position center) const { std::string whichEnemies = "All Enemies"; @@ -114,7 +126,7 @@ bool CombatSimulation::includeEnemy(CombatSimEnemies which, BWAPI::UnitType type if (which == CombatSimEnemies::ScourgeEnemies) { // Only ground enemies that can shoot up. - // For scourge only. The scourge will take on air enemies no matter what. + // For scourge only. The scourge will take on air enemies no matter the odds. return !type.isFlyer() && UnitUtil::GetAirWeapon(type) != BWAPI::WeaponTypes::None; @@ -124,10 +136,59 @@ bool CombatSimulation::includeEnemy(CombatSimEnemies which, BWAPI::UnitType type return true; } +// Our air units ignore undetected dark templar. +// This variant of includeEnemy() is called only when the enemy unit is visible. +bool CombatSimulation::includeEnemy(CombatSimEnemies which, BWAPI::Unit enemy) const +{ + if (_allFriendliesFlying && + enemy->getType() == BWAPI::UnitTypes::Protoss_Dark_Templar && !enemy->isDetected()) + { + return false; + } + + return includeEnemy(which, enemy->getType()); +} + +bool CombatSimulation::undetectedEnemy(BWAPI::Unit enemy) const +{ + if (enemy->isVisible()) + { + return !enemy->isDetected(); + } + + // The enemy is out of sight. + // Consider it undetected if it is likely to be cloaked, or if it is an arbiter. + // NOTE This will often be wrong! + return + enemy->getType() != BWAPI::UnitTypes::Terran_Vulture_Spider_Mine && + enemy->getType() != BWAPI::UnitTypes::Protoss_Dark_Templar && + enemy->getType() != BWAPI::UnitTypes::Protoss_Arbiter && + enemy->getType() != BWAPI::UnitTypes::Zerg_Lurker; +} + +bool CombatSimulation::undetectedEnemy(const UnitInfo & enemyUI) const +{ + if (enemyUI.unit->isVisible()) + { + return !enemyUI.unit->isDetected(); + } + + // The enemy is out of sight. + // Consider it undetected if it is likely to be cloaked. + // NOTE This will often be wrong! + return + enemyUI.type != BWAPI::UnitTypes::Terran_Vulture_Spider_Mine && + enemyUI.type != BWAPI::UnitTypes::Protoss_Dark_Templar && + enemyUI.type != BWAPI::UnitTypes::Protoss_Arbiter && + enemyUI.type != BWAPI::UnitTypes::Zerg_Lurker; +} + // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- CombatSimulation::CombatSimulation() : _whichEnemies(CombatSimEnemies::AllEnemies) + , _allEnemiesUndetected(false) + , _allFriendliesFlying(false) { } @@ -167,6 +228,10 @@ void CombatSimulation::setCombatUnits ) { _whichEnemies = analyzeForEnemies(myUnits); + _allFriendliesFlying = allFlying(myUnits); + + // If all enemies are cloaked and undetected, we can run away without needing to do a sim. + _allEnemiesUndetected = true; fap.clearState(); @@ -197,14 +262,35 @@ void CombatSimulation::setCombatUnits // Add enemy units. if (visibleOnly) { - // Only units that we can see right now. + // Static defense that is out of sight. + // NOTE getNearbyForce() includes completed units and uncompleted buildings which are out of vision. + std::vector enemyStaticDefense; + InformationManager::Instance().getNearbyForce(enemyStaticDefense, center, BWAPI::Broodwar->enemy(), radius); + for (const UnitInfo & ui : enemyStaticDefense) + { + if (ui.type.isBuilding() && !ui.unit->isVisible() && includeEnemy(_whichEnemies, ui.type)) + { + _allEnemiesUndetected = false; + fap.addIfCombatUnitPlayer2(ui); + if (Config::Debug::DrawCombatSimulationInfo) + { + BWAPI::Broodwar->drawCircleMap(ui.lastPosition, 3, BWAPI::Colors::Orange, true); + } + } + } + + // Only units that we can see right now. BWAPI::Unitset enemyCombatUnits; MapGrid::Instance().getUnits(enemyCombatUnits, center, radius, false, true); - for (const auto unit : enemyCombatUnits) + for (BWAPI::Unit unit : enemyCombatUnits) { if (UnitUtil::IsCombatSimUnit(unit) && - includeEnemy(_whichEnemies, unit->getType())) + includeEnemy(_whichEnemies, unit)) { + if (_allEnemiesUndetected && !undetectedEnemy(unit)) + { + _allEnemiesUndetected = false; + } fap.addIfCombatUnitPlayer2(unit); if (Config::Debug::DrawCombatSimulationInfo) { @@ -212,22 +298,6 @@ void CombatSimulation::setCombatUnits } } } - - // Also static defense that is out of sight. - // NOTE getNearbyForce() includes completed units and uncompleted buildings which are out of vision. - std::vector enemyStaticDefense; - InformationManager::Instance().getNearbyForce(enemyStaticDefense, center, BWAPI::Broodwar->enemy(), radius); - for (const UnitInfo & ui : enemyStaticDefense) - { - if (ui.type.isBuilding() && !ui.unit->isVisible() && includeEnemy(_whichEnemies, ui.type)) - { - fap.addIfCombatUnitPlayer2(ui); - if (Config::Debug::DrawCombatSimulationInfo) - { - BWAPI::Broodwar->drawCircleMap(ui.lastPosition, 3, BWAPI::Colors::Orange, true); - } - } - } } else { @@ -239,9 +309,13 @@ void CombatSimulation::setCombatUnits { // The check is careful about seen units and assumes that unseen units are completed and powered. if ((ui.unit->exists() || ui.lastPosition.isValid() && !ui.goneFromLastPosition) && - includeEnemy(_whichEnemies, ui.type)) + ui.unit->isVisible() ? includeEnemy(_whichEnemies, ui.unit) : includeEnemy(_whichEnemies, ui.type)) { - fap.addIfCombatUnitPlayer2(ui); + if (_allEnemiesUndetected && !undetectedEnemy(ui)) + { + _allEnemiesUndetected = false; + } + fap.addIfCombatUnitPlayer2(ui); if (ui.type == BWAPI::UnitTypes::Terran_Missile_Turret) { @@ -267,7 +341,8 @@ void CombatSimulation::setCombatUnits // Add our units. // Add them from the input set. Other units have been given other instructions // and may not cooperate in the fight, so skip them. - for (const auto unit : myUnits) + // NOTE This does not include our static defense unless the caller passed it in! + for (BWAPI::Unit unit : myUnits) { if (UnitUtil::IsCombatSimUnit(unit)) { @@ -297,6 +372,13 @@ double CombatSimulation::simulateCombat(bool meatgrinder) return 0.0; } + // If all enemies are undetected, and can hit us, we should run away. + // We approximate "and can hit us" by ignoring undetected enemy DTs if we are all flying units. + if (_allEnemiesUndetected) + { + return -1.0; + } + fap.simulate(); std::pair endScores = fap.playerScores(); diff --git a/Steamhammer/Source/CombatSimulation.h b/Steamhammer/Source/CombatSimulation.h index 0593c12..421076d 100644 --- a/Steamhammer/Source/CombatSimulation.h +++ b/Steamhammer/Source/CombatSimulation.h @@ -19,10 +19,17 @@ class CombatSimulation { private: CombatSimEnemies _whichEnemies; + bool _allEnemiesUndetected; + bool _allFriendliesFlying; CombatSimEnemies analyzeForEnemies(const BWAPI::Unitset units) const; - void drawWhichEnemies(const BWAPI::Position center) const; + bool allFlying(const BWAPI::Unitset units) const; + void drawWhichEnemies(const BWAPI::Position center) const; bool includeEnemy(CombatSimEnemies which, BWAPI::UnitType type) const; + bool includeEnemy(CombatSimEnemies which, BWAPI::Unit enemy) const; + + bool undetectedEnemy(BWAPI::Unit enemy) const; + bool undetectedEnemy(const UnitInfo & enemyUI) const; BWAPI::Position getClosestEnemyCombatUnit(const BWAPI::Position & center) const; diff --git a/Steamhammer/Source/Common.cpp b/Steamhammer/Source/Common.cpp index 3baf795..c50a4fa 100644 --- a/Steamhammer/Source/Common.cpp +++ b/Steamhammer/Source/Common.cpp @@ -97,6 +97,11 @@ std::string UnitTypeName(BWAPI::UnitType type) return TrimRaceName(type.getName()); } +std::string UnitTypeName(BWAPI::Unit unit) +{ + return UnitTypeName(unit->getType()); +} + // Post a message to the game including the bot's name. void GameMessage(const char * message) { @@ -110,8 +115,9 @@ void GameMessage(const char * message) // Point b specifies a direction from point a. // Return a position at the given distance and direction from a. +// The result may be off the map. // The distance can be negative. -BWAPI::Position DistanceAndDirection(const BWAPI::Position & a, const BWAPI::Position & b, int distance) +BWAPI::Position RawDistanceAndDirection(const BWAPI::Position & a, const BWAPI::Position & b, int distance) { if (a == b) { @@ -122,6 +128,14 @@ BWAPI::Position DistanceAndDirection(const BWAPI::Position & a, const BWAPI::Pos return a + (difference.normalize() * double(distance)); } +// Point b specifies a direction from point a. +// Return a position at the given distance and direction from a, clipped to the map boundaries. +// The distance can be negative. +BWAPI::Position DistanceAndDirection(const BWAPI::Position & a, const BWAPI::Position & b, int distance) +{ + return RawDistanceAndDirection(a, b, distance).makeValid(); +} + // Return the speed (pixels per frame) at which unit u is approaching the position. // It may be positive or negative. // This is approach speed only, ignoring transverse speed. For example, if the @@ -135,6 +149,45 @@ double ApproachSpeed(const BWAPI::Position & pos, BWAPI::Unit u) return velocity.dot(direction); } +BWAPI::Unit NearestOf(const BWAPI::Position & pos, const BWAPI::Unitset & set) +{ + int bestDistance = 999999; + BWAPI::Unit bestUnit = nullptr; + + for (BWAPI::Unit unit : set) + { + int dist = unit->getDistance(pos); + if (dist < bestDistance) + { + bestDistance = dist; + bestUnit = unit; + } + } + + return bestUnit; +} + +BWAPI::Unit NearestOf(const BWAPI::Position & pos, const BWAPI::Unitset & set, BWAPI::UnitType type) +{ + int bestDistance = 999999; + BWAPI::Unit bestUnit = nullptr; + + for (BWAPI::Unit unit : set) + { + if (unit->getType() == type) + { + int dist = unit->getDistance(pos); + if (dist < bestDistance) + { + bestDistance = dist; + bestUnit = unit; + } + } + } + + return bestUnit; +} + // Find the geometric center of a set of visible units. // We call it (0,0) if there are no units--better check this before calling. BWAPI::Position CenterOfUnitset(const BWAPI::Unitset units) diff --git a/Steamhammer/Source/Common.h b/Steamhammer/Source/Common.h index c15997e..9b1c9e6 100644 --- a/Steamhammer/Source/Common.h +++ b/Steamhammer/Source/Common.h @@ -74,6 +74,7 @@ std::string TrimRaceName(const std::string & s); char RaceChar(BWAPI::Race race); std::string NiceMacroActName(const std::string & s); std::string UnitTypeName(BWAPI::UnitType type); +std::string UnitTypeName(BWAPI::Unit unit); // Short color codes for drawing text on the screen. // The dim colors can be hard to read, but are useful occasionally. @@ -89,9 +90,12 @@ const char cyan = '\x1F'; void GameMessage(const char * message); +BWAPI::Position RawDistanceAndDirection(const BWAPI::Position & a, const BWAPI::Position & b, int distance); BWAPI::Position DistanceAndDirection(const BWAPI::Position & a, const BWAPI::Position & b, int distance); double ApproachSpeed(const BWAPI::Position & pos, BWAPI::Unit u); BWAPI::Position CenterOfUnitset(const BWAPI::Unitset units); +BWAPI::Unit NearestOf(const BWAPI::Position & pos, const BWAPI::Unitset & set); +BWAPI::Unit NearestOf(const BWAPI::Position & pos, const BWAPI::Unitset & set, BWAPI::UnitType type); BWAPI::Position PredictMovement(BWAPI::Unit unit, int frames); bool CanCatchUnit(BWAPI::Unit chaser, BWAPI::Unit runaway); diff --git a/Steamhammer/Source/Config.cpp b/Steamhammer/Source/Config.cpp index b3c180e..b13e524 100644 --- a/Steamhammer/Source/Config.cpp +++ b/Steamhammer/Source/Config.cpp @@ -12,7 +12,7 @@ namespace Config { bool ConfigFileFound = false; bool ConfigFileParsed = false; - std::string ConfigFileLocation = "bwapi-data/AI/Steamhammer_2.3.json"; + std::string ConfigFileLocation = "bwapi-data/AI/Steamhammer_2.3.5.json"; } namespace IO @@ -20,7 +20,8 @@ namespace Config std::string ErrorLogFilename = "Steamhammer_ErrorLog.txt"; bool LogAssertToErrorFile = false; - std::string ReadDir = "bwapi-data/read/"; + std::string StaticDir = "bwapi-data/AI/"; + std::string ReadDir = "bwapi-data/read/"; std::string WriteDir = "bwapi-data/write/"; int MaxGameRecords = 0; bool ReadOpponentModel = false; diff --git a/Steamhammer/Source/Config.h b/Steamhammer/Source/Config.h index f18685f..801b919 100644 --- a/Steamhammer/Source/Config.h +++ b/Steamhammer/Source/Config.h @@ -24,6 +24,7 @@ namespace Config extern std::string ErrorLogFilename; extern bool LogAssertToErrorFile; + extern std::string StaticDir; extern std::string ReadDir; extern std::string WriteDir; extern int MaxGameRecords; diff --git a/Steamhammer/Source/FAP.cpp b/Steamhammer/Source/FAP.cpp index 67d830a..3de1d31 100644 --- a/Steamhammer/Source/FAP.cpp +++ b/Steamhammer/Source/FAP.cpp @@ -1,5 +1,6 @@ #include "FAP.h" #include "BWAPI.h" +#include "UnitUtil.h" UAlbertaBot::FastAPproximation fap; @@ -132,7 +133,9 @@ namespace UAlbertaBot { fu.health -= std::max(1, damage - fu.armor); } - int inline FastAPproximation::distButNotReally(const FastAPproximation::FAPUnit &u1, const FastAPproximation::FAPUnit &u2) const { + // The square of the Euclidean distance between the units' positions. + // Skip the expensive step of computing the square root. + int inline FastAPproximation::distSquared(const FastAPproximation::FAPUnit &u1, const FastAPproximation::FAPUnit &u2) const { return (u1.x - u2.x)*(u1.x - u2.x) + (u1.y - u2.y)*(u1.y - u2.y); } @@ -151,23 +154,25 @@ namespace UAlbertaBot { } auto closestEnemy = enemyUnits.end(); - int closestDist = 99999; + int closestDist = 99999999; // actually distance squared // NOTE This skips siege tanks, which do splash damage under swarm. const bool hitUnderSwarm = fu.groundDamage && - ( fu.groundMaxRange <= 32 || + !fu.unitType.isWorker() && + ( fu.groundMaxRange <= 32 * 32 || // "range" is actually squared range isSuicideUnit(fu.unitType) || fu.unitType == BWAPI::UnitTypes::Protoss_Archon || - fu.unitType == BWAPI::UnitTypes::Zerg_Lurker + fu.unitType == BWAPI::UnitTypes::Protoss_Reaver || + fu.unitType == BWAPI::UnitTypes::Zerg_Lurker ); // Find the closest enemy unit which is not too close to hit with our weapon. // A sieged tank has a minimum range; all other weapons have min range 0 (so we only check ground weapons). - for (auto enemyIt = enemyUnits.begin(); enemyIt != enemyUnits.end(); ++ enemyIt) { + for (auto enemyIt = enemyUnits.begin(); enemyIt != enemyUnits.end(); ++enemyIt) { if (enemyIt->flying) { if (fu.airDamage) { - int d = distButNotReally(fu, *enemyIt); + int d = distSquared(fu, *enemyIt); if (closestEnemy == enemyUnits.end() || d < closestDist) { closestDist = d; closestEnemy = enemyIt; @@ -176,7 +181,7 @@ namespace UAlbertaBot { } else { if (fu.groundDamage && (!enemyIt->underSwarm || hitUnderSwarm)) { - int d = distButNotReally(fu, *enemyIt); + int d = distSquared(fu, *enemyIt); if ((closestEnemy == enemyUnits.end() || d < closestDist) && d >= fu.groundMinRange) { closestDist = d; closestEnemy = enemyIt; @@ -187,7 +192,9 @@ namespace UAlbertaBot { // If we can reach the enemy this simulated frame, do it and continue. - if (closestEnemy != enemyUnits.end() && closestDist <= fu.speed * fu.speed && !(fu.x == closestEnemy->x && fu.y == closestEnemy->y)) { + if (closestEnemy != enemyUnits.end() && + closestDist <= fu.speed * fu.speed && + !(fu.x == closestEnemy->x && fu.y == closestEnemy->y)) { fu.x = closestEnemy->x; fu.y = closestEnemy->y; closestDist = 0; @@ -196,7 +203,8 @@ namespace UAlbertaBot { } // Shoot at the enemy if in range, otherwise move toward the enemy. - if (closestEnemy != enemyUnits.end() && closestDist <= (closestEnemy->flying ? fu.airMaxRange : fu.groundMaxRange)) { + if (closestEnemy != enemyUnits.end() && + closestDist <= (closestEnemy->flying ? fu.airMaxRange : fu.groundMaxRange)) { if (closestEnemy->flying) { dealDamage(*closestEnemy, fu.airDamage, fu.airDamageType); fu.attackCooldownRemaining = fu.airCooldown; @@ -230,11 +238,11 @@ namespace UAlbertaBot { void FastAPproximation::medicsim(const FAPUnit & fu, std::vector &friendlyUnits) { auto closestHealable = friendlyUnits.end(); - int closestDist = 99999; + int closestDist = 99999999; for (auto it = friendlyUnits.begin(); it != friendlyUnits.end(); ++it) { if (it->isOrganic && it->health < it->maxHealth && !it->didHealThisFrame) { - int d = distButNotReally(fu, *it); + int d = distSquared(fu, *it); if (closestHealable == friendlyUnits.end() || d < closestDist) { closestHealable = it; closestDist = d; @@ -260,12 +268,12 @@ namespace UAlbertaBot { bool FastAPproximation::suicideSim(const FAPUnit & fu, std::vector& enemyUnits) { auto closestEnemy = enemyUnits.end(); - int closestDist = 99999; + int closestDist = 99999999; for (auto enemyIt = enemyUnits.begin(); enemyIt != enemyUnits.end(); ++enemyIt) { if (enemyIt->flying) { if (fu.airDamage) { - int d = distButNotReally(fu, *enemyIt); + int d = distSquared(fu, *enemyIt); if (closestEnemy == enemyUnits.end() || d < closestDist) { closestDist = d; closestEnemy = enemyIt; @@ -274,7 +282,7 @@ namespace UAlbertaBot { } else { if (fu.groundDamage) { - int d = distButNotReally(fu, *enemyIt); + int d = distSquared(fu, *enemyIt); if ((closestEnemy == enemyUnits.end() || d < closestDist) && d >= fu.groundMinRange) { closestDist = d; closestEnemy = enemyIt; @@ -456,6 +464,7 @@ namespace UAlbertaBot { elevation = BWAPI::Broodwar->getGroundHeight(BWAPI::TilePosition(x,y)); } + // Convert ranges to squared ranges so they can be compared with squared distances. groundMaxRange *= groundMaxRange; groundMinRange *= groundMinRange; airMaxRange *= airMaxRange; @@ -472,6 +481,7 @@ namespace UAlbertaBot { maxShields *= 2; } + // Copy a FAPUnit. const FastAPproximation::FAPUnit &FastAPproximation::FAPUnit::operator=(const FAPUnit & other) const { x = other.x, y = other.y; health = other.health, maxHealth = other.maxHealth; @@ -495,19 +505,36 @@ namespace UAlbertaBot { // Some types get special case scores. int FastAPproximation::FAPUnit::unitScore(BWAPI::UnitType type) const { - if (type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) { + if (type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) + { return 20; } - if (type == BWAPI::UnitTypes::Protoss_Archon) { + if (type == BWAPI::UnitTypes::Protoss_Archon) + { return 2 * (50 + 150); } - if (type == BWAPI::UnitTypes::Protoss_Dark_Archon) { + if (type == BWAPI::UnitTypes::Protoss_Dark_Archon) + { return 2 * (125 + 100); } - if (type == BWAPI::UnitTypes::Zerg_Sunken_Colony || - type == BWAPI::UnitTypes::Zerg_Spore_Colony) + if (type == BWAPI::UnitTypes::Protoss_Reaver) { - return 50 + 75 + 50; + return 200 + 100 + 5 * 15; // account for scarabs + } + if (type == BWAPI::UnitTypes::Protoss_Carrier) + { + return 350 + 250 + 8 * 25; // account for interceptors + } + if (type.getRace() == BWAPI::Races::Zerg && (type.isBuilding() || UnitUtil::IsMorphedUnitType(type))) + { + // Add up the total price of a morphed unit, e.g. hydra + lurker morph, muta + guardian morph. + // The longest chain that goes into combat sim is drone + creep colony + sunken/spore. + int cost = 0; + for (BWAPI::UnitType t = type; t != BWAPI::UnitTypes::Zerg_Larva; t = t.whatBuilds().first) + { + cost += t.mineralPrice() + t.gasPrice(); + } + return cost; } if (type == BWAPI::UnitTypes::Zerg_Broodling) { diff --git a/Steamhammer/Source/FAP.h b/Steamhammer/Source/FAP.h index b333162..ac83836 100644 --- a/Steamhammer/Source/FAP.h +++ b/Steamhammer/Source/FAP.h @@ -31,13 +31,13 @@ namespace UAlbertaBot { mutable int groundDamage = 0; mutable int groundCooldown = 0; - mutable int groundMaxRange = 0; - mutable int groundMinRange = 0; + mutable int groundMaxRange = 0; // square of the true range + mutable int groundMinRange = 0; // square of the true range mutable BWAPI::DamageType groundDamageType; mutable int airDamage = 0; mutable int airCooldown = 0; - mutable int airMaxRange = 0; + mutable int airMaxRange = 0; // square of the true range mutable BWAPI::DamageType airDamageType; mutable BWAPI::UnitType unitType; @@ -82,7 +82,7 @@ namespace UAlbertaBot { bool didSomething; void dealDamage(const FastAPproximation::FAPUnit & fu, int damage, BWAPI::DamageType damageType) const; - int distButNotReally(const FastAPproximation::FAPUnit & u1, const FastAPproximation::FAPUnit & u2) const; + int distSquared(const FastAPproximation::FAPUnit & u1, const FastAPproximation::FAPUnit & u2) const; bool isSuicideUnit(BWAPI::UnitType ut); void unitsim(const FAPUnit & fu, std::vector &enemyUnits); void medicsim(const FAPUnit & fu, std::vector &friendlyUnits); diff --git a/Steamhammer/Source/GameCommander.cpp b/Steamhammer/Source/GameCommander.cpp index 6336454..f456e59 100644 --- a/Steamhammer/Source/GameCommander.cpp +++ b/Steamhammer/Source/GameCommander.cpp @@ -45,6 +45,7 @@ void GameCommander::update() _timerManager.stopTimer(TimerManager::InformationManager); _timerManager.startTimer(TimerManager::MapGrid); + the.update(); MapGrid::Instance().update(); _timerManager.stopTimer(TimerManager::MapGrid); @@ -284,6 +285,16 @@ void GameCommander::setValidUnits() if (UnitUtil::IsValidUnit(unit)) { _validUnits.insert(unit); + + /* + // TODO testing + static bool firstTime = false; + if (!firstTime && unit->getType() == BWAPI::UnitTypes::Zerg_Mutalisk) + { + BWAPI::Broodwar->printf("mutalisk timing %d", BWAPI::Broodwar->getFrameCount()); + firstTime = true; + } + */ } } } diff --git a/Steamhammer/Source/Grid.cpp b/Steamhammer/Source/Grid.cpp index aba0790..15f2d8d 100644 --- a/Steamhammer/Source/Grid.cpp +++ b/Steamhammer/Source/Grid.cpp @@ -44,3 +44,23 @@ int Grid::at(const BWAPI::Position & pos) const { return at(BWAPI::TilePosition(pos)); } + + +// Draw a number in each tile. +// This default method is overridden in some subclasses. +void Grid::draw() const +{ + for (int x = 0; x < width; ++x) + { + for (int y = 0; y < height; ++y) + { + int n = grid[x][y]; + if (n) + { + BWAPI::Broodwar->drawTextMap( + BWAPI::Position(x * 32 + 8, y * 32 + 8), + "%c%d", yellow, n); + } + } + } +} \ No newline at end of file diff --git a/Steamhammer/Source/Grid.h b/Steamhammer/Source/Grid.h index 3059e13..bc50252 100644 --- a/Steamhammer/Source/Grid.h +++ b/Steamhammer/Source/Grid.h @@ -26,5 +26,7 @@ class Grid virtual int at(const BWAPI::TilePosition & pos) const; virtual int at(const BWAPI::WalkPosition & pos) const; virtual int at(const BWAPI::Position & pos) const; + + virtual void draw() const; }; } diff --git a/Steamhammer/Source/GridAttacks.cpp b/Steamhammer/Source/GridAttacks.cpp index 40ef440..7d5075b 100644 --- a/Steamhammer/Source/GridAttacks.cpp +++ b/Steamhammer/Source/GridAttacks.cpp @@ -1,77 +1,101 @@ -#include "Gridattacks.h" +#include "GridAttacks.h" #include "InformationManager.h" #include "UnitUtil.h" // NOTE -// This class is unused and untested! Test carefully before use. +// This class is untested! Test carefully before use. using namespace UAlbertaBot; -void Gridattacks::computeAir(const std::map & unitsInfo) +GroundAttacks::GroundAttacks() + : GridAttacks(false) +{ +} + +AirAttacks::AirAttacks() + : GridAttacks(true) +{ +} + +// Count 1 for each tile in range of the given enemy position. +void GridAttacks::addTilesInRange(const BWAPI::Position & enemyPosition, int range) +{ + // Find a bounding box that all affected tiles fit within. + BWAPI::Position topLeft(enemyPosition.x - range - 1, enemyPosition.y - range - 1); + BWAPI::Position bottomRight(enemyPosition.x + range + 1, enemyPosition.y + range + 1); + BWAPI::TilePosition topLeftTile(topLeft); + BWAPI::TilePosition bottomRightTile(bottomRight); + + // Find the tiles inside the bounding box which are in range. + // Be conservative: If the corner nearest the enemy is in range, the tile is in range. + // The 32 is for converting from tiles to pixels. + for (int x = std::max(0, topLeftTile.x); x <= std::min(width-1, bottomRightTile.x); ++x) + { + int nearestX = 32 * ((32 * x + 31 < enemyPosition.x) ? x + 1 : x); + for (int y = std::max(0, topLeftTile.y); y <= std::min(height-1, bottomRightTile.y); ++y) + { + int nearestY = 32 * ((32 * y + 31 <= enemyPosition.y) ? y + 1 : y); + if (BWAPI::Position(nearestX, nearestY).getApproxDistance(enemyPosition) <= range) + { + grid[x][y] += 1; + } + } + } +} + +void GridAttacks::computeAir(const std::map & unitsInfo) { for (const auto & kv : unitsInfo) { const auto & ui = kv.second; - if (ui.type.isBuilding() && UnitUtil::TypeCanAttackAir(ui.type)) + if (ui.type.isBuilding() && + UnitUtil::TypeCanAttackAir(ui.type) && + ui.unit && + (!ui.unit->isVisible() || ui.unit->isCompleted() && ui.unit->isPowered())) { int airRange = UnitUtil::GetAttackRangeAssumingUpgrades(ui.type, BWAPI::UnitTypes::Terran_Wraith); - BWAPI::Position topLeft(ui.lastPosition.x - airRange, ui.lastPosition.y - airRange); - BWAPI::Position bottomRight(ui.lastPosition.x + airRange, ui.lastPosition.y + airRange); - BWAPI::TilePosition topLeftTile(topLeft); - BWAPI::TilePosition bottomRightTile(bottomRight); - - // NOTE To save work, we assume the attack pattern is a square. - for (int x = topLeftTile.x; x <= bottomRightTile.y; ++x) - { - for (int y = topLeftTile.y; y <= bottomRightTile.y; ++y) - { - grid[x][y] += 1; - } - } + addTilesInRange(ui.lastPosition, airRange); } } } -void Gridattacks::computeGround(const std::map & unitsInfo) +void GridAttacks::computeGround(const std::map & unitsInfo) { for (const auto & kv : unitsInfo) { const auto & ui = kv.second; - if (ui.type.isBuilding() && UnitUtil::TypeCanAttackGround(ui.type)) + if (ui.type.isBuilding() && + UnitUtil::TypeCanAttackGround(ui.type) && + ui.unit && + (!ui.unit->isVisible() || ui.unit->isCompleted() && ui.unit->isPowered())) { int groundRange = UnitUtil::GetAttackRangeAssumingUpgrades(ui.type, BWAPI::UnitTypes::Terran_Marine); - BWAPI::Position topLeft(ui.lastPosition.x - groundRange, ui.lastPosition.y - groundRange); - BWAPI::Position bottomRight(ui.lastPosition.x + groundRange, ui.lastPosition.y + groundRange); - BWAPI::TilePosition topLeftTile(topLeft); - BWAPI::TilePosition bottomRightTile(bottomRight); - - // NOTE To save work, we assume the attack pattern is a square. - for (int x = topLeftTile.x; x <= bottomRightTile.y; ++x) - { - for (int y = topLeftTile.y; y <= bottomRightTile.y; ++y) - { - grid[x][y] += 1; - } - } + addTilesInRange(ui.lastPosition, groundRange); } } } -Gridattacks::Gridattacks(bool air) - : Grid() +GridAttacks::GridAttacks(bool air) + : Grid(BWAPI::Broodwar->mapWidth(), BWAPI::Broodwar->mapHeight(), 0) , versusAir(air) { } // Initialize with attacks by the enemy, against either air or ground units. -void Gridattacks::update() +void GridAttacks::update() { - // TODO erase to 0 + // Zero out the grid. + for (int x = 0; x < width; ++x) + { + std::fill(grid[x].begin(), grid[x].end(), 0); + } - const std::map & unitsInfo = InformationManager::Instance().getUnitData(BWAPI::Broodwar->enemy()).getUnits(); + // Fill in the grid. + const std::map & unitsInfo = + InformationManager::Instance().getUnitData(BWAPI::Broodwar->enemy()).getUnits(); if (versusAir) { computeAir(unitsInfo); @@ -81,3 +105,53 @@ void Gridattacks::update() computeGround(unitsInfo); } } + +bool GridAttacks::inRange(const BWAPI::TilePosition & pos) const +{ + return pos.isValid() && grid[pos.x][pos.y]; +} + +bool GridAttacks::inRange(const BWAPI::TilePosition & topLeft, const BWAPI::TilePosition & bottomRight) const +{ + UAB_ASSERT(topLeft.isValid() && bottomRight.isValid(), "bad rectangle"); + + if (grid[topLeft.x][topLeft.y]) + { + return true; + } + + // If the rectangle covers more than one tile, check each corner. + if (topLeft != bottomRight) + { + if (grid[bottomRight.x][bottomRight.y] || + grid[topLeft.x][bottomRight.y] || + grid[bottomRight.x][topLeft.y]) + { + return true; + } + } + return false; +} + +// For placing buildings. +bool GridAttacks::inRange(BWAPI::UnitType buildingType, const BWAPI::TilePosition & topLeftTile) const +{ + UAB_ASSERT(buildingType.isBuilding(), "bad type"); + + BWAPI::TilePosition bottomRightTile( + topLeftTile.x + buildingType.tileWidth() - 1, + topLeftTile.y + buildingType.tileHeight() - 1); + + return inRange(topLeftTile, bottomRightTile); +} + +// For checking the safety of a unit. +bool GridAttacks::inRange(BWAPI::Unit unit) const +{ + UAB_ASSERT(unit && unit->isVisible(), "bad unit"); + + BWAPI::TilePosition topLeftTile(BWAPI::Position(unit->getLeft(), unit->getTop())); + BWAPI::TilePosition bottomRightTile(BWAPI::Position(unit->getRight(), unit->getBottom())); + + return inRange(topLeftTile, bottomRightTile); +} \ No newline at end of file diff --git a/Steamhammer/Source/GridAttacks.h b/Steamhammer/Source/GridAttacks.h index 8ce2be5..3b6c924 100644 --- a/Steamhammer/Source/GridAttacks.h +++ b/Steamhammer/Source/GridAttacks.h @@ -1,6 +1,5 @@ #pragma once -#include #include "BWAPI.h" #include "Grid.h" #include "UnitData.h" @@ -10,17 +9,37 @@ namespace UAlbertaBot { -class Gridattacks : public Grid +class GridAttacks : public Grid { const bool versusAir; + void addTilesInRange(const BWAPI::Position & enemy, int range); + void computeAir(const std::map & unitsInfo); void computeGround(const std::map & unitsInfo); public: - Gridattacks(bool air); + GridAttacks(bool air); void update(); + + bool inRange(const BWAPI::TilePosition & pos) const; + bool inRange(const BWAPI::TilePosition & topLeft, const BWAPI::TilePosition & bottomRight) const; + bool inRange(BWAPI::UnitType buildingType, const BWAPI::TilePosition & topLeftTile) const; + bool inRange(BWAPI::Unit unit) const; +}; + +class GroundAttacks : public GridAttacks +{ +public: + GroundAttacks(); }; + +class AirAttacks : public GridAttacks +{ +public: + AirAttacks(); +}; + } \ No newline at end of file diff --git a/Steamhammer/Source/InformationManager.cpp b/Steamhammer/Source/InformationManager.cpp index 2012e81..1f0cd6f 100644 --- a/Steamhammer/Source/InformationManager.cpp +++ b/Steamhammer/Source/InformationManager.cpp @@ -331,7 +331,7 @@ void InformationManager::drawExtendedInterface() } // draw neutral units and our units - for (const auto & unit : BWAPI::Broodwar->getAllUnits()) + for (BWAPI::Unit unit : BWAPI::Broodwar->getAllUnits()) { if (unit->getPlayer() == _enemy) { @@ -421,11 +421,8 @@ void InformationManager::drawUnitInformation(int x, int y) return; } - char color = white; - BWAPI::Broodwar->drawTextScreen(x, y-10, "\x03 Self Loss:\x04 Minerals: \x1f%d \x04Gas: \x07%d", _unitData[_self].getMineralsLost(), _unitData[_self].getGasLost()); BWAPI::Broodwar->drawTextScreen(x, y, "\x03 Enemy Loss:\x04 Minerals: \x1f%d \x04Gas: \x07%d", _unitData[_enemy].getMineralsLost(), _unitData[_enemy].getGasLost()); - BWAPI::Broodwar->drawTextScreen(x, y+10, "\x04 Enemy: %s", _enemy->getName().c_str()); BWAPI::Broodwar->drawTextScreen(x, y+20, "\x04 UNIT NAME"); BWAPI::Broodwar->drawTextScreen(x+140, y+20, "\x04#"); BWAPI::Broodwar->drawTextScreen(x+160, y+20, "\x04X"); @@ -438,18 +435,30 @@ void InformationManager::drawUnitInformation(int x, int y) int numUnits = _unitData[_enemy].getNumUnits(t); int numDeadUnits = _unitData[_enemy].getNumDeadUnits(t); - if (numUnits > 0) + if (numUnits || numDeadUnits) { - if (t.isDetector()) { color = purple; } + char color = white; + + if (t.isDetector()) { color = purple; } else if (t.canAttack()) { color = red; } else if (t.isBuilding()) { color = yellow; } - else { color = white; } BWAPI::Broodwar->drawTextScreen(x, y+40+((yspace)*10), " %c%s", color, t.getName().c_str()); BWAPI::Broodwar->drawTextScreen(x+140, y+40+((yspace)*10), "%c%d", color, numUnits); BWAPI::Broodwar->drawTextScreen(x+160, y+40+((yspace++)*10), "%c%d", color, numDeadUnits); } } + + for (const auto & kv : getUnitData(_enemy).getUnits()) + { + const UnitInfo & ui(kv.second); + + if (ui.type.isBuilding()) + { + char color = ui.completed ? cyan : orange; + BWAPI::Broodwar->drawTextMap(ui.lastPosition.x, ui.lastPosition.y - 20, "%c%d", color, ui.completeBy); + } + } } void InformationManager::maybeClearNeutral(BWAPI::Unit unit) @@ -541,7 +550,22 @@ void InformationManager::getNearbyForce(std::vector & unitsOut, BWAPI: int InformationManager::getNumUnits(BWAPI::UnitType t, BWAPI::Player player) const { - return getUnitData(player).getNumUnits(t); + int count = 0; + + for (const auto & kv : getUnitData(player).getUnits()) + { + const UnitInfo & ui(kv.second); + + if (t == ui.type) + { + ++count; + } + } + + return count; + + // Buggy! The original method can be extremely wrong, even giving negative counts. + // return getUnitData(player).getNumUnits(t); } // We have complated combat units (excluding workers). @@ -884,6 +908,25 @@ void InformationManager::enemySeenBurrowing() _enemyCloakedUnitsSeen = true; } +// Look up when an enemy building finished, or is predicted to finish. +// If none, give a time far in the future. +// This is for checking the timing of enemy tech buildings. +int InformationManager::getEnemyBuildingTiming(BWAPI::UnitType type) const +{ + for (const auto & kv : getUnitData(_enemy).getUnits()) + { + const UnitInfo & ui(kv.second); + + if (ui.type == type) + { + return ui.completeBy; + } + } + + // "Infinite" time in the future. + return 999999; +} + // Enemy has spore colonies, photon cannons, turrets, or spider mines. // It's the same as enemyHasStaticAntiAir() except for spider mines. // Spider mines only catch cloaked ground units, so this routine is not for countering wraiths. @@ -969,11 +1012,11 @@ bool InformationManager::enemyHasSiegeMode() const UnitInfo & ui(kv.second); // If the tank is in the process of sieging, it is still in tank mode. - // If it is unsieging, it is still in siege mode. + // If it is unsieging, it is still in siege mode. So this condition catches everything. if (ui.type == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode || ui.unit->isVisible() && ui.unit->getOrder() == BWAPI::Orders::Sieging) { - _enemyHasStaticAntiAir = true; + _enemyHasSiegeMode = true; return true; } } diff --git a/Steamhammer/Source/InformationManager.h b/Steamhammer/Source/InformationManager.h index 706c0ce..758b91b 100644 --- a/Steamhammer/Source/InformationManager.h +++ b/Steamhammer/Source/InformationManager.h @@ -93,6 +93,7 @@ class InformationManager bool enemyHasSiegeMode(); void enemySeenBurrowing(); + int getEnemyBuildingTiming(BWAPI::UnitType type) const; const BWAPI::Unitset & getStaticDefense() const { return _staticDefense; }; const BWAPI::Unitset & getOurPylons() const { return _ourPylons; }; diff --git a/Steamhammer/Source/MacroAct.cpp b/Steamhammer/Source/MacroAct.cpp index 6459ef6..bd664c7 100644 --- a/Steamhammer/Source/MacroAct.cpp +++ b/Steamhammer/Source/MacroAct.cpp @@ -217,7 +217,7 @@ MacroAct::MacroAct(MacroCommandType t, int amount) { } -const size_t & MacroAct::type() const +size_t MacroAct::type() const { return _type; } @@ -247,7 +247,7 @@ bool MacroAct::isCommand() const return _type == MacroActs::Command; } -const BWAPI::Race & MacroAct::getRace() const +BWAPI::Race MacroAct::getRace() const { return _race; } @@ -281,36 +281,38 @@ bool MacroAct::isSupply() const || _unitType == BWAPI::UnitTypes::Zerg_Overlord); } -const BWAPI::UnitType & MacroAct::getUnitType() const +BWAPI::UnitType MacroAct::getUnitType() const { UAB_ASSERT(_type == MacroActs::Unit, "getUnitType of non-unit"); return _unitType; } -const BWAPI::TechType & MacroAct::getTechType() const +BWAPI::TechType MacroAct::getTechType() const { UAB_ASSERT(_type == MacroActs::Tech, "getTechType of non-tech"); return _techType; } -const BWAPI::UpgradeType & MacroAct::getUpgradeType() const +BWAPI::UpgradeType MacroAct::getUpgradeType() const { UAB_ASSERT(_type == MacroActs::Upgrade, "getUpgradeType of non-upgrade"); return _upgradeType; } -const MacroCommand MacroAct::getCommandType() const +MacroCommand MacroAct::getCommandType() const { UAB_ASSERT(_type == MacroActs::Command, "getCommandType of non-command"); return _macroCommandType; } -const MacroLocation MacroAct::getMacroLocation() const +MacroLocation MacroAct::getMacroLocation() const { return _macroLocation; } // Supply required if this is produced. +// It is NOT THE SAME as the supply required to have one of the units; it is the extra supply needed +// to make one of them. int MacroAct::supplyRequired() const { if (isUnit()) diff --git a/Steamhammer/Source/MacroAct.h b/Steamhammer/Source/MacroAct.h index fb68433..8f35720 100644 --- a/Steamhammer/Source/MacroAct.h +++ b/Steamhammer/Source/MacroAct.h @@ -59,14 +59,14 @@ class MacroAct bool isRefinery() const; bool isSupply() const; - const size_t & type() const; - const BWAPI::Race & getRace() const; + size_t type() const; + BWAPI::Race getRace() const; - const BWAPI::UnitType & getUnitType() const; - const BWAPI::TechType & getTechType() const; - const BWAPI::UpgradeType & getUpgradeType() const; - const MacroCommand getCommandType() const; - const MacroLocation getMacroLocation() const; + BWAPI::UnitType getUnitType() const; + BWAPI::TechType getTechType() const; + BWAPI::UpgradeType getUpgradeType() const; + MacroCommand getCommandType() const; + MacroLocation getMacroLocation() const; int supplyRequired() const; int mineralPrice() const; diff --git a/Steamhammer/Source/MapTools.cpp b/Steamhammer/Source/MapTools.cpp index b068842..38869fe 100644 --- a/Steamhammer/Source/MapTools.cpp +++ b/Steamhammer/Source/MapTools.cpp @@ -116,12 +116,10 @@ void MapTools::setBWAPIMapData() { for (int dy = -3; dy <= 3; dy++) { - if (!BWAPI::TilePosition(x + dx, y + dy).isValid()) + if (BWAPI::TilePosition(x + dx, y + dy).isValid()) { - continue; - } - - _depotBuildable[x + dx][y + dy] = false; + _depotBuildable[x + dx][y + dy] = false; + } } } } @@ -317,6 +315,12 @@ Base * MapTools::nextExpansion(bool hidden, bool wantMinerals, bool wantGas) continue; } + // Don't take a base if the building location is known to be in range of enemy static defense. + if (the.groundAttacks.inRange(player->getRace().getCenter(), tile)) + { + continue; + } + double score = 0.0; // NOTE Ground distances are computed at tile resolution, which is coarser than the walking @@ -384,7 +388,7 @@ Base * MapTools::nextExpansion(bool hidden, bool wantMinerals, bool wantGas) score += 0.02 * base->getInitialGas(); } - /* TODO our map analysis does not provide regions (yet) + /* TODO on a flat map, all mains may be in the same zone // Big penalty for enemy buildings in the same region. if (InformationManager::Instance().isEnemyBuildingInRegion(base->getRegion())) { diff --git a/Steamhammer/Source/Micro.cpp b/Steamhammer/Source/Micro.cpp index ef95a6d..55a27ec 100644 --- a/Steamhammer/Source/Micro.cpp +++ b/Steamhammer/Source/Micro.cpp @@ -1,5 +1,6 @@ #include "Micro.h" +#include "GridDistances.h" #include "InformationManager.h" #include "MapGrid.h" #include "The.h" @@ -13,11 +14,16 @@ size_t TotalCommands = 0; // not all commands are counted // Complain if there is an "obvious" problem. // So far, the only problem is issuing two orders during the same frame. -void MicroState::check() +void MicroState::check(BWAPI::Unit u, BWAPI::Order o) const { - // TODO this likely bug occurs often but has no known bad effects - // save it to track down later - //UAB_ASSERT(orderFrame < BWAPI::Broodwar->getFrameCount(), ">1 order this frame"); + return; // TODO only check when debugging + + if (orderFrame == BWAPI::Broodwar->getFrameCount()) + { + BWAPI::Broodwar->printf(">1 order for %s %d frame %d, %s -> %s", + UnitTypeName(u).c_str(), u->getID(), BWAPI::Broodwar->getFrameCount(), + order.getName().c_str(), o.getName().c_str()); + } } // Execute the unit's order. @@ -25,10 +31,14 @@ void MicroState::execute(BWAPI::Unit u) { if (order == BWAPI::Orders::Move) { - if (u->getPosition() != targetPosition && u->move(targetPosition)) + if (u->getPosition() != targetPosition && BWAPI::Broodwar->getFrameCount() >= lastActionFrame + framesBetweenActions) { - lastCheckFrame = executeFrame = BWAPI::Broodwar->getFrameCount(); - needsMonitoring = true; + if (u->move(getNextMovePosition(u))) + { + lastCheckFrame = executeFrame = BWAPI::Broodwar->getFrameCount(); + needsMonitoring = true; + } + lastActionFrame = executeFrame; } } else @@ -37,7 +47,6 @@ void MicroState::execute(BWAPI::Unit u) executeFrame = 0; needsMonitoring = false; } - // TODO other commands are not implemented yet } // Monitor the order: Check for and try to correct failure to execute. @@ -45,16 +54,16 @@ void MicroState::monitor(BWAPI::Unit u) { if (order == BWAPI::Orders::Move) { - if (u->getPosition() == targetPosition) + if (u->isFlying()) + { + // Flying units do not have problems. + needsMonitoring = false; + } + else if (u->getPosition() == targetPosition) { // We're there. All done. needsMonitoring = false; } - else if (u->isFlying()) - { - // Flying units do not have problems. - needsMonitoring = false; - } // Some ways to fail: // 1. The command might not have been accepted despite the return value. else if (u->getOrder() != BWAPI::Orders::Move || @@ -70,7 +79,8 @@ void MicroState::monitor(BWAPI::Unit u) */ u->move(targetPosition); lastCheckFrame = BWAPI::Broodwar->getFrameCount(); - } + lastActionFrame = lastCheckFrame; + } // 2. The unit could be "stuck", moving randomly to escape overlapping with other units. else if (u->isStuck()) { @@ -85,42 +95,64 @@ void MicroState::monitor(BWAPI::Unit u) // BWAPI::Broodwar->printf("moving unit %d froze velocity %g,%g", u->getID(), u->getVelocityX(), u->getVelocityY()); u->stop(); lastCheckFrame = BWAPI::Broodwar->getFrameCount(); - // On the next retry, the order will not be Move and the order will be reissued. + lastActionFrame = lastCheckFrame; + // On the next retry, the order will not be Move and the Move order will be reissued. } // 4. The unit could be blocked and unable to make progress. UNIMPLEMENTED + // OTHERWISE it may be time to switch to the next waypoint. + } else { - // It's an order we don't support. Set the retry time to infinity to save effort. + // It's an order we don't support. Turn off monitoring to save effort. needsMonitoring = false; } // TODO other commands are not implemented yet } +// If we're moving the unit a long distance, use pathfinding to avoid blocked paths. +BWAPI::Position MicroState::getNextMovePosition(BWAPI::Unit u) +{ + /* + // Flying units don't need ground pathfinding. + // Neither do units within one step of the target position. + if (u->isFlying() || + u->getDistance(targetPosition) <= 32 * distanceStep) + { + return targetPosition; + } + */ + + // TODO pathfinding not implemented + return targetPosition; +} + // -- -- // Create a blank MicroState. Values will be filled in later. MicroState::MicroState() - : orderFrame(-1) - , executeFrame(-1) - , needsMonitoring(false) - , lastCheckFrame(-1) - , order(BWAPI::Orders::None) - , targetUnit(nullptr) - , targetPosition(BWAPI::Positions::None) - , startPosition(BWAPI::Positions::None) + : order(BWAPI::Orders::None) + , targetUnit(nullptr) + , targetPosition(BWAPI::Positions::None) + , distanceToPosition(nullptr) + , orderFrame(-1) + , executeFrame(-1) + , needsMonitoring(false) + , lastCheckFrame(-1) + , lastActionFrame(-framesBetweenActions) // an action at time 0 must execute + , startPosition(BWAPI::Positions::None) { } // No-argument order. void MicroState::setOrder(BWAPI::Unit u, BWAPI::Order o) { - check(); + check(u, o); if (order != o) { order = o; - targetPosition = BWAPI::Positions::None; + targetPosition = BWAPI::Positions::None; targetUnit = nullptr; orderFrame = BWAPI::Broodwar->getFrameCount(); @@ -133,36 +165,37 @@ void MicroState::setOrder(BWAPI::Unit u, BWAPI::Order o) // Order that targets a unit. void MicroState::setOrder(BWAPI::Unit u, BWAPI::Order o, BWAPI::Unit t) { - check(); + check(u, o); - if (order != o || targetUnit != t) { + if (order != o || targetUnit != t) + { order = o; - targetPosition = BWAPI::Positions::None; - targetUnit = t; + targetPosition = BWAPI::Positions::None; + targetUnit = t; orderFrame = BWAPI::Broodwar->getFrameCount(); executeFrame = -1; startPosition = u->getPosition(); - } + } } // Order that targets a position. void MicroState::setOrder(BWAPI::Unit u, BWAPI::Order o, BWAPI::Position p) { - check(); + check(u, o); if (order != o || targetPosition != p) { order = o; - targetPosition = p; + targetPosition = p; targetUnit = nullptr; orderFrame = BWAPI::Broodwar->getFrameCount(); executeFrame = -1; startPosition = u->getPosition(); - } + } } void MicroState::update(BWAPI::Unit u) @@ -258,6 +291,19 @@ Micro::Micro() { } +// Was the unit already given a command this frame? +bool Micro::alreadyCommanded(BWAPI::Unit unit) const +{ + auto it = orders.find(unit); + + if (it == orders.end()) + { + return false; + } + + return (*it).second.getOrderFrame() >= BWAPI::Broodwar->getFrameCount(); +} + // If our ground unit is next to an undetected dark templar, run it away and return true. // Otherwise return false. bool Micro::fleeDT(BWAPI::Unit unit) @@ -278,12 +324,12 @@ bool Micro::fleeDT(BWAPI::Unit unit) 64); if (dt) { - BWAPI::Position fleePosition = DistanceAndDirection(unit->getPosition(), dt->getPosition(), -96); + BWAPI::Position fleePosition = RawDistanceAndDirection(unit->getPosition(), dt->getPosition(), -96); if (fleePosition.isValid()) { /* BWAPI::Broodwar->printf("%s flees dt dist=%d, fleedist=%d", - UnitTypeName(unit->getType()).c_str(), + UnitTypeName(unit).c_str(), unit->getDistance(dt), unit->getDistance(fleePosition)); BWAPI::Broodwar->drawLineMap(unit->getPosition(), fleePosition, BWAPI::Colors::Yellow); @@ -481,7 +527,7 @@ void Micro::Move(BWAPI::Unit attacker, const BWAPI::Position & targetPosition) if (!attacker->exists()) { UAB_ASSERT(false, "SmartMove: nonexistent"); - BWAPI::Broodwar->printf("SM: non-exist %s @ %d, %d", attacker->getType().getName().c_str(), targetPosition.x, targetPosition.y); + BWAPI::Broodwar->`("SM: non-exist %s @ %d, %d", attacker->getType().getName().c_str(), targetPosition.x, targetPosition.y); return; } if (attacker->getPlayer() != BWAPI::Broodwar->self()) @@ -547,25 +593,27 @@ void Micro::Move(BWAPI::Unit attacker, const BWAPI::Position & targetPosition) // Suitable for approaching a moving target that is distant, or slow, or needs little accuracy. // This reduces unnecessary orders. It's up to the caller to decide that they're unnecessary. // With fewer orders, units should be less likely to get stuck. -void Micro::MoveNear(BWAPI::Unit attacker, const BWAPI::Position & targetPosition) +void Micro::MoveNear(BWAPI::Unit unit, const BWAPI::Position & targetPosition) { - auto it = orders.find(attacker); + auto it = orders.find(unit); if (it == orders.end()) { // The unit doesn't have an existing order. - Move(attacker, targetPosition); + Move(unit, targetPosition); } - - MicroState & state = it->second; - - if (state.getOrder() != BWAPI::Orders::Move || - state.getOrderFrame() - BWAPI::Broodwar->getFrameCount() >= 12 || - state.getTargetPosition().getApproxDistance(targetPosition) > 2 * 32) - { - Move(attacker, targetPosition); - } - - // Otherwise do nothing. It's close enough. + else + { + // The unit has an existing order. Check it. + MicroState & state = it->second; + + if (state.getOrder() != BWAPI::Orders::Move || + BWAPI::Broodwar->getFrameCount() - state.getOrderFrame() >= 18 || + state.getTargetPosition().getApproxDistance(targetPosition) > 2 * 32) + { + Move(unit, targetPosition); + } + // Otherwise do nothing. It's close enough. + } } void Micro::RightClick(BWAPI::Unit unit, BWAPI::Unit target) @@ -1029,7 +1077,7 @@ bool Micro::LarvaTrick(const BWAPI::Unitset & larvas) } // Use a tech on a target unit. -// So far, we only support defiler tech. +// NOTE The order is set correctly only for techs that Steamhammer already implements. bool Micro::UseTech(BWAPI::Unit unit, BWAPI::TechType tech, BWAPI::Unit target) { if (!unit || !unit->exists() || !unit->getPosition().isValid() || unit->getPlayer() != BWAPI::Broodwar->self() || @@ -1039,16 +1087,27 @@ bool Micro::UseTech(BWAPI::Unit unit, BWAPI::TechType tech, BWAPI::Unit target) return false; } - UAB_ASSERT(tech == BWAPI::TechTypes::Consume, "unsupported tech"); - - // The Orders are Burrowing and Burrowed. Also lurkers can do stuff while burrowed. - orders[unit].setOrder(unit, BWAPI::Orders::CastConsume); + BWAPI::Order o; + if (tech == BWAPI::TechTypes::Consume) + { + o = BWAPI::Orders::CastConsume; + } + else if (tech == BWAPI::TechTypes::Parasite) + { + o = BWAPI::Orders::CastParasite; + } + else + { + o = BWAPI::Orders::UnusedNothing; + UAB_ASSERT(false, "unsupported tech"); + } + orders[unit].setOrder(unit, o); - return unit->useTech(BWAPI::TechTypes::Consume, target); + return unit->useTech(tech, target); } // Use a tech on a target position. -// So far, we only support defiler tech. +// NOTE The order is set correctly only for techs that Steamhammer already implements. bool Micro::UseTech(BWAPI::Unit unit, BWAPI::TechType tech, const BWAPI::Position & target) { if (!unit || !unit->exists() || !unit->getPosition().isValid() || unit->getPlayer() != BWAPI::Broodwar->self() || @@ -1058,18 +1117,21 @@ bool Micro::UseTech(BWAPI::Unit unit, BWAPI::TechType tech, const BWAPI::Positio return false; } + BWAPI::Order o; if (tech == BWAPI::TechTypes::Dark_Swarm) { - orders[unit].setOrder(unit, BWAPI::Orders::CastDarkSwarm); + o = BWAPI::Orders::CastDarkSwarm; } - else if (BWAPI::TechTypes::Plague) + else if (tech == BWAPI::TechTypes::Plague) { - orders[unit].setOrder(unit, BWAPI::Orders::CastPlague); + o = BWAPI::Orders::CastPlague; } else { + o = BWAPI::Orders::UnusedNothing; UAB_ASSERT(false, "unsupported tech"); } + orders[unit].setOrder(unit, o); return unit->useTech(tech, target); } diff --git a/Steamhammer/Source/Micro.h b/Steamhammer/Source/Micro.h index 640cc57..6db0d7d 100644 --- a/Steamhammer/Source/Micro.h +++ b/Steamhammer/Source/Micro.h @@ -6,6 +6,7 @@ namespace UAlbertaBot { class The; +class GridDistances; class MicroState { @@ -14,16 +15,25 @@ class MicroState BWAPI::Unit targetUnit; // nullptr if none BWAPI::Position targetPosition; // None if none + GridDistances * distanceToPosition; // sometimes filled in + int orderFrame; // when the order was given int executeFrame; // -1 if not executed yet bool needsMonitoring; // if true, monitor the result int lastCheckFrame; // execute frame or latest monitored frame + int lastActionFrame; // time of issuing last order to BWAPI, persists across setOrder() + + static const int framesBetweenActions = 3; + static const int distanceStep = 8; // travel long distances in steps of this size, in tiles - void check(); // complain if the order looks bad + // Debugging test: Complain if something looks bad. + void check(BWAPI::Unit u, BWAPI::Order o) const; void execute(BWAPI::Unit u); // carry out the order void monitor(BWAPI::Unit u); // check for and try to correct failures + BWAPI::Position getNextMovePosition(BWAPI::Unit u); + public: BWAPI::Position startPosition; @@ -61,6 +71,8 @@ class Micro // Call this at the end of the frame to execute any orders stored in the orders map. void update(); + bool alreadyCommanded(BWAPI::Unit unit) const; + bool fleeDT(BWAPI::Unit unit); void Stop(BWAPI::Unit unit); diff --git a/Steamhammer/Source/MicroAirToAir.cpp b/Steamhammer/Source/MicroAirToAir.cpp index 5bf76f6..6b76e94 100644 --- a/Steamhammer/Source/MicroAirToAir.cpp +++ b/Steamhammer/Source/MicroAirToAir.cpp @@ -32,7 +32,7 @@ void MicroAirToAir::assignTargets(const BWAPI::Unitset & airUnits, const BWAPI:: u->isFlying() && u->isVisible() && u->isDetected() && - !u->isStasised(); + !u->isInvincible(); }); for (const auto airUnit : airUnits) diff --git a/Steamhammer/Source/MicroDefilers.cpp b/Steamhammer/Source/MicroDefilers.cpp index fcae0c0..252e0b6 100644 --- a/Steamhammer/Source/MicroDefilers.cpp +++ b/Steamhammer/Source/MicroDefilers.cpp @@ -1,471 +1,507 @@ -#include "MicroDefilers.h" - -#include "Bases.h" -#include "The.h" -#include "UnitUtil.h" - -using namespace UAlbertaBot; - -// NOTE -// The computations to decide whether and where to swarm and plague are expensive. -// Don't have too many defilers at the same time, or you'll time out. - -// The defilers in this cluster. -// This will rarely return more than one defiler. -BWAPI::Unitset MicroDefilers::getDefilers(const UnitCluster & cluster) const -{ - BWAPI::Unitset defilers; - - for (BWAPI::Unit unit : getUnits()) - { - if (unit->getType() == BWAPI::UnitTypes::Zerg_Defiler && - cluster.units.contains(unit)) - { - defilers.insert(unit); - } - } - - return defilers; -} - -// The defiler is probably about to die. It should cast immediately if it is ever going to. -bool MicroDefilers::aboutToDie(const BWAPI::Unit defiler) const -{ - return - defiler->getHitPoints() < 30 || - defiler->isIrradiated() || - defiler->isUnderStorm() || - defiler->isPlagued(); -} - -// We need to consume and have it researched. Look around for food. -// For now, we consume zerglings. -// NOTE This doesn't take latency into account, so it issues the consume order -// repeatedly until the latency time has elapsed. It looks funny in game, -// but there don't seem to be any bad effects. -bool MicroDefilers::maybeConsume(BWAPI::Unit defiler, BWAPI::Unitset & food) -{ - // If there is a zergling in range, snarf it down. - // Consume has a range of 1 tile = 32 pixels. - for (BWAPI::Unit zergling : food) - { - if (defiler->getDistance(zergling) <= 32 && - defiler->canUseTechUnit(BWAPI::TechTypes::Consume, zergling)) - { - // BWAPI::Broodwar->printf("consume!"); - (void) the.micro.UseTech(defiler, BWAPI::TechTypes::Consume, zergling); - food.erase(zergling); - return true; - } - } - - return false; -} - -// The decision is made. Move closer if necessary, then swarm or plague. -bool MicroDefilers::swarmOrPlague(BWAPI::Unit defiler, BWAPI::TechType techType, BWAPI::Position target) const -{ - // Swarm and plague both have range 9. - if (defiler->getDistance(target) > 9 * 32) - { - // We're out of range. Move closer. - // BWAPI::Broodwar->printf("defiler moving in..."); - the.micro.Move(defiler, target); - return true; - } - else if (defiler->canUseTech(techType, target)) - { - // BWAPI::Broodwar->printf(techType == BWAPI::TechTypes::Dark_Swarm ? "SWARM!" : "PLAGUE!"); - return the.micro.UseTech(defiler, techType, target); - } - - return false; -} - -// This unit is nearby. How much does it affect the decision to cast swarm? -// Units that do damage under swarm get a positive score, others get zero. -int MicroDefilers::swarmScore(BWAPI::Unit u) const -{ - BWAPI::UnitType type = u->getType(); - - if (u->isUnderDarkSwarm()) - { - return -1; // try not to overlap swarms - } - if (type.isWorker()) - { - // Workers count as ranged units and cannot do damage under dark swarm. - return 0; - } - if (type.isBuilding()) - { - // Even if it is static defense, it cannot do damage under swarm - // (unless it is a bunker with firebats inside, a case that we ignore). - return 0; - } - if (type == BWAPI::UnitTypes::Protoss_High_Templar || - type == BWAPI::UnitTypes::Protoss_Reaver || - type == BWAPI::UnitTypes::Zerg_Lurker) - { - // Special cases. - return type.supplyRequired(); - } - if (type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) - { - return 1; // it doesn't take supply - } - if (type == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode) - { - return 2; // it does splash damage only - } - if (type == BWAPI::UnitTypes::Protoss_Archon) - { - return 4; // it does splash damage only - } - if (type.groundWeapon() == BWAPI::WeaponTypes::None) - { - // This includes reavers, so they have to be counted first. - return 0; - } - if (type.groundWeapon().maxRange() <= 32) - { - // It's a melee attacker. - return type.supplyRequired(); - } - - // Remaining units are ranged units that cannot do damage under swarm. - // We also ignore spellcasters that could do damage but probably won't, like - // queens and battlecruisers. - return 0; -} - -// We can cast dark swarm. Do it if it makes sense. -// There are a lot of cases when we might want to swarm. For example: -// - Swarm defensively if the enemy has air units and we have ground units. -// - Swarm if we have melee units and the enemy has ranged units. -// - Swarm offensively to take down cannons/bunkers/sunkens. -// So far, we only implement a simple case: We're attacking toward enemy -// buildings, and the enemy is short of units that can cause damage under swarm. -// The buildings guarantee that the enemy can't simply run away without further -// consequence. - -// Score units, pick the ones with higher scores and try to swarm there. -// Swarm has a range of 9 and covers a 6x6 area (according to Liquipedia) or 5x5 (according to BWAPI). -bool MicroDefilers::maybeSwarm(BWAPI::Unit defiler) -{ - // Plague has range 9 and affects a 6x6 box. We look a little beyond that range for targets. - const int limit = 14; - - const bool dying = aboutToDie(defiler); - - // Usually, swarm only if there is an enemy building to cover. - // If the defiler is about to die, swarm may still be worth it even if it covers nothing. - if (!dying && - BWAPI::Broodwar->getUnitsInRadius(defiler->getPosition(), limit * 32, BWAPI::Filter::IsEnemy && BWAPI::Filter::IsBuilding).empty()) - { - return false; - } - - // Look for the box with the best effect. - // NOTE This is not really the calculation we want. Better would be to find the box - // that nullifies the most enemy fire where we want to attack, no matter where the fire is from. - int bestScore = 0; - BWAPI::Position bestPlace = defiler->getPosition(); - for (int tileX = std::max(3, defiler->getTilePosition().x - limit); tileX <= std::min(BWAPI::Broodwar->mapWidth() - 4, defiler->getTilePosition().x + limit); ++tileX) - { - for (int tileY = std::max(3, defiler->getTilePosition().y - limit); tileY <= std::min(BWAPI::Broodwar->mapHeight() - 4, defiler->getTilePosition().y + limit); ++tileY) - { - int score = 0; - bool hasEnemyBuilding = false; - bool hasRangedEnemy = false; - BWAPI::Position place(BWAPI::TilePosition(tileX, tileY)); - const BWAPI::Position offset(3 * 32, 3 * 32); - BWAPI::Unitset affected = BWAPI::Broodwar->getUnitsInRectangle(place - offset, place + offset); - for (BWAPI::Unit u : affected) - { - if (u->getPlayer() == BWAPI::Broodwar->self()) - { - score += swarmScore(u); - } - else if (u->getPlayer() == BWAPI::Broodwar->enemy()) - { - score -= swarmScore(u); - if (u->getType().isBuilding() && !u->getType().isAddon()) - { - hasEnemyBuilding = true; - score += 2; // enemy buildings under swarm are targets - } - if (u->getType().groundWeapon() != BWAPI::WeaponTypes::None && - u->getType().groundWeapon().maxRange() > 32) - { - hasRangedEnemy = true; - } - } - } - if (hasEnemyBuilding && hasRangedEnemy && score > bestScore) - { - bestScore = score; - bestPlace = place; - } - } - } - - if (bestScore > 0.0) - { - // BWAPI::Broodwar->printf("swarm score %d at %d,%d", bestScore, bestPlace.x, bestPlace.y); - } - - // NOTE If bestScore is 0, then bestPlace is the defiler's location (set above). - if (bestScore > 20 || dying) - { - return swarmOrPlague(defiler, BWAPI::TechTypes::Dark_Swarm, bestPlace); - } - - return false; -} - -// How valuable is it to plague this unit? -// The caller worries about whether it is our unit or the enemy's. -double MicroDefilers::plagueScore(BWAPI::Unit u) const -{ - if (u->isPlagued() || u->isBurrowed()) - { - return 0.0; - } - - // How many HP will it lose? Assume incorrectly that it can go down to 0 HP (it's simpler). - int endHP = std::max(0, u->getHitPoints() - 2400); - double score = double(u->getHitPoints() - endHP); - - // If it's cloaked, give it a bonus--a bigger bonus if it is not detected. - if (u->isVisible() && !u->isDetected()) - { - score = 100.0; // we don't know its type, so give it a base score (we may plague observers) - } - else if (u->isCloaked()) - { - score += 20.0; // because plague will keep it detected - } - // If it's a static defense building, give it a bonus. - else if (UnitUtil::IsStaticDefense(u->getType())) - { - score += 40.0; - } - // If it's a building other than static defense, give it a discount. - else if (u->getType().isBuilding()) - { - score = 0.3 * score; - } - - // If it's a carrier interceptor, give it a bonus. We like plague on interceptor. - else if (u->getType() == BWAPI::UnitTypes::Protoss_Interceptor) - { - score += 20.0; - } - - return std::pow(score, 0.8); -} - -// We can plague. Look around to see if we should, and if so, do it. -bool MicroDefilers::maybePlague(BWAPI::Unit defiler) -{ - // Plague has range 9 and affects a 4x4 box. We look a little beyond that range for targets. - const int limit = 12; - - // Unless the defiler is in trouble, don't bother to plague a small number of enemy units. - BWAPI::Unitset targets = BWAPI::Broodwar->getUnitsInRadius(defiler->getPosition(), limit * 32, BWAPI::Filter::IsEnemy); - - const bool dying = aboutToDie(defiler); - - if (targets.empty() || !dying && targets.size() < 5) - { - // So little enemy stuff that it's unlikely to be worth it. Bail. - return false; - } - - // Plague has range 9 and affects a box of size 4x4. - // Look for the box with the best effect. - double bestScore = 0.0; - BWAPI::Position bestPlace; - for (int tileX = std::max(2, defiler->getTilePosition().x-limit); tileX <= std::min(BWAPI::Broodwar->mapWidth()-3, defiler->getTilePosition().x+limit); ++tileX) - { - for (int tileY = std::max(2, defiler->getTilePosition().y - limit); tileY <= std::min(BWAPI::Broodwar->mapHeight()-3, defiler->getTilePosition().y+limit); ++tileY) - { - double score = 0.0; - BWAPI::Position place(BWAPI::TilePosition(tileX, tileY)); - const BWAPI::Position offset(2 * 32, 2 * 32); - BWAPI::Unitset affected = BWAPI::Broodwar->getUnitsInRectangle(place - offset, place + offset); - for (BWAPI::Unit u : affected) - { - if (u->getPlayer() == BWAPI::Broodwar->self()) - { - score -= plagueScore(u); - } - else if (u->getPlayer() == BWAPI::Broodwar->enemy()) - { - score += plagueScore(u); - } - } - if (score > bestScore) - { - bestScore = score; - bestPlace = place; - } - } - } - - if (bestScore > 0.0) - { - // BWAPI::Broodwar->printf("plague score %g at %d,%d", bestScore, bestPlace.x, bestPlace.y); - } - - if (bestScore > 200.0 || dying && bestScore > 0.0) - { - return swarmOrPlague(defiler, BWAPI::TechTypes::Plague, bestPlace); - } - - return false; -} - -MicroDefilers::MicroDefilers() -{ -} - -// Unused but required. -void MicroDefilers::executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) -{ -} - -// Consume for energy if possible and necessary; otherwise move. -void MicroDefilers::updateMovement(const UnitCluster & cluster, BWAPI::Unit vanguard) -{ - BWAPI::Unitset defilers = getDefilers(cluster); - if (defilers.empty()) - { - return; - } - - // Collect the food. - // The food may be far away, not in the current cluster. - BWAPI::Unitset food; - for (BWAPI::Unit unit : getUnits()) - { - if (unit->getType() != BWAPI::UnitTypes::Zerg_Defiler) - { - food.insert(unit); - } - } - - // Control the defilers. - for (BWAPI::Unit defiler : defilers) - { - bool canMove = true; - if (defiler->isBurrowed()) - { - canMove = false; - if (!defiler->isIrradiated() && defiler->canUnburrow()) - { - the.micro.Unburrow(defiler); - } - } - else if (defiler->isIrradiated() && defiler->getEnergy() < 90 && defiler->canBurrow()) - { - canMove = false; - the.micro.Burrow(defiler); - } - - if (canMove && defiler->getEnergy() < 150 && - BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Consume) && - !food.empty()) - { - canMove = !maybeConsume(defiler, food); - } - - if (canMove) - { - BWAPI::Position destination; - - // Figure out where the defiler should move to. - if (vanguard && defiler->getEnergy() >= 100) - { - destination = vanguard->getPosition(); - } - else - { - destination = cluster.center; - } - - if (destination.isValid()) - { - the.micro.Move(defiler, destination); - } - } - } - - // Control the surviving food--move it to the defiler's position. - for (BWAPI::Unit zergling : food) - { - // Find the nearest defiler with low energy and move toward it. - BWAPI::Unit bestDefiler = nullptr; - int bestDist = 999999; - for (BWAPI::Unit defiler : defilers) - { - if (defiler->getType() == BWAPI::UnitTypes::Zerg_Defiler && - defiler->getEnergy() < 150) - { - int dist = zergling->getDistance(defiler); - if (dist < bestDist) - { - bestDefiler = defiler; - bestDist = dist; - } - } - } - - if (bestDefiler && zergling->getDistance(bestDefiler) >= 32) - { - the.micro.Move(zergling, PredictMovement(bestDefiler, 8)); - } - } -} - -// Cast dark swarm if possible and useful. -void MicroDefilers::updateSwarm(const UnitCluster & cluster) -{ - BWAPI::Unitset defilers = getDefilers(cluster); - if (defilers.empty()) - { - return; - } - - for (BWAPI::Unit defiler : defilers) - { - if (!defiler->isBurrowed() && - defiler->getEnergy() >= 100 && - defiler->canUseTech(BWAPI::TechTypes::Dark_Swarm, defiler->getPosition())) - { - (void) maybeSwarm(defiler); - } - } -} - -// Cast plague if possible and useful. -void MicroDefilers::updatePlague(const UnitCluster & cluster) -{ - BWAPI::Unitset defilers = getDefilers(cluster); - if (defilers.empty()) - { - return; - } - - for (BWAPI::Unit defiler : defilers) - { - if (!defiler->isBurrowed() && - defiler->getEnergy() >= 150 && - BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Plague) && - defiler->canUseTech(BWAPI::TechTypes::Plague, defiler->getPosition())) - { - (void) maybePlague(defiler); - } - } -} +#include "MicroDefilers.h" + +#include "Bases.h" +#include "The.h" +#include "UnitUtil.h" + +using namespace UAlbertaBot; + +// NOTE +// The computations to decide whether and where to swarm and plague are expensive. +// Don't have too many defilers at the same time, or you'll time out. +// A natural fix would be to designate one defiler as the "active" defiler at a +// given time, and keep the others in reserve. + +// The defilers in this cluster. +// This will rarely return more than one defiler. +BWAPI::Unitset MicroDefilers::getDefilers(const UnitCluster & cluster) const +{ + BWAPI::Unitset defilers; + + for (BWAPI::Unit unit : getUnits()) + { + if (unit->getType() == BWAPI::UnitTypes::Zerg_Defiler && + cluster.units.contains(unit)) + { + defilers.insert(unit); + } + } + + return defilers; +} + +// The defiler is probably about to die. It should cast immediately if it is ever going to. +bool MicroDefilers::aboutToDie(const BWAPI::Unit defiler) const +{ + return + defiler->getHitPoints() < 30 || + defiler->isIrradiated() || + defiler->isUnderStorm() || + defiler->isPlagued(); +} + +// We need to consume and have it researched. Look around for food. +// For now, we consume zerglings. +// NOTE This doesn't take latency into account, so it issues the consume order +// repeatedly until the latency time has elapsed. It looks funny in game, +// but there don't seem to be any bad effects. +bool MicroDefilers::maybeConsume(BWAPI::Unit defiler, BWAPI::Unitset & food) +{ + // If there is a zergling in range, snarf it down. + // Consume has a range of 1 tile = 32 pixels. + for (BWAPI::Unit zergling : food) + { + if (defiler->getDistance(zergling) <= 32 && + defiler->canUseTechUnit(BWAPI::TechTypes::Consume, zergling)) + { + // BWAPI::Broodwar->printf("consume!"); + (void) the.micro.UseTech(defiler, BWAPI::TechTypes::Consume, zergling); + food.erase(zergling); + return true; + } + } + + return false; +} + +// This unit is nearby. How much does it affect the decision to cast swarm? +// Units that do damage under swarm get a positive score, others get zero. +int MicroDefilers::swarmScore(BWAPI::Unit u) const +{ + BWAPI::UnitType type = u->getType(); + + if (type.isWorker()) + { + // Workers cannot do damage under dark swarm. + return 0; + } + if (type.isBuilding()) + { + // Even if it is static defense, it cannot do damage under swarm + // (unless it is a bunker with firebats inside, a case that we ignore). + return 0; + } + if (type == BWAPI::UnitTypes::Zerg_Lurker) + { + // Lurkers are especially great under swarm. + return 2 * type.supplyRequired(); + } + if (type == BWAPI::UnitTypes::Protoss_High_Templar || + type == BWAPI::UnitTypes::Protoss_Reaver) + { + // Special cases. + return type.supplyRequired(); + } + if (type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) + { + return 1; // it doesn't take supply + } + if (type == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode) + { + return 2; // it does splash damage only + } + if (type == BWAPI::UnitTypes::Protoss_Archon) + { + return 4; // it does splash damage only + } + if (type.groundWeapon() == BWAPI::WeaponTypes::None) + { + // This includes reavers, so they have to be counted first. + return 0; + } + if (type.groundWeapon().maxRange() <= 32) + { + // It's a melee attacker. + return type.supplyRequired(); + } + + // Remaining units are ranged units that cannot do damage under swarm. + // We also ignore spellcasters that could do damage but probably won't, like + // queens and battlecruisers. + return 0; +} + +// We can cast dark swarm. Do it if it makes sense. +// There are a lot of cases when we might want to swarm. For example: +// - Swarm defensively if the enemy has air units and we have ground units. +// - Swarm if we have melee units and the enemy has ranged units. +// - Swarm offensively to take down cannons/bunkers/sunkens. +// So far, we only implement a simple case: We're attacking toward enemy +// buildings, and the enemy is short of units that can cause damage under swarm. +// The buildings guarantee that the enemy can't simply run away without further +// consequence. + +// Score units, pick the ones with higher scores and try to swarm there. +// Swarm has a range of 9 and covers a 6x6 area (according to Liquipedia) or 5x5 (according to BWAPI). +bool MicroDefilers::maybeSwarm(BWAPI::Unit defiler) +{ + // Plague has range 9 and affects a 6x6 box. We look a little beyond that range for targets. + const int limit = 14; + + const bool dying = aboutToDie(defiler); + + // Usually, swarm only if there is an enemy building to cover or ranged unit to neutralize. + // (The condition for ranged units is not comprehensive.) + // NOTE A carrier does not have a ground weapon, but its interceptors do. Swarm under interceptors. + // If the defiler is about to die, swarm may still be worth it even if it covers nothing. + if (!dying && + BWAPI::Broodwar->getUnitsInRadius(defiler->getPosition(), limit * 32, + BWAPI::Filter::IsEnemy && (BWAPI::Filter::IsBuilding || + BWAPI::Filter::IsFlyer && BWAPI::Filter::GroundWeapon != BWAPI::WeaponTypes::None || + BWAPI::Filter::GetType == BWAPI::UnitTypes::Terran_Marine || + BWAPI::Filter::GetType == BWAPI::UnitTypes::Protoss_Dragoon || + BWAPI::Filter::GetType == BWAPI::UnitTypes::Zerg_Hydralisk) + ).empty()) + { + return false; + } + + // Look for the box with the best effect. + // NOTE This is not really the calculation we want. Better would be to find the box + // that nullifies the most enemy fire where we want to attack, no matter where the fire is from. + int bestScore = 0; + BWAPI::Position bestPlace = defiler->getPosition(); + for (int tileX = std::max(3, defiler->getTilePosition().x - limit); tileX <= std::min(BWAPI::Broodwar->mapWidth() - 4, defiler->getTilePosition().x + limit); ++tileX) + { + for (int tileY = std::max(3, defiler->getTilePosition().y - limit); tileY <= std::min(BWAPI::Broodwar->mapHeight() - 4, defiler->getTilePosition().y + limit); ++tileY) + { + int score = 0; + bool hasRangedEnemy = false; + BWAPI::Position place(BWAPI::TilePosition(tileX, tileY)); + const BWAPI::Position offset(3 * 32, 3 * 32); + BWAPI::Unitset affected = BWAPI::Broodwar->getUnitsInRectangle(place - offset, place + offset); + for (BWAPI::Unit u : affected) + { + if (u->isUnderDarkSwarm()) + { + // Avoid overlapping swarms. + continue; + } + + if (u->getPlayer() == BWAPI::Broodwar->self()) + { + score += swarmScore(u); + } + else if (u->getPlayer() == BWAPI::Broodwar->enemy()) + { + score -= swarmScore(u); + if (u->getType().isBuilding() && !u->getType().isAddon()) + { + score += 2; // enemy buildings under swarm are targets + } + if (u->getType().groundWeapon() != BWAPI::WeaponTypes::None && + u->getType().groundWeapon().maxRange() > 32) + { + hasRangedEnemy = true; + } + } + } + if (hasRangedEnemy && score > bestScore) + { + bestScore = score; + bestPlace = place; + } + } + } + + if (bestScore > 0) + { + // BWAPI::Broodwar->printf("swarm score %d at %d,%d", bestScore, bestPlace.x, bestPlace.y); + } + + // NOTE If bestScore is 0, then bestPlace is the defiler's location (set above). + if (bestScore > 10 || dying) + { + setReadyToCast(defiler, CasterSpell::DarkSwarm); + return spell(defiler, BWAPI::TechTypes::Dark_Swarm, bestPlace); + } + + return false; +} + +// How valuable is it to plague this unit? +// The caller worries about whether it is our unit or the enemy's. +double MicroDefilers::plagueScore(BWAPI::Unit u) const +{ + if (u->isPlagued() || u->isBurrowed() || u->isInvincible()) + { + return 0.0; + } + + // How many HP will it lose? Assume incorrectly that it may go down to 1 HP (it's simpler). + int endHP = std::max(1, u->getHitPoints() - 2400); + double score = double(u->getHitPoints() - endHP); + + // If it's cloaked, give it a bonus--a bigger bonus if it is not detected. + if (u->isVisible() && !u->isDetected()) + { + score += 100.0; + } + else if (u->isCloaked()) + { + score += 40.0; // because plague will keep it detected + } + // If it's a static defense building, give it a bonus. + else if (UnitUtil::IsStaticDefense(u->getType())) + { + score += 50.0; + } + // If it's a building other than static defense, give it a discount. + else if (u->getType().isBuilding()) + { + score = 0.25 * score; + } + // If it's a carrier interceptor, give it a bonus. We like plague on interceptor. + else if (u->getType() == BWAPI::UnitTypes::Protoss_Interceptor) + { + score += 35.0; + } + + if (score == 0.0) + { + return 0.0; + } + + return std::pow(score, 0.8); +} + +// We can plague. Look around to see if we should, and if so, do it. +bool MicroDefilers::maybePlague(BWAPI::Unit defiler) +{ + // Plague has range 9 and affects a 4x4 box. We look a little beyond that range for targets. + const int limit = 12; + + // Unless the defiler is in trouble, don't bother to plague a small number of enemy units. + BWAPI::Unitset targets = BWAPI::Broodwar->getUnitsInRadius(defiler->getPosition(), limit * 32, BWAPI::Filter::IsEnemy); + + const bool dying = aboutToDie(defiler); + + if (targets.empty() || !dying && targets.size() < 4) + { + // So little enemy stuff that it's unlikely to be worth it. Bail. + return false; + } + + // Plague has range 9 and affects a box of size 4x4. + // Look for the box with the best effect. + double bestScore = 0.0; + BWAPI::Position bestPlace; + for (int tileX = std::max(2, defiler->getTilePosition().x-limit); tileX <= std::min(BWAPI::Broodwar->mapWidth()-3, defiler->getTilePosition().x+limit); ++tileX) + { + for (int tileY = std::max(2, defiler->getTilePosition().y - limit); tileY <= std::min(BWAPI::Broodwar->mapHeight()-3, defiler->getTilePosition().y+limit); ++tileY) + { + double score = 0.0; + BWAPI::Position place(BWAPI::TilePosition(tileX, tileY)); + const BWAPI::Position offset(2 * 32, 2 * 32); + BWAPI::Unitset affected = BWAPI::Broodwar->getUnitsInRectangle(place - offset, place + offset); + for (BWAPI::Unit u : affected) + { + if (u->getPlayer() == BWAPI::Broodwar->self()) + { + score -= plagueScore(u); + } + else if (u->getPlayer() == BWAPI::Broodwar->enemy()) + { + score += plagueScore(u); + } + } + if (score > bestScore) + { + bestScore = score; + bestPlace = place; + } + } + } + + if (bestScore > 0.0) + { + // BWAPI::Broodwar->printf("plague score %g at %d,%d", bestScore, bestPlace.x, bestPlace.y); + } + + if (bestScore > 100.0 || dying && bestScore > 0.0) + { + setReadyToCast(defiler, CasterSpell::Plague); + return spell(defiler, BWAPI::TechTypes::Plague, bestPlace); + } + + return false; +} + +MicroDefilers::MicroDefilers() +{ +} + +// Unused but required. +void MicroDefilers::executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) +{ +} + +// Consume for energy if possible and necessary; otherwise move. +void MicroDefilers::updateMovement(const UnitCluster & cluster, BWAPI::Unit vanguard) +{ + // Collect the defilers and update their states. + BWAPI::Unitset defilers = getDefilers(cluster); + if (defilers.empty()) + { + return; + } + updateCasters(defilers); + + // BWAPI::Broodwar->printf("cluster size %d, defilers %d, energy %d", cluster.size(), defilers.size(), (*defilers.begin())->getEnergy()); + + // Collect the food. + // The food may be far away, not in the current cluster. + BWAPI::Unitset food; + for (BWAPI::Unit unit : getUnits()) + { + if (unit->getType() != BWAPI::UnitTypes::Zerg_Defiler) + { + food.insert(unit); + } + } + + //BWAPI::Broodwar->printf("defiler update movement, %d food", food.size()); + + // Control the defilers. + for (BWAPI::Unit defiler : defilers) + { + bool canMove = !isReadyToCast(defiler); + if (!canMove) + { + // BWAPI::Broodwar->printf("defiler can't move, ready to cast"); + } + if (defiler->isBurrowed()) + { + // Must unburrow to cast, whether we canMove or not. + canMove = false; + if (!defiler->isIrradiated() && defiler->canUnburrow()) + { + the.micro.Unburrow(defiler); + } + } + else if (defiler->isIrradiated() && defiler->getEnergy() < 90 && defiler->canBurrow()) + { + // BWAPI::Broodwar->printf("defiler is irradiated"); + canMove = false; + the.micro.Burrow(defiler); + } + + if (canMove && defiler->getEnergy() < 150 && + BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Consume) && + !food.empty()) + { + canMove = !maybeConsume(defiler, food); + } + + if (canMove) + { + BWAPI::Position destination; + + // Figure out where the defiler should move to. + if (vanguard) + { + if (defiler->getEnergy() < 100 && cluster.size() > getUnits().size()) + { + destination = cluster.center; + } + else + { + destination = vanguard->getPosition(); + } + } + else + { + // Retreat to home. + destination = Bases::Instance().myMainBase()->getPosition(); + } + + if (destination.isValid()) + { + // BWAPI::Broodwar->printf("defiler %d at %d,%d will move near %d,%d", + // defiler->getID(), defiler->getPosition().x, defiler->getPosition().y, destination.x, destination.y); + the.micro.MoveNear(defiler, destination); + } + else + { + // BWAPI::Broodwar->printf("defiler wants to move, has nowhere to go"); + } + } + else + { + // BWAPI::Broodwar->printf("defiler cannot move"); + } + } + + // Control the surviving food--move it to the defiler's position. + for (BWAPI::Unit zergling : food) + { + // Find the nearest defiler with low energy and move toward it. + BWAPI::Unit bestDefiler = nullptr; + int bestDist = 999999; + for (BWAPI::Unit defiler : defilers) + { + if (defiler->getType() == BWAPI::UnitTypes::Zerg_Defiler && + defiler->getEnergy() < 150) + { + int dist = zergling->getDistance(defiler); + if (dist < bestDist) + { + bestDefiler = defiler; + bestDist = dist; + } + } + } + + if (bestDefiler && zergling->getDistance(bestDefiler) >= 32) + { + the.micro.Move(zergling, PredictMovement(bestDefiler, 8)); + // BWAPI::Broodwar->printf("move food %d", zergling->getID()); + // BWAPI::Broodwar->drawLineMap(bestDefiler->getPosition(), zergling->getPosition(), BWAPI::Colors::Yellow); + } + } +} + +// Cast dark swarm if possible and useful. +void MicroDefilers::updateSwarm(const UnitCluster & cluster) +{ + BWAPI::Unitset defilers = getDefilers(cluster); + if (defilers.empty()) + { + return; + } + updateCasters(defilers); + + for (BWAPI::Unit defiler : defilers) + { + if (!defiler->isBurrowed() && + defiler->getEnergy() >= 100 && + defiler->canUseTech(BWAPI::TechTypes::Dark_Swarm, defiler->getPosition()) && + !isReadyToCastOtherThan(defiler, CasterSpell::DarkSwarm)) + { + (void) maybeSwarm(defiler); + } + } +} + +// Cast plague if possible and useful. +void MicroDefilers::updatePlague(const UnitCluster & cluster) +{ + BWAPI::Unitset defilers = getDefilers(cluster); + if (defilers.empty()) + { + return; + } + updateCasters(defilers); + + for (BWAPI::Unit defiler : defilers) + { + if (!defiler->isBurrowed() && + defiler->getEnergy() >= 150 && + BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Plague) && + defiler->canUseTech(BWAPI::TechTypes::Plague, defiler->getPosition()) && + !isReadyToCastOtherThan(defiler, CasterSpell::Plague)) + { + (void) maybePlague(defiler); + } + } +} diff --git a/Steamhammer/Source/MicroDefilers.h b/Steamhammer/Source/MicroDefilers.h index b953a84..bf9e492 100644 --- a/Steamhammer/Source/MicroDefilers.h +++ b/Steamhammer/Source/MicroDefilers.h @@ -8,6 +8,7 @@ class MicroDefilers : public MicroManager { // NOTE // This micro manager controls all defilers plus any units assigned as defiler food. + // That means its set of units can include both defilers and zerglings. BWAPI::Unitset getDefilers(const UnitCluster & cluster) const; @@ -15,8 +16,6 @@ class MicroDefilers : public MicroManager bool maybeConsume(BWAPI::Unit defiler, BWAPI::Unitset & food); - bool swarmOrPlague(BWAPI::Unit defiler, BWAPI::TechType techType, BWAPI::Position target) const; - int swarmScore(BWAPI::Unit u) const; bool maybeSwarm(BWAPI::Unit defiler); diff --git a/Steamhammer/Source/MicroDetectors.cpp b/Steamhammer/Source/MicroDetectors.cpp index f6bc7c0..c95df3a 100644 --- a/Steamhammer/Source/MicroDetectors.cpp +++ b/Steamhammer/Source/MicroDetectors.cpp @@ -2,6 +2,7 @@ #include "MicroDetectors.h" #include "The.h" +#include "Bases.h" #include "UnitUtil.h" using namespace UAlbertaBot; @@ -16,29 +17,28 @@ void MicroDetectors::executeMicro(const BWAPI::Unitset & targets, const UnitClus { } -void MicroDetectors::go() +void MicroDetectors::go(const BWAPI::Unitset & squadUnits) { - const BWAPI::Unitset & detectorUnits = getUnits(); + const BWAPI::Unitset & detectorUnits = getUnits(); if (detectorUnits.empty()) { return; } - /* currently unused // Look through the targets to find those which we want to seek or to avoid. BWAPI::Unitset cloakedTargets; BWAPI::Unitset enemies; - int nAirThreats = 0; - + for (const BWAPI::Unit target : BWAPI::Broodwar->enemy()->getUnits()) { - // 1. Find cloaked units. Keep them in detection range. + // 1. Find cloaked units. We want to keep them in detection range. if (target->getType().hasPermanentCloak() || // dark templar, observer target->getType().isCloakable() || // wraith, ghost target->getType() == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine || target->getType() == BWAPI::UnitTypes::Zerg_Lurker || - target->isBurrowed()) + target->isBurrowed() || + target->getOrder() == BWAPI::Orders::Burrowing) { cloakedTargets.insert(target); } @@ -47,19 +47,18 @@ void MicroDetectors::go() { // 2. Find threats. Keep away from them. enemies.insert(target); - - // 3. Count air threats. Stay near anti-air units. - if (target->isFlying()) - { - ++nAirThreats; - } } } - */ // Anti-air units that can fire on air attackers, including static defense. - // TODO not yet implemented - // BWAPI::Unitset defenders; + BWAPI::Unitset defenders; + for (BWAPI::Unit unit : squadUnits) + { + if (UnitUtil::CanAttackAir(unit)) + { + defenders.insert(unit); + } + } // For each detector. // In Steamhammer, detectors in the squad are normally zero or one. @@ -69,15 +68,46 @@ void MicroDetectors::go() { // The detector is alone in the squad. Move to the order position. // This allows the Recon squad to scout with a detector on island maps. - the.micro.Move(detectorUnit, order.getPosition()); + the.micro.MoveNear(detectorUnit, order.getPosition()); return; } - BWAPI::Position destination = detectorUnit->getPosition(); - if (unitClosestToEnemy && unitClosestToEnemy->getPosition().isValid()) + BWAPI::Position destination; + BWAPI::Unit nearestEnemy = NearestOf(detectorUnit->getPosition(), enemies); + BWAPI::Unit nearestDefender = NearestOf(detectorUnit->getPosition(), defenders); + BWAPI::Unit nearestCloaked = NearestOf(detectorUnit->getPosition(), cloakedTargets); + + if (nearestEnemy && + detectorUnit->getDistance(nearestEnemy) <= 2 * 32 + UnitUtil::GetAttackRange(nearestEnemy, detectorUnit)) + { + if (nearestEnemy->isFlying() && + nearestDefender && + detectorUnit->getDistance(nearestDefender) <= 8 * 32) + { + // Move toward the defender, our only hope to escape a flying attacker. + destination = nearestDefender->getPosition(); + } + else + { + // There is no appropriate defender near. Move away from the attacker. + destination = DistanceAndDirection(detectorUnit->getPosition(), nearestEnemy->getPosition(), -8 * 32); + } + } + else if (nearestCloaked && + detectorUnit->getDistance(nearestCloaked) > 9 * 32) // detection range is 11 tiles + { + destination = nearestCloaked->getPosition(); + } + else if (unitClosestToEnemy && + unitClosestToEnemy->getPosition().isValid() && + !the.airAttacks.at(unitClosestToEnemy->getTilePosition())) { destination = unitClosestToEnemy->getPosition(); - the.micro.Move(detectorUnit, destination); } + else + { + destination = Bases::Instance().myMainBase()->getPosition(); + } + the.micro.MoveNear(detectorUnit, destination); } } diff --git a/Steamhammer/Source/MicroDetectors.h b/Steamhammer/Source/MicroDetectors.h index f28f8a5..9ead782 100644 --- a/Steamhammer/Source/MicroDetectors.h +++ b/Steamhammer/Source/MicroDetectors.h @@ -19,6 +19,6 @@ class MicroDetectors : public MicroManager void setSquadSize(int n) { squadSize = n; }; void setUnitClosestToEnemy(BWAPI::Unit unit) { unitClosestToEnemy = unit; } void executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster); - void go(); + void go(const BWAPI::Unitset & squadUnits); }; } \ No newline at end of file diff --git a/Steamhammer/Source/MicroLurkers.cpp b/Steamhammer/Source/MicroLurkers.cpp index 436b9e2..fa37e00 100644 --- a/Steamhammer/Source/MicroLurkers.cpp +++ b/Steamhammer/Source/MicroLurkers.cpp @@ -6,6 +6,62 @@ using namespace UAlbertaBot; +// Prefer the nearest target, as most units do. +BWAPI::Unit MicroLurkers::getNearestTarget(BWAPI::Unit lurker, const BWAPI::Unitset & targets) const +{ + int highPriority = 0; + int closestDist = 99999; + BWAPI::Unit bestTarget = nullptr; + + for (BWAPI::Unit target : targets) + { + int distance = lurker->getDistance(target); + int priority = getAttackPriority(target); + + // BWAPI::Broodwar->drawTextMap(target->getPosition() + BWAPI::Position(20, -10), "%c%d", yellow, priority); + + if (priority > highPriority || priority == highPriority && distance < closestDist) + { + closestDist = distance; + highPriority = priority; + bestTarget = target; + } + } + + return bestTarget; +} + +// Prefer the farthest target with the highest priority. +// The caller promises that all targets are in range, so all targets can be attacked. +// Choosing a distant target gives better odds of accidentally also hitting nearer targets, +// since nearby targets subtend a larger angle from the point of view of the lurker. +// It's a way to slightly improve lurker targeting without doing a full analysis. +BWAPI::Unit MicroLurkers::getFarthestTarget(BWAPI::Unit lurker, const BWAPI::Unitset & targets) const +{ + int highPriority = 0; + int farthestDist = -1; + BWAPI::Unit bestTarget = nullptr; + + for (BWAPI::Unit target : targets) + { + int distance = lurker->getDistance(target); + int priority = getAttackPriority(target); + + // BWAPI::Broodwar->drawTextMap(target->getPosition() + BWAPI::Position(20, -10), "%c%d", yellow, priority); + + if (priority > highPriority || priority == highPriority && distance > farthestDist) + { + farthestDist = distance; + highPriority = priority; + bestTarget = target; + } + } + + return bestTarget; +} + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + MicroLurkers::MicroLurkers() { } @@ -28,7 +84,7 @@ void MicroLurkers::executeMicro(const BWAPI::Unitset & targets, const UnitCluste u->getPosition().isValid(); }); - for (const auto lurker : lurkers) + for (BWAPI::Unit lurker : lurkers) { const bool inOrderRange = lurker->getDistance(order.getPosition()) <= 3 * 32; BWAPI::Unit target = getTarget(lurker, lurkerTargets); @@ -123,7 +179,8 @@ void MicroLurkers::executeMicro(const BWAPI::Unitset & targets, const UnitCluste } else { - if (Config::Debug::DrawUnitTargetInfo) { + if (Config::Debug::DrawUnitTargetInfo) + { BWAPI::Broodwar->drawLineMap(lurker->getPosition(), order.getPosition(), BWAPI::Colors::White); } @@ -164,7 +221,7 @@ BWAPI::Unit MicroLurkers::getTarget(BWAPI::Unit lurker, const BWAPI::Unitset & t const int lurkerRange = BWAPI::UnitTypes::Zerg_Lurker.groundWeapon().maxRange(); BWAPI::Unitset targetsInRange; - for (const auto target : targets) + for (BWAPI::Unit target : targets) { if (lurker->getDistance(target) <= lurkerRange) { @@ -172,29 +229,14 @@ BWAPI::Unit MicroLurkers::getTarget(BWAPI::Unit lurker, const BWAPI::Unitset & t } } - // If any targets are in lurker range, then always return one of the targets in range. - const BWAPI::Unitset & newTargets = targetsInRange.empty() ? targets : targetsInRange; - - int highPriority = 0; - int closestDist = 99999; - BWAPI::Unit bestTarget = nullptr; - - for (const auto target : newTargets) + if (lurker->isBurrowed() && !targetsInRange.empty()) { - int distance = lurker->getDistance(target); - int priority = getAttackPriority(target); - - // BWAPI::Broodwar->drawTextMap(target->getPosition() + BWAPI::Position(20, -10), "%c%d", yellow, priority); - - if ((priority > highPriority) || (priority == highPriority && distance < closestDist)) - { - closestDist = distance; - highPriority = priority; - bestTarget = target; - } + return getFarthestTarget(lurker, targetsInRange); } - return bestTarget; + // If any targets are in lurker range, then always return one of the targets in range. + const BWAPI::Unitset & newTargets = targetsInRange.empty() ? targets : targetsInRange; + return getNearestTarget(lurker, newTargets); } // Only ground units are passed in as potential targets. diff --git a/Steamhammer/Source/MicroLurkers.h b/Steamhammer/Source/MicroLurkers.h index 605be89..5e9a80d 100644 --- a/Steamhammer/Source/MicroLurkers.h +++ b/Steamhammer/Source/MicroLurkers.h @@ -8,6 +8,11 @@ namespace UAlbertaBot class MicroLurkers : public MicroManager { + private: + + BWAPI::Unit getNearestTarget(BWAPI::Unit lurker, const BWAPI::Unitset & targets) const; + BWAPI::Unit getFarthestTarget(BWAPI::Unit lurker, const BWAPI::Unitset & targets) const; + public: MicroLurkers(); diff --git a/Steamhammer/Source/MicroManager.cpp b/Steamhammer/Source/MicroManager.cpp index 87d082d..9015061 100644 --- a/Steamhammer/Source/MicroManager.cpp +++ b/Steamhammer/Source/MicroManager.cpp @@ -8,6 +8,43 @@ using namespace UAlbertaBot; +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +CasterState::CasterState() + : spell(CasterSpell::None) + , lastEnergy(0) + , lastCastFrame(0) +{ +} + +CasterState::CasterState(BWAPI::Unit caster) + : spell(CasterSpell::None) + , lastEnergy(caster->getEnergy()) + , lastCastFrame(0) +{ +} + +void CasterState::update(BWAPI::Unit caster) +{ + if (caster->getEnergy() < lastEnergy) + { + // We either cast the spell, or we were hit by EMP or feedback. + // Whatever the case, we're not going to cast now. + spell = CasterSpell::None; + lastCastFrame = BWAPI::Broodwar->getFrameCount(); + // BWAPI::Broodwar->printf("... spell complete"); + } + lastEnergy = caster->getEnergy(); +} + +// Not enough time since the last spell. +bool CasterState::waitToCast() const +{ + return BWAPI::Broodwar->getFrameCount() - lastCastFrame < framesBetweenCasts; +} + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + MicroManager::MicroManager() : the(The::Root()) { @@ -15,7 +52,7 @@ MicroManager::MicroManager() void MicroManager::setUnits(const BWAPI::Unitset & u) { - _units = u; + _units = u; } void MicroManager::setOrder(const SquadOrder & inputOrder) @@ -107,7 +144,7 @@ void MicroManager::destroyNeutralTargets(const BWAPI::Unitset & targets) for (const auto unit : _units) { - if (visibleTarget) + if (visibleTarget) { // We see a target, so we can issue attack orders to units that can attack. if (UnitUtil::CanAttackGround(unit) && unit->canAttack()) @@ -222,17 +259,9 @@ void MicroManager::regroup(const BWAPI::Position & regroupPosition, const UnitCl } else { - // We have retreated to a good position. - // A ranged unit holds position, a melee unit attack-moves to its own position. - BWAPI::WeaponType weapon = UnitUtil::GetGroundWeapon(unit) || UnitUtil::GetAirWeapon(unit); - if (weapon && weapon.maxRange() >= 32) - { - the.micro.HoldPosition(unit); - } - else - { - the.micro.AttackMove(unit, unit->getPosition()); - } + // We have retreated to a good position. Stay put. + // NOTE Units can attack while on hold position, if an enemy comes in range. + the.micro.HoldPosition(unit); } } } @@ -363,7 +392,7 @@ bool MicroManager::dodgeMine(BWAPI::Unit u) const // Send the protoss unit to the shield battery and recharge its shields. // The caller should have already checked all conditions. -// TODO shielf batteries are not quite working +// TODO shield batteries are not quite working void MicroManager::useShieldBattery(BWAPI::Unit unit, BWAPI::Unit shieldBattery) { if (unit->getDistance(shieldBattery) >= 32) @@ -378,7 +407,122 @@ void MicroManager::useShieldBattery(BWAPI::Unit unit, BWAPI::Unit shieldBattery) } } -void MicroManager::drawOrderText() +// The decision is made. Move closer if necessary, then cast the spell. +// The target is a map position. +bool MicroManager::spell(BWAPI::Unit caster, BWAPI::TechType techType, BWAPI::Position target) const +{ + UAB_ASSERT(techType.targetsPosition() && target.isValid(), "can't target that"); + + // Enough time since the last spell? + // Forcing a delay prevents double-casting on the same target. + auto it = _casterState.find(caster); + if (it == _casterState.end() || (*it).second.waitToCast()) + { + return false; + } + + if (caster->getDistance(target) > techType.getWeapon().maxRange()) + { + // We're out of range. Move closer. + // BWAPI::Broodwar->printf("%s moving in...", UnitTypeName(caster).c_str()); + the.micro.Move(caster, target); + return true; + } + else if (caster->canUseTech(techType, target)) + { + // BWAPI::Broodwar->printf("%s!", techType.getName().c_str()); + return the.micro.UseTech(caster, techType, target); + } + + return false; +} + +// The decision is made. Move closer if necessary, then cast the spell. +// The target is a unit. +bool MicroManager::spell(BWAPI::Unit caster, BWAPI::TechType techType, BWAPI::Unit target) const +{ + UAB_ASSERT(techType.targetsUnit() && target->exists() && target->getPosition().isValid(), "can't target that"); + + // Enough time since the last spell? + // Forcing a delay prevents double-casting on the same target. + auto it = _casterState.find(caster); + if (it == _casterState.end() || (*it).second.waitToCast()) + { + return false; + } + + if (caster->getDistance(target) > techType.getWeapon().maxRange()) + { + // We're out of range. Move closer. + // BWAPI::Broodwar->printf("%s moving in...", UnitTypeName(caster).c_str()); + the.micro.Move(caster, target->getPosition()); + return true; + } + else if (caster->canUseTech(techType, target)) + { + // BWAPI::Broodwar->printf("%s!", techType.getName().c_str()); + return the.micro.UseTech(caster, techType, target); + } + + return false; +} + +// A spell caster declares that it is ready to cast. +void MicroManager::setReadyToCast(BWAPI::Unit caster, CasterSpell spell) +{ + _casterState.at(caster).setSpell(spell); +} + +// Is it ready to cast? If so, don't interrupt it with another action. +bool MicroManager::isReadyToCast(BWAPI::Unit caster) +{ + return _casterState.at(caster).getSpell() != CasterSpell::None; +} + +// Is it ready to cast a spell other than the given one? If so, don't interrupt it with another action. +bool MicroManager::isReadyToCastOtherThan(BWAPI::Unit caster, CasterSpell spellToAvoid) +{ + CasterSpell spell = _casterState.at(caster).getSpell(); + return spell != CasterSpell::None && spell != spellToAvoid; +} + +// Update records for spells that have finished casting, and delete obsolete records. +// Called only by micro managers which control casters. +void MicroManager::updateCasters(const BWAPI::Unitset & casters) +{ + // Update the known casters. + for (auto it = _casterState.begin(); it != _casterState.end();) + { + BWAPI::Unit caster = (*it).first; + CasterState & state = (*it).second; + + if (caster->exists()) + { + if (casters.contains(caster)) + { + state.update(caster); + } + ++it; + } + else + { + // Delete records for units which are gone. + it = _casterState.erase(it); + } + } + + // Add any new casters. + for (BWAPI::Unit caster : casters) + { + auto it = _casterState.find(caster); + if (it == _casterState.end()) + { + _casterState.insert(std::pair(caster, CasterState(caster))); + } + } +} + +void MicroManager::drawOrderText() { if (Config::Debug::DrawUnitTargetInfo) { @@ -388,3 +532,27 @@ void MicroManager::drawOrderText() } } } + +// Is the enemy threatening to shoot at any of our units in the set? +bool MicroManager::anyUnderThreat(const BWAPI::Unitset & units) const +{ + for (const BWAPI::Unit unit : units) + { + // Is static defense in range? + if (unit->isFlying() ? the.airAttacks.inRange(unit) : the.groundAttacks.inRange(unit)) + { + return true; + } + + // Are enemy mobile units close and intending to shoot? + for (BWAPI::Unit enemy : InformationManager::Instance().getEnemyFireteam(unit)) + { + if (UnitUtil::IsSuicideUnit(enemy) || + unit->getDistance(enemy) < 32 + UnitUtil::GetAttackRange(enemy, unit)) + { + return true; + } + } + } + return false; +} diff --git a/Steamhammer/Source/MicroManager.h b/Steamhammer/Source/MicroManager.h index 36fc906..aa6bff9 100644 --- a/Steamhammer/Source/MicroManager.h +++ b/Steamhammer/Source/MicroManager.h @@ -7,26 +7,66 @@ namespace UAlbertaBot class The; class UnitCluster; +enum class CasterSpell + { None + , Parasite + , DarkSwarm + , Plague + }; + +// Used by some micro managers to keep track of what spell casters are intending. +// It prevents other operations from interrupting. +class CasterState +{ +private: + + CasterSpell spell; // preparing to cast, don't interrupt + int lastEnergy; + int lastCastFrame; // prevent double casting on the same target + + static const int framesBetweenCasts = 24; + +public: + + CasterState(); + CasterState(BWAPI::Unit caster); + + void update(BWAPI::Unit caster); + + CasterSpell getSpell() const { return spell; }; + void setSpell(CasterSpell s) { spell = s; }; + bool waitToCast() const; +}; + class MicroManager { - BWAPI::Unitset _units; + BWAPI::Unitset _units; + std::map _casterState; protected: - The & the; + The & the; - SquadOrder order; + SquadOrder order; - virtual void executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) = 0; - void destroyNeutralTargets(const BWAPI::Unitset & targets); - bool checkPositionWalkable(BWAPI::Position pos); - bool unitNearEnemy(BWAPI::Unit unit); - bool unitNearChokepoint(BWAPI::Unit unit) const; + virtual void executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) = 0; + void destroyNeutralTargets(const BWAPI::Unitset & targets); + bool checkPositionWalkable(BWAPI::Position pos); + bool unitNearEnemy(BWAPI::Unit unit); + bool unitNearChokepoint(BWAPI::Unit unit) const; - bool dodgeMine(BWAPI::Unit u) const; - void useShieldBattery(BWAPI::Unit unit, BWAPI::Unit shieldBattery); + bool dodgeMine(BWAPI::Unit u) const; - void drawOrderText(); + void useShieldBattery(BWAPI::Unit unit, BWAPI::Unit shieldBattery); + bool spell(BWAPI::Unit caster, BWAPI::TechType techType, BWAPI::Position target) const; + bool spell(BWAPI::Unit caster, BWAPI::TechType techType, BWAPI::Unit target) const; + + void setReadyToCast(BWAPI::Unit caster, CasterSpell spell); + bool isReadyToCast(BWAPI::Unit caster); + bool isReadyToCastOtherThan(BWAPI::Unit caster, CasterSpell spellToAvoid); + void updateCasters(const BWAPI::Unitset & casters); + + void drawOrderText(); public: MicroManager(); @@ -40,5 +80,7 @@ class MicroManager void execute(const UnitCluster & cluster); void regroup(const BWAPI::Position & regroupPosition, const UnitCluster & cluster) const; + bool anyUnderThreat(const BWAPI::Unitset & units) const; + }; } \ No newline at end of file diff --git a/Steamhammer/Source/MicroMedics.cpp b/Steamhammer/Source/MicroMedics.cpp index 2520d03..3dfcde0 100644 --- a/Steamhammer/Source/MicroMedics.cpp +++ b/Steamhammer/Source/MicroMedics.cpp @@ -1,91 +1,92 @@ -#include "MicroManager.h" -#include "MicroMedics.h" - -#include "The.h" - -using namespace UAlbertaBot; - -MicroMedics::MicroMedics() -{ -} - -// Unused but required. -void MicroMedics::executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) -{ -} - -void MicroMedics::update(const UnitCluster & cluster, const BWAPI::Position & goal) -{ - const BWAPI::Unitset & medics = Intersection(getUnits(), cluster.units); - if (medics.empty()) - { - return; - } - - // create a set of all medic targets - BWAPI::Unitset medicTargets; - for (const auto unit : BWAPI::Broodwar->self()->getUnits()) - { - if (unit->getHitPoints() < unit->getInitialHitPoints() && unit->getType().isOrganic()) - { - medicTargets.insert(unit); - } - } - - BWAPI::Unitset availableMedics(medics); - - // for each target, send the closest medic to heal it - for (const auto target : medicTargets) - { - // only one medic can heal a target at a time - if (target->isBeingHealed()) - { - continue; - } - - int closestMedicDist = 99999; - BWAPI::Unit closestMedic = nullptr; - - for (const auto medic : availableMedics) - { - int dist = medic->getDistance(target); - - if (dist < closestMedicDist) - { - closestMedic = medic; - closestMedicDist = dist; - } - } - - // if we found a medic, send it to heal the target - if (closestMedic) - { - closestMedic->useTech(BWAPI::TechTypes::Healing, target); - - availableMedics.erase(closestMedic); - } - // otherwise we didn't find a medic which means they're all in use so break - else - { - break; - } - } - - // remaining medics should head toward the goal position - for (const auto medic : availableMedics) - { - the.micro.AttackMove(medic, goal); - } -} - -// Add up the energy of all medics. -// This info is used in deciding whether to stim, and could have other uses. -int MicroMedics::getTotalEnergy() -{ - int energy = 0; - for (const auto unit : getUnits()) - { - energy += unit->getEnergy(); - } - return energy; +#include "MicroManager.h" +#include "MicroMedics.h" + +#include "The.h" + +using namespace UAlbertaBot; + +MicroMedics::MicroMedics() +{ +} + +// Unused but required. +void MicroMedics::executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) +{ +} + +void MicroMedics::update(const UnitCluster & cluster, BWAPI::Unit vanguard) +{ + const BWAPI::Unitset & medics = Intersection(getUnits(), cluster.units); + if (medics.empty()) + { + return; + } + + // create a set of all medic targets + BWAPI::Unitset medicTargets; + for (const auto unit : BWAPI::Broodwar->self()->getUnits()) + { + if (unit->getHitPoints() < unit->getInitialHitPoints() && unit->getType().isOrganic()) + { + medicTargets.insert(unit); + } + } + + BWAPI::Unitset availableMedics(medics); + + // for each target, send the closest medic to heal it + for (const auto target : medicTargets) + { + // only one medic can heal a target at a time + if (target->isBeingHealed()) + { + continue; + } + + int closestMedicDist = 99999; + BWAPI::Unit closestMedic = nullptr; + + for (const auto medic : availableMedics) + { + int dist = medic->getDistance(target); + + if (dist < closestMedicDist) + { + closestMedic = medic; + closestMedicDist = dist; + } + } + + // if we found a medic, send it to heal the target + if (closestMedic) + { + closestMedic->useTech(BWAPI::TechTypes::Healing, target); + + availableMedics.erase(closestMedic); + } + else + { + // We didn't find a medic, so they're all in use. Stop looping. + break; + } + } + + // remaining medics should head toward the goal position + BWAPI::Position medicGoal = vanguard ? vanguard->getPosition() : cluster.center; + for (const auto medic : availableMedics) + { + the.micro.AttackMove(medic, medicGoal); // the same as heal-move + } +} + +// Add up the energy of all medics. +// This info is used in deciding whether to stim, and could have other uses. +int MicroMedics::getTotalEnergy() +{ + int energy = 0; + for (const auto unit : getUnits()) + { + energy += unit->getEnergy(); + } + return energy; } \ No newline at end of file diff --git a/Steamhammer/Source/MicroMedics.h b/Steamhammer/Source/MicroMedics.h index 06d03a0..d669f9a 100644 --- a/Steamhammer/Source/MicroMedics.h +++ b/Steamhammer/Source/MicroMedics.h @@ -10,7 +10,7 @@ class MicroMedics : public MicroManager MicroMedics(); void executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster); - void update(const UnitCluster & cluster, const BWAPI::Position & goal); + void update(const UnitCluster & cluster, BWAPI::Unit vanguard); int getTotalEnergy(); }; } \ No newline at end of file diff --git a/Steamhammer/Source/MicroMelee.cpp b/Steamhammer/Source/MicroMelee.cpp index 567372e..77bc6d2 100644 --- a/Steamhammer/Source/MicroMelee.cpp +++ b/Steamhammer/Source/MicroMelee.cpp @@ -27,22 +27,29 @@ void MicroMelee::executeMicro(const BWAPI::Unitset & targets, const UnitCluster void MicroMelee::assignTargets(const BWAPI::Unitset & meleeUnits, const BWAPI::Unitset & targets) { BWAPI::Unitset meleeUnitTargets; - for (const auto target : targets) + for (const BWAPI::Unit target : targets) { if (target->isVisible() && target->isDetected() && !target->isFlying() && - target->getPosition().isValid() && - target->getType() != BWAPI::UnitTypes::Zerg_Larva && - target->getType() != BWAPI::UnitTypes::Zerg_Egg && - !target->isStasised() && + target->getType() != BWAPI::UnitTypes::Zerg_Larva && + target->getType() != BWAPI::UnitTypes::Zerg_Egg && + target->getPosition().isValid() && + !target->isInvincible() && !target->isUnderDisruptionWeb()) // melee unit can't attack under dweb { meleeUnitTargets.insert(target); } } - for (const auto meleeUnit : meleeUnits) + // Are any enemies in range to shoot at the melee units? + bool underThreat = false; + if (order.isCombatOrder()) + { + underThreat = anyUnderThreat(meleeUnits); + } + + for (const auto meleeUnit : meleeUnits) { if (meleeUnit->isBurrowed()) { @@ -90,7 +97,7 @@ void MicroMelee::assignTargets(const BWAPI::Unitset & meleeUnits, const BWAPI::U } else { - BWAPI::Unit target = getTarget(meleeUnit, meleeUnitTargets); + BWAPI::Unit target = getTarget(meleeUnit, meleeUnitTargets, underThreat); if (target) { the.micro.CatchAndAttackUnit(meleeUnit, target); @@ -112,7 +119,8 @@ void MicroMelee::assignTargets(const BWAPI::Unitset & meleeUnits, const BWAPI::U } // Choose a target from the set. -BWAPI::Unit MicroMelee::getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets) +// underThreat is true if any of the melee units is under immediate threat of attack. +BWAPI::Unit MicroMelee::getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets, bool underThreat) { int bestScore = -999999; BWAPI::Unit bestTarget = nullptr; @@ -160,6 +168,20 @@ BWAPI::Unit MicroMelee::getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & score -= 6 * 32; } + if (!underThreat) + { + // We're not under threat. Prefer to attack stuff outside enemy static defense range. + if (!the.groundAttacks.inRange(target)) + { + score += 2 * 32; + } + // Also prefer to attack stuff that can't shoot back. + if (!UnitUtil::CanAttackGround(target)) + { + score += 2 * 32; + } + } + // A bonus for attacking enemies that are "in front". // It helps reduce distractions from moving toward the goal, the order position. if (closerToGoal > 0) @@ -221,7 +243,6 @@ BWAPI::Unit MicroMelee::getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & return bestTarget; } -// get the attack priority of a type int MicroMelee::getAttackPriority(BWAPI::Unit attacker, BWAPI::Unit target) const { BWAPI::UnitType targetType = target->getType(); @@ -241,93 +262,93 @@ int MicroMelee::getAttackPriority(BWAPI::Unit attacker, BWAPI::Unit target) cons { return 10; } - if ((targetType == BWAPI::UnitTypes::Terran_Missile_Turret || targetType == BWAPI::UnitTypes::Terran_Comsat_Station) && - (BWAPI::Broodwar->self()->deadUnitCount(BWAPI::UnitTypes::Protoss_Dark_Templar) == 0)) - { - return 9; - } - if (targetType == BWAPI::UnitTypes::Zerg_Spore_Colony) - { - return 8; - } - if (targetType.isWorker()) - { - return 8; - } - } +if ((targetType == BWAPI::UnitTypes::Terran_Missile_Turret || targetType == BWAPI::UnitTypes::Terran_Comsat_Station) && + (BWAPI::Broodwar->self()->deadUnitCount(BWAPI::UnitTypes::Protoss_Dark_Templar) == 0)) +{ + return 9; +} +if (targetType == BWAPI::UnitTypes::Zerg_Spore_Colony) +{ + return 8; +} +if (targetType.isWorker()) +{ + return 8; +} + } - // Short circuit: Enemy unit which is far enough outside its range is lower priority than a worker. - int enemyRange = UnitUtil::GetAttackRange(target, attacker); - if (enemyRange && - !targetType.isWorker() && - attacker->getDistance(target) > 32 + enemyRange) - { - return 8; - } - // Short circuit: Units before bunkers! - if (targetType == BWAPI::UnitTypes::Terran_Bunker) - { - return 10; - } - // Medics and ordinary combat units. Include workers that are doing stuff. - if (targetType == BWAPI::UnitTypes::Terran_Medic || - targetType == BWAPI::UnitTypes::Protoss_High_Templar || - targetType == BWAPI::UnitTypes::Zerg_Defiler || - UnitUtil::CanAttackGround(target) && !targetType.isWorker()) // includes cannons and sunkens - { - return 12; - } - if (targetType.isWorker() && (target->isRepairing() || target->isConstructing() || unitNearChokepoint(target))) - { - return 12; - } - // next priority is bored workers and turrets - if (targetType.isWorker() || targetType == BWAPI::UnitTypes::Terran_Missile_Turret) - { - return 9; - } + // Short circuit: Enemy unit which is far enough outside its range is lower priority than a worker. + int enemyRange = UnitUtil::GetAttackRange(target, attacker); + if (enemyRange && + !targetType.isWorker() && + attacker->getDistance(target) > 32 + enemyRange) + { + return 8; + } + // Short circuit: Units before bunkers! + if (targetType == BWAPI::UnitTypes::Terran_Bunker) + { + return 10; + } + // Medics and ordinary combat units. Include workers that are doing stuff. + if (targetType == BWAPI::UnitTypes::Terran_Medic || + targetType == BWAPI::UnitTypes::Protoss_High_Templar || + targetType == BWAPI::UnitTypes::Zerg_Defiler || + UnitUtil::CanAttackGround(target) && !targetType.isWorker()) // includes cannons and sunkens + { + return 12; + } + if (targetType.isWorker() && (target->isRepairing() || target->isConstructing() || unitNearChokepoint(target))) + { + return 12; + } + // next priority is bored workers and turrets + if (targetType.isWorker() || targetType == BWAPI::UnitTypes::Terran_Missile_Turret) + { + return 9; + } + // Nydus canal is critical. + if (targetType == BWAPI::UnitTypes::Zerg_Nydus_Canal) + { + return 10; + } // Buildings come under attack during free time, so they can be split into more levels. - // Nydus canal is critical. - if (targetType == BWAPI::UnitTypes::Zerg_Nydus_Canal) - { - return 10; - } - if (targetType == BWAPI::UnitTypes::Zerg_Spire || - targetType == BWAPI::UnitTypes::Zerg_Greater_Spire) - { - return 6; - } - if (targetType == BWAPI::UnitTypes::Protoss_Templar_Archives || - targetType.isSpellcaster()) - { - return 5; - } - if (targetType == BWAPI::UnitTypes::Protoss_Pylon || - targetType == BWAPI::UnitTypes::Zerg_Spawning_Pool) - { - return 4; - } - // Short circuit: Addons other than a completed comsat are worth almost nothing. - // TODO should also check that it is attached - if (targetType.isAddon() && !(targetType == BWAPI::UnitTypes::Terran_Comsat_Station && target->isCompleted())) - { - return 1; - } - // anything with a cost - if (targetType.gasPrice() > 0 || targetType.mineralPrice() > 0) - { - return 3; - } - - // then everything else - return 1; + if (targetType == BWAPI::UnitTypes::Zerg_Spire || + targetType == BWAPI::UnitTypes::Zerg_Greater_Spire) + { + return 6; + } + if (targetType == BWAPI::UnitTypes::Protoss_Templar_Archives || + targetType.isSpellcaster()) + { + return 5; + } + if (targetType == BWAPI::UnitTypes::Protoss_Pylon || + targetType == BWAPI::UnitTypes::Zerg_Spawning_Pool) + { + return 4; + } + // Short circuit: Addons other than a completed comsat are worth almost nothing. + // TODO should also check that it is attached + if (targetType.isAddon() && !(targetType == BWAPI::UnitTypes::Terran_Comsat_Station && target->isCompleted())) + { + return 1; + } + // anything with a cost + if (targetType.gasPrice() > 0 || targetType.mineralPrice() > 0) + { + return 3; + } + + // then everything else + return 1; } // Retreat hurt units to allow them to regenerate health (zerg) or shields (protoss). bool MicroMelee::meleeUnitShouldRetreat(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets) { // Terran don't regen so it doesn't make sense to retreat. - // NOTE We might want to retreat a firebat if medics are available. + // NOTE We might want to retreat a firebat if medics are available. if (meleeUnit->getType().getRace() == BWAPI::Races::Terran) { return false; @@ -340,8 +361,14 @@ bool MicroMelee::meleeUnitShouldRetreat(BWAPI::Unit meleeUnit, const BWAPI::Unit return false; } + // An irradiated unit does not gain from retreating. + if (meleeUnit->isIrradiated() && meleeUnit->getType().isOrganic()) + { + return false; + } + // if there is a ranged enemy unit within attack range of this melee unit then we shouldn't bother retreating since it could fire and kill it anyway - for (auto & unit : targets) + for (BWAPI::Unit unit : targets) { int groundWeaponRange = UnitUtil::GetAttackRange(unit, meleeUnit); if (groundWeaponRange >= 64 && unit->getDistance(meleeUnit) < groundWeaponRange) diff --git a/Steamhammer/Source/MicroMelee.h b/Steamhammer/Source/MicroMelee.h index 813e902..d872997 100644 --- a/Steamhammer/Source/MicroMelee.h +++ b/Steamhammer/Source/MicroMelee.h @@ -15,7 +15,7 @@ class MicroMelee : public MicroManager void assignTargets(const BWAPI::Unitset & meleeUnits, const BWAPI::Unitset & targets); int getAttackPriority(BWAPI::Unit attacker, BWAPI::Unit unit) const; - BWAPI::Unit getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets); + BWAPI::Unit getTarget(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets, bool underThreat); bool meleeUnitShouldRetreat(BWAPI::Unit meleeUnit, const BWAPI::Unitset & targets); }; } \ No newline at end of file diff --git a/Steamhammer/Source/MicroMutas.cpp b/Steamhammer/Source/MicroMutas.cpp index c5693a7..291692a 100644 --- a/Steamhammer/Source/MicroMutas.cpp +++ b/Steamhammer/Source/MicroMutas.cpp @@ -316,7 +316,7 @@ void MicroMutas::scoreTargets(const BWAPI::Position & center, const BWAPI::Units if (bestTargets.size() > 0) { - BWAPI::Broodwar->printf("best score %d", bestTargets[0].second); + //BWAPI::Broodwar->printf("best score %d", bestTargets[0].second); } } @@ -327,11 +327,12 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) // Special cases for ZvZ. if (BWAPI::Broodwar->enemy()->getRace() == BWAPI::Races::Zerg) { - if (targetType == BWAPI::UnitTypes::Zerg_Scourge) - { - return 12; - } - if (targetType == BWAPI::UnitTypes::Zerg_Mutalisk) + if (targetType == BWAPI::UnitTypes::Zerg_Scourge || + targetType == BWAPI::UnitTypes::Zerg_Defiler) + { + return 12; + } + if (targetType == BWAPI::UnitTypes::Zerg_Mutalisk) { return 11; } @@ -339,7 +340,12 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) { return 10; } - if (targetType == BWAPI::UnitTypes::Zerg_Drone) + // Nydus canal is the most important building to kill. + if (targetType == BWAPI::UnitTypes::Zerg_Nydus_Canal) + { + return 10; + } + if (targetType == BWAPI::UnitTypes::Zerg_Drone) { return 9; } @@ -359,6 +365,16 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) { return 5; } + // Don't forget the hatcheries. + if (targetType.isResourceDepot()) + { + return 4; + } + if (targetType.gasPrice() > 0) + { + return 3; + } + return 1; } // A ghost which is nuking is the highest priority by a mile. @@ -397,11 +413,6 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) } } - if (targetType == BWAPI::UnitTypes::Zerg_Scourge) - { - return 12; - } - // Failing, that, give higher priority to air units hitting tanks. // Not quite as high a priority as hitting reavers or high templar, though. if (targetType == BWAPI::UnitTypes::Terran_Siege_Tank_Tank_Mode || targetType == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode) @@ -467,11 +478,6 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) { return 8; } - // Nydus canal is the most important building to kill. - if (targetType == BWAPI::UnitTypes::Zerg_Nydus_Canal) - { - return 10; - } // Spellcasters are as important as key buildings. // Also remember to target other non-threat combat units. if (targetType.isSpellcaster() || @@ -485,10 +491,6 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) { return 7; } - if (targetType == BWAPI::UnitTypes::Zerg_Spawning_Pool) - { - return 7; - } // Don't forget the nexus/cc/hatchery. if (targetType.isResourceDepot()) { @@ -502,10 +504,6 @@ int MicroMutas::getAttackPriority(BWAPI::Unit target) { return 5; } - if (targetType == BWAPI::UnitTypes::Zerg_Spire) - { - return 5; - } // Downgrade unfinished/unpowered buildings, with exceptions. if (targetType.isBuilding() && (!target->isCompleted() || !target->isPowered()) && diff --git a/Steamhammer/Source/MicroOverlords.cpp b/Steamhammer/Source/MicroOverlords.cpp index 83f0cdb..1532e1f 100644 --- a/Steamhammer/Source/MicroOverlords.cpp +++ b/Steamhammer/Source/MicroOverlords.cpp @@ -6,12 +6,12 @@ using namespace UAlbertaBot; -// Behaviors: +// Basic behaviors: // If overlord hunters are expected, seek spore colonies. // Otherwise, 1 overlord to each base. // The nearest spore colony, if any. -BWAPI::Unit MicroOverlords::nearestSpore(BWAPI::Unit overlord) +BWAPI::Unit MicroOverlords::nearestSpore(BWAPI::Unit overlord) const { BWAPI::Unit best = nullptr; int bestDistance = 99999; @@ -32,6 +32,26 @@ BWAPI::Unit MicroOverlords::nearestSpore(BWAPI::Unit overlord) return best; } +void MicroOverlords::goToSpore(BWAPI::Unit overlord) const +{ + BWAPI::Unit spore = nearestSpore(overlord); + if (spore) + { + if (overlord->isMoving() && overlord->getDistance(spore) <= 16) + { + the.micro.Stop(overlord); + } + else + { + the.micro.Move(overlord, spore->getPosition()); + } + } + else + { + UAB_ASSERT(false, "no spore"); + } +} + // We fear cloaked units. // Assign one overlord to each base for detection, as best possible. void MicroOverlords::assignOverlords() @@ -112,11 +132,8 @@ void MicroOverlords::update() // In this case, we don't care about base assignments. for (BWAPI::Unit overlord : getUnits()) { - BWAPI::Unit spore = nearestSpore(overlord); - UAB_ASSERT(spore, "no spore after all"); - the.micro.Move(overlord, spore->getPosition()); + goToSpore(overlord); } - //BWAPI::Broodwar->printf("run to spores"); } else { @@ -133,11 +150,8 @@ void MicroOverlords::update() // Move remaining overlords to spores. for (BWAPI::Unit overlord : unassignedOverlords) { - BWAPI::Unit spore = nearestSpore(overlord); - UAB_ASSERT(spore, "no spore after all"); - the.micro.Move(overlord, spore->getPosition()); + goToSpore(overlord); } - //BWAPI::Broodwar->printf("run spares to spores"); } // Otherwise don't worry (for now) about where unassigned overlords are. } diff --git a/Steamhammer/Source/MicroOverlords.h b/Steamhammer/Source/MicroOverlords.h index b7ab877..9a23415 100644 --- a/Steamhammer/Source/MicroOverlords.h +++ b/Steamhammer/Source/MicroOverlords.h @@ -11,7 +11,8 @@ class MicroOverlords : public MicroManager std::map baseAssignments; // base -> overlord, only 1 overlord is assigned BWAPI::Unitset unassignedOverlords; - BWAPI::Unit nearestSpore(BWAPI::Unit overlord); + BWAPI::Unit nearestSpore(BWAPI::Unit overlord) const; + void goToSpore(BWAPI::Unit overlord) const; void assignOverlords(); public: diff --git a/Steamhammer/Source/MicroQueens.cpp b/Steamhammer/Source/MicroQueens.cpp new file mode 100644 index 0000000..621e545 --- /dev/null +++ b/Steamhammer/Source/MicroQueens.cpp @@ -0,0 +1,190 @@ +#include "MicroQueens.h" + +#include "Bases.h" +#include "The.h" +#include "UnitUtil.h" + +using namespace UAlbertaBot; + +// The queen is probably about to die. It should cast immediately if it is ever going to. +bool MicroQueens::aboutToDie(const BWAPI::Unit queen) const +{ + return + queen->getHitPoints() < 30 || + queen->isIrradiated() || + queen->isPlagued(); +} + +// This unit is nearby. How much do we want to parasite it? +// Scores >= 100 are worth parasiting now. Scores < 100 are worth it if the queen is about to die. +int MicroQueens::parasiteScore(BWAPI::Unit u) const +{ + if (u->getPlayer() == BWAPI::Broodwar->neutral()) + { + if (u->isFlying()) + { + // It's a flying critter--worth tagging. + return 100; + } + return 1; + } + + // It's an enemy unit. + + BWAPI::UnitType type = u->getType(); + + if (type == BWAPI::UnitTypes::Protoss_Arbiter) + { + return 110; + } + + if (type == BWAPI::UnitTypes::Terran_Battlecruiser || + type == BWAPI::UnitTypes::Terran_Dropship || + type == BWAPI::UnitTypes::Terran_Science_Vessel || + + type == BWAPI::UnitTypes::Protoss_Carrier || + type == BWAPI::UnitTypes::Protoss_Shuttle) + { + return 101; + } + + if (type == BWAPI::UnitTypes::Terran_Siege_Tank_Tank_Mode || + type == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode || + type == BWAPI::UnitTypes::Terran_Valkyrie || + + type == BWAPI::UnitTypes::Protoss_Corsair || + type == BWAPI::UnitTypes::Protoss_Archon || + type == BWAPI::UnitTypes::Protoss_Dark_Archon || + type == BWAPI::UnitTypes::Protoss_Reaver || + type == BWAPI::UnitTypes::Protoss_Scout) + { + return 70; + } + + if (type.isWorker() || + type == BWAPI::UnitTypes::Terran_Ghost || + type == BWAPI::UnitTypes::Terran_Medic || + type == BWAPI::UnitTypes::Terran_Wraith || + type == BWAPI::UnitTypes::Protoss_Observer) + { + return 60; + } + + // A random enemy is worth something to parasite--but not much. + return 2; +} + +// Score units, pick the one with the highest score and maybe parasite it. +bool MicroQueens::maybeParasite(BWAPI::Unit queen) +{ + // Parasite has range 12. We look for targets within the limit range. + const int limit = 12 + 1; + + BWAPI::Unitset targets = BWAPI::Broodwar->getUnitsInRadius(queen->getPosition(), limit * 32, + !BWAPI::Filter::IsBuilding && (BWAPI::Filter::IsEnemy || BWAPI::Filter::IsCritter) && + !BWAPI::Filter::IsInvincible && !BWAPI::Filter::IsParasited); + + if (targets.empty()) + { + return false; + } + + // Look for the target with the best score. + int bestScore = 0; + BWAPI::Unit bestTarget = nullptr; + for (BWAPI::Unit target : targets) + { + int score = parasiteScore(target); + if (score > bestScore) + { + bestScore = score; + bestTarget = target; + } + } + + if (bestTarget) + { + // Parasite something important. + // Or, if the queen is at full energy, parasite something reasonable. + // Or, if the queen is about to die, parasite anything. + if (bestScore >= 100 || + bestScore >= 50 && queen->getEnergy() >= 200 || + aboutToDie(queen)) + { + //BWAPI::Broodwar->printf("parasite score %d on %s @ %d,%d", + // bestScore, UnitTypeName(bestTarget->getType()).c_str(), bestTarget->getPosition().x, bestTarget->getPosition().y); + setReadyToCast(queen, CasterSpell::Parasite); + return spell(queen, BWAPI::TechTypes::Parasite, bestTarget); + } + } + + return false; +} + +void MicroQueens::updateMovement(BWAPI::Unit vanguard) +{ + for (BWAPI::Unit queen : getUnits()) + { + // If it's intending to cast, we don't want to interrupt by moving. + if (!isReadyToCast(queen)) + { + BWAPI::Position destination = Bases::Instance().myMainBase()->getPosition(); + + if (vanguard && queen->getEnergy() >= 65) + { + destination = vanguard->getPosition(); + } + + if (destination.isValid()) + { + the.micro.MoveNear(queen, destination); + } + } + } +} + +// Cast parasite if possible and useful. +void MicroQueens::updateParasite() +{ + for (BWAPI::Unit queen : getUnits()) + { + if (queen->getEnergy() >= 75) + { + (void) maybeParasite(queen); + } + } +} + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +MicroQueens::MicroQueens() +{ +} + +// Unused but required. +void MicroQueens::executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster) +{ +} + +// Control the queen (we only make one). +// Queens are not clustered. +void MicroQueens::update(BWAPI::Unit vanguard) +{ + if (getUnits().empty()) + { + return; + } + + updateCasters(getUnits()); + + const int phase = BWAPI::Broodwar->getFrameCount() % 12; + + if (phase == 0) + { + updateMovement(vanguard); + } + else if (phase == 6) + { + updateParasite(); + } +} diff --git a/Steamhammer/Source/MicroQueens.h b/Steamhammer/Source/MicroQueens.h new file mode 100644 index 0000000..e939462 --- /dev/null +++ b/Steamhammer/Source/MicroQueens.h @@ -0,0 +1,24 @@ +#pragma once; + +#include "MicroManager.h" + +namespace UAlbertaBot +{ +class MicroQueens : public MicroManager +{ + bool aboutToDie(const BWAPI::Unit queen) const; + + int parasiteScore(BWAPI::Unit u) const; + bool maybeParasite(BWAPI::Unit queen); + + // The different updates are done on different frames to spread out the work. + void updateMovement(BWAPI::Unit vanguard); + void updateParasite(); + +public: + MicroQueens(); + void executeMicro(const BWAPI::Unitset & targets, const UnitCluster & cluster); + + void update(BWAPI::Unit vanguard); +}; +} diff --git a/Steamhammer/Source/MicroRanged.cpp b/Steamhammer/Source/MicroRanged.cpp index b6fde1e..4ec73e7 100644 --- a/Steamhammer/Source/MicroRanged.cpp +++ b/Steamhammer/Source/MicroRanged.cpp @@ -43,7 +43,7 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: u->isDetected() && u->getType() != BWAPI::UnitTypes::Zerg_Larva && u->getType() != BWAPI::UnitTypes::Zerg_Egg && - !u->isStasised(); + !u->isInvincible(); }); // Figure out if the enemy is ready to attack ground or air. @@ -51,9 +51,9 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: bool enemyHasAntiAir = false; for (BWAPI::Unit target : rangedUnitTargets) { - if (UnitUtil::AttackOrder(target)) + // If the enemy unit is retreating or whatever, it won't attack. + if (UnitUtil::AttackOrder(target)) { - // If the enemy unit is retreating or whatever, it won't attack. if (UnitUtil::CanAttackGround(target)) { enemyHasAntiGround = true; @@ -65,12 +65,19 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: } } - for (const auto rangedUnit : rangedUnits) + // Are any enemies in range to shoot at the ranged units? + bool underThreat = false; + if (order.isCombatOrder()) + { + underThreat = anyUnderThreat(rangedUnits); + } + + for (const auto rangedUnit : rangedUnits) { if (rangedUnit->isBurrowed()) { // For now, it would burrow only if irradiated. Leave it. - // Lurkers are controlled by a different class. + // Lurkers are controlled elsewhere. continue; } @@ -81,7 +88,7 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: } // Special case for irradiated zerg units. - if (rangedUnit->isIrradiated() && rangedUnit->getType().getRace() == BWAPI::Races::Zerg) + if (rangedUnit->isIrradiated() && rangedUnit->getType().isOrganic()) { if (rangedUnit->isFlying()) { @@ -117,7 +124,7 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: if (order.isCombatOrder()) { // If a target is found, - BWAPI::Unit target = getTarget(rangedUnit, rangedUnitTargets); + BWAPI::Unit target = getTarget(rangedUnit, rangedUnitTargets, underThreat); if (target) { if (Config::Debug::DrawUnitTargetInfo) @@ -140,7 +147,7 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: // No target found. If we're not near the order position, go there. if (rangedUnit->getDistance(order.getPosition()) > 100) { - the.micro.AttackMove(rangedUnit, order.getPosition()); + the.micro.Move(rangedUnit, order.getPosition()); } } } @@ -148,11 +155,11 @@ void MicroRanged::assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI: } // This can return null if no target is worth attacking. -BWAPI::Unit MicroRanged::getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset & targets) +// underThreat is true if any of the melee units is under immediate threat of attack. +BWAPI::Unit MicroRanged::getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset & targets, bool underThreat) { int bestScore = -999999; BWAPI::Unit bestTarget = nullptr; - int bestPriority = -1; // TODO debug only for (const auto target : targets) { @@ -192,7 +199,16 @@ BWAPI::Unit MicroRanged::getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset score += 2 * 32; } - const bool isThreat = UnitUtil::CanAttack(target, rangedUnit); // may include workers as threats + if (!underThreat) + { + // We're not under threat. Prefer to attack stuff outside enemy static defense range. + if (rangedUnit->isFlying() ? !the.airAttacks.inRange(target) : !the.groundAttacks.inRange(target)) + { + score += 4 * 32; + } + } + + const bool isThreat = UnitUtil::CanAttack(target, rangedUnit); // may include workers as threats const bool canShootBack = isThreat && range <= 32 + UnitUtil::GetAttackRange(target, rangedUnit); if (isThreat) @@ -282,8 +298,6 @@ BWAPI::Unit MicroRanged::getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset { bestScore = score; bestTarget = target; - - bestPriority = priority; } } diff --git a/Steamhammer/Source/MicroRanged.h b/Steamhammer/Source/MicroRanged.h index b0b6ecf..3618e2f 100644 --- a/Steamhammer/Source/MicroRanged.h +++ b/Steamhammer/Source/MicroRanged.h @@ -19,7 +19,7 @@ class MicroRanged : public MicroManager void assignTargets(const BWAPI::Unitset & rangedUnits, const BWAPI::Unitset & targets); int getAttackPriority(BWAPI::Unit rangedUnit, BWAPI::Unit target); - BWAPI::Unit getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset & targets); + BWAPI::Unit getTarget(BWAPI::Unit rangedUnit, const BWAPI::Unitset & targets, bool underThreat); bool stayHomeUntilReady(const BWAPI::Unit u) const; }; diff --git a/Steamhammer/Source/MicroScourge.cpp b/Steamhammer/Source/MicroScourge.cpp index 0aaf719..08c3b0f 100644 --- a/Steamhammer/Source/MicroScourge.cpp +++ b/Steamhammer/Source/MicroScourge.cpp @@ -27,35 +27,45 @@ void MicroScourge::assignTargets(const BWAPI::Unitset & scourge, const BWAPI::Un { // The set of potential targets. BWAPI::Unitset scourgeTargets; - std::copy_if(targets.begin(), targets.end(), std::inserter(scourgeTargets, scourgeTargets.end()), - [](BWAPI::Unit u) { - return - u->isVisible() && - u->isDetected() && - u->isFlying(); - }); + for (BWAPI::Unit target : targets) + { + if (target->isVisible() && + target->isFlying() && + target->getType() != BWAPI::UnitTypes::Protoss_Interceptor && + target->getType() != BWAPI::UnitTypes::Zerg_Overlord && + !the.airAttacks.inRange(target->getTilePosition()) && // skip defended targets + target->isDetected() && + !target->isInvincible()) + { + scourgeTargets.insert(target); + } + } for (const auto scourgeUnit : scourge) { - // If a target is found, BWAPI::Unit target = getTarget(scourgeUnit, scourgeTargets); if (target) { + // A target was found. Attack it. if (Config::Debug::DrawUnitTargetInfo) { - BWAPI::Broodwar->drawLineMap(scourgeUnit->getPosition(), scourgeUnit->getTargetPosition(), BWAPI::Colors::Purple); + BWAPI::Broodwar->drawLineMap(scourgeUnit->getPosition(), scourgeUnit->getTargetPosition(), BWAPI::Colors::Blue); } - //the.micro.CatchAndAttackUnit(scourgeUnit, target); - the.micro.AttackUnit(scourgeUnit, target); + the.micro.CatchAndAttackUnit(scourgeUnit, target); } else { // No target found. If we're not near the order position, go there. + // Use Move (not AttackMove) so that we don't attack overlords and such along the way. if (scourgeUnit->getDistance(order.getPosition()) > 3 * 32) { - the.micro.AttackMove(scourgeUnit, order.getPosition()); - } + the.micro.MoveNear(scourgeUnit, order.getPosition()); + if (Config::Debug::DrawUnitTargetInfo) + { + BWAPI::Broodwar->drawLineMap(scourgeUnit->getPosition(), order.getPosition(), BWAPI::Colors::Orange); + } + } } } } @@ -70,9 +80,9 @@ BWAPI::Unit MicroScourge::getTarget(BWAPI::Unit scourge, const BWAPI::Unitset & const int priority = getAttackPriority(target->getType()); // 0..12 const int range = scourge->getDistance(target); // 0..map diameter in pixels - // Let's say that 1 priority step is worth 160 pixels (5 tiles). + // Let's say that 1 priority step is worth 3 tiles. // We care about unit-target range and target-order position distance. - int score = 5 * 32 * priority - range; + int score = 3 * 32 * priority - range; if (score > bestScore) { @@ -101,7 +111,7 @@ int MicroScourge::getAttackPriority(BWAPI::UnitType targetType) targetType == BWAPI::UnitTypes::Protoss_Shuttle || targetType == BWAPI::UnitTypes::Zerg_Queen) { - // Transports other than overlords, plus queens: They are important and defenseless. + // Transports other than overlords, plus queens. They are important and defenseless. return 9; } if (targetType == BWAPI::UnitTypes::Terran_Battlecruiser || @@ -118,7 +128,8 @@ int MicroScourge::getAttackPriority(BWAPI::UnitType targetType) } if (targetType == BWAPI::UnitTypes::Protoss_Observer) { - return 3; + // Higher priority if we have lurkers. + return UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Lurker) > 0 ? 7 : 3; } // Overlords, scourge, interceptors. diff --git a/Steamhammer/Source/MicroTanks.cpp b/Steamhammer/Source/MicroTanks.cpp index d553938..6f4876b 100644 --- a/Steamhammer/Source/MicroTanks.cpp +++ b/Steamhammer/Source/MicroTanks.cpp @@ -159,7 +159,7 @@ void MicroTanks::executeMicro(const BWAPI::Unitset & targets, const UnitCluster } else { - the.micro.AttackMove(tank, order.getPosition()); + the.micro.Move(tank, order.getPosition()); } } } diff --git a/Steamhammer/Source/OpponentModel.cpp b/Steamhammer/Source/OpponentModel.cpp index 4d2dcf8..10f40f7 100644 --- a/Steamhammer/Source/OpponentModel.cpp +++ b/Steamhammer/Source/OpponentModel.cpp @@ -94,7 +94,6 @@ void OpponentModel::considerSingleStrategy() } } - // If the opponent model has collected useful information, // set _recommendedOpening, the opening to play (or instructions for choosing it). // Leaving _recommendedOpening blank continues as if the opponent model were turned off. @@ -157,7 +156,6 @@ void OpponentModel::considerOpenings() // For the first games, stick to the counter openings based on the predicted plan. if (_summary.totalGames <= 5) { - // BWAPI::Broodwar->printf("initial exploration phase"); _recommendedOpening = getOpeningForEnemyPlan(_expectedEnemyPlan); return; // with or without expected play } @@ -205,13 +203,13 @@ void OpponentModel::considerOpenings() } if (!alwaysWins.empty()) { - // BWAPI::Broodwar->printf("always wins"); + //BWAPI::Broodwar->printf("always wins"); _recommendedOpening = alwaysWins; return; } if (!alwaysWinsOnThisMap.empty()) { - // BWAPI::Broodwar->printf("always wins on this map"); + //BWAPI::Broodwar->printf("always wins on this map"); _recommendedOpening = alwaysWinsOnThisMap; return; } @@ -244,7 +242,7 @@ void OpponentModel::considerOpenings() } } -// The enemy always plays the same plan against us. +// The enemy always plays the same plan against us (we think). // Seek the single opening that best counters it. void OpponentModel::singleStrategyEnemyOpenings() { @@ -259,7 +257,7 @@ void OpponentModel::singleStrategyEnemyOpenings() // Decide whether to explore. if (summary.totalWins == 0 || Random::Instance().flag(explorationRate)) { - // BWAPI::Broodwar->printf("single strategy - explore"); + //BWAPI::Broodwar->printf("single strategy - explore"); _recommendedOpening = getExploreOpening(summary); return; } @@ -342,7 +340,7 @@ void OpponentModel::multipleStrategyEnemyOpenings() } } - // BWAPI::Broodwar->printf("multiple strategy choice %s", _recommendedOpening.c_str()); + //BWAPI::Broodwar->printf("multiple strategy choice %s", _recommendedOpening.c_str()); if (_recommendedOpening == "explore") { @@ -535,13 +533,22 @@ void OpponentModel::read() { if (Config::IO::ReadOpponentModel) { - std::ifstream inFile(Config::IO::ReadDir + _filename); + std::ifstream inFile; + + inFile.open(Config::IO::ReadDir + _filename); - // There may not be a file to read. That's OK. - if (inFile.bad()) + // There may not be a file to read. Check for a prepared file in the AI directory. + if (!inFile.good()) { - return; + inFile.clear(); + inFile.open(Config::IO::StaticDir + _filename); + if (!inFile.good()) + { + // No prepared file either. That's OK. + return; + } } + // At this point, we have a file to read. while (inFile.good()) { diff --git a/Steamhammer/Source/OpponentPlan.cpp b/Steamhammer/Source/OpponentPlan.cpp index 87775bc..ef542e1 100644 --- a/Steamhammer/Source/OpponentPlan.cpp +++ b/Steamhammer/Source/OpponentPlan.cpp @@ -151,7 +151,7 @@ void OpponentPlan::recognize() // Recognize slower rushes. // TODO make sure we've seen the bare geyser in the enemy base! - // TODO seeing a unit carrying gas also means the enemy has gas + // TODO seeing an enemy worker carrying gas also means the enemy has gas if (snap.getCount(BWAPI::UnitTypes::Zerg_Hatchery) >= 2 && snap.getCount(BWAPI::UnitTypes::Zerg_Spawning_Pool) > 0 && snap.getCount(BWAPI::UnitTypes::Zerg_Extractor) == 0 diff --git a/Steamhammer/Source/OpsBoss.h b/Steamhammer/Source/OpsBoss.h index b88c8d3..c1a9581 100644 --- a/Steamhammer/Source/OpsBoss.h +++ b/Steamhammer/Source/OpsBoss.h @@ -29,9 +29,9 @@ namespace UAlbertaBot bool air; double speed; - int count; + size_t count; int hp; - double groundDPF; // damage per frame + double groundDPF; // DPF = damage per frame double airDPF; BWAPI::Unitset units; @@ -40,6 +40,7 @@ namespace UAlbertaBot void clear(); void add(const UnitInfo & ui); + size_t size() const { return count; }; void draw(BWAPI::Color color, const std::string & label = "") const; }; diff --git a/Steamhammer/Source/ParseUtils.cpp b/Steamhammer/Source/ParseUtils.cpp index 7c23789..84638a7 100644 --- a/Steamhammer/Source/ParseUtils.cpp +++ b/Steamhammer/Source/ParseUtils.cpp @@ -147,7 +147,8 @@ void ParseUtils::ParseConfigFile(const std::string & filename) JSONTools::ReadString("ErrorLogFilename", io, Config::IO::ErrorLogFilename); JSONTools::ReadBool("LogAssertToErrorFile", io, Config::IO::LogAssertToErrorFile); - JSONTools::ReadString("ReadDirectory", io, Config::IO::ReadDir); + JSONTools::ReadString("StaticDirectory", io, Config::IO::StaticDir); + JSONTools::ReadString("ReadDirectory", io, Config::IO::ReadDir); JSONTools::ReadString("WriteDirectory", io, Config::IO::WriteDir); JSONTools::ReadInt("MaxGameRecords", io, Config::IO::MaxGameRecords); @@ -256,7 +257,7 @@ void ParseUtils::ParseConfigFile(const std::string & filename) } } - // 0.5 TEMPORARY SPECIAL CASE FOR AIST S1 + // 0.5 SPECIAL CASE FOR AIST S1 // On the map Sparkle, play a Sparkle opening. if (BWAPI::Broodwar->mapFileName().find("Sparkle") != std::string::npos) { diff --git a/Steamhammer/Source/ProductionManager.cpp b/Steamhammer/Source/ProductionManager.cpp index 20e149e..4ac65b8 100644 --- a/Steamhammer/Source/ProductionManager.cpp +++ b/Steamhammer/Source/ProductionManager.cpp @@ -68,6 +68,13 @@ void ProductionManager::update() BWAPI::Broodwar->drawTextScreen(150, 10, "Nothing left to build, replanning."); } + if (!_outOfBook) + { + //BWAPI::Broodwar->printf("build finished %d, minerals and gas %d %d", + // BWAPI::Broodwar->getFrameCount(), BWAPI::Broodwar->self()->minerals(), BWAPI::Broodwar->self()->gas() + // ); + } + goOutOfBookAndClearQueue(); StrategyManager::Instance().freshProductionPlan(); } @@ -273,7 +280,7 @@ void ProductionManager::manageBuildOrderQueue() // don't actually loop around in here // TODO because we don't keep track of resources used, // we wait until the next frame to build the next thing. - // Can cause delays in late game! + // Can cause delays, especially in late game! break; } @@ -405,6 +412,19 @@ BWAPI::Unit ProductionManager::getProducer(MacroAct act, BWAPI::Position closest act.getCandidateProducers(candidateProducers); + if (candidateProducers.empty()) + { + return nullptr; + } + + // If we're producing from a larva and we don't care where, seek an appropriate one. + if (act.isUnit() && + act.getUnitType().whatBuilds().first == BWAPI::UnitTypes::Zerg_Larva && + closestTo == BWAPI::Positions::None) + { + return getBestLarva(act, candidateProducers); + } + // Trick: If we're producing a worker, choose the producer (command center, nexus, // or larva) which is farthest from the main base. That way expansions are preferentially // populated with less need to transfer workers. @@ -413,46 +433,118 @@ BWAPI::Unit ProductionManager::getProducer(MacroAct act, BWAPI::Position closest return getFarthestUnitFromPosition(candidateProducers, Bases::Instance().myMainBase()->getPosition()); } - else + + return getClosestUnitToPosition(candidateProducers, closestTo); +} + +// We're producing a zerg unit from a larva. +// The list of units (candidate producers) is guaranteed not empty. +BWAPI::Unit ProductionManager::getBestLarva(const MacroAct & act, const std::vector & units) const +{ + // 1. If it's a worker, seek the least saturated base. + // For equally saturated bases, take the one with the highest larva count. + // Count only the base's resource depot, not any possible macro hatcheries there. + if (act.getUnitType().isWorker()) + { + Base * bestBase = nullptr; + int maxShortfall = -1; + size_t maxLarvas = 0; + for (Base * base : Bases::Instance().getBases()) + { + if (base->getOwner() == BWAPI::Broodwar->self() && UnitUtil::IsCompletedResourceDepot(base->getDepot())) + { + auto larvaSet = base->getDepot()->getLarva(); + if (!larvaSet.empty()) + { + int shortfall = std::max(0, base->getMaxWorkers() - base->getNumWorkers()); + if (shortfall > maxShortfall || shortfall == maxShortfall && larvaSet.size() > maxLarvas) + { + bestBase = base; + maxShortfall = shortfall; + maxLarvas = larvaSet.size(); + } + } + } + } + + if (bestBase) + { + return *bestBase->getDepot()->getLarva().begin(); + } + } + + // 2. Otherwise, pick a hatchery that has the most larvas. + // This reduces wasted larvas; a hatchery won't make another if it has three. + BWAPI::Unit bestHatchery = nullptr; + size_t maxLarvas = 0; + for (BWAPI::Unit hatchery : BWAPI::Broodwar->self()->getUnits()) { - return getClosestUnitToPosition(candidateProducers, closestTo); + if (UnitUtil::IsCompletedResourceDepot(hatchery)) + { + auto larvaSet = hatchery->getLarva(); + if (larvaSet.size() > maxLarvas) + { + bestHatchery = hatchery; + maxLarvas = larvaSet.size(); + if (maxLarvas >= 3) + { + break; + } + } + } + } + + if (bestHatchery) + { + return *bestHatchery->getLarva().begin(); } + + // 3. There might be a larva not attached to any hatchery. + for (BWAPI::Unit larva : BWAPI::Broodwar->self()->getUnits()) + { + if (larva->getType() == BWAPI::UnitTypes::Zerg_Larva) + { + return larva; + } + } + + return nullptr; } BWAPI::Unit ProductionManager::getClosestUnitToPosition(const std::vector & units, BWAPI::Position closestTo) const { - if (units.size() == 0) - { - return nullptr; - } + if (units.empty()) + { + return nullptr; + } - // if we don't care where the unit is return the first one we have - if (closestTo == BWAPI::Positions::None) - { - return *(units.begin()); - } + // if we don't care where the unit is return the first one we have + if (closestTo == BWAPI::Positions::None) + { + return *(units.begin()); + } - BWAPI::Unit closestUnit = nullptr; - int minDist(1000000); + BWAPI::Unit closestUnit = nullptr; + int minDist(1000000); - for (const auto unit : units) - { - UAB_ASSERT(unit != nullptr, "Unit was null"); + for (const auto unit : units) + { + UAB_ASSERT(unit != nullptr, "Unit was null"); int distance = unit->getDistance(closestTo); - if (distance < minDist) - { + if (distance < minDist) + { closestUnit = unit; minDist = distance; } } - return closestUnit; + return closestUnit; } BWAPI::Unit ProductionManager::getFarthestUnitFromPosition(const std::vector & units, BWAPI::Position farthest) const { - if (units.size() == 0) + if (units.empty()) { return nullptr; } @@ -709,6 +801,7 @@ void ProductionManager::executeCommand(MacroCommand command) } else if (cmd == MacroCommandType::StopGas) { + //BWAPI::Broodwar->printf("stop gas command"); WorkerManager::Instance().setCollectGas(false); } else if (cmd == MacroCommandType::StartGas) diff --git a/Steamhammer/Source/ProductionManager.h b/Steamhammer/Source/ProductionManager.h index 4e87d47..929ac86 100644 --- a/Steamhammer/Source/ProductionManager.h +++ b/Steamhammer/Source/ProductionManager.h @@ -38,6 +38,7 @@ class ProductionManager BWAPI::UnitType _extractorTrickUnitType; // drone or zergling Building * _extractorTrickBuilding; // set depending on the extractor trick state + BWAPI::Unit getBestLarva(const MacroAct & act, const std::vector & units) const; BWAPI::Unit getClosestUnitToPosition(const std::vector & units, BWAPI::Position closestTo) const; BWAPI::Unit getFarthestUnitFromPosition(const std::vector & units, BWAPI::Position farthest) const; BWAPI::Unit getClosestLarvaToPosition(BWAPI::Position closestTo) const; diff --git a/Steamhammer/Source/Squad.cpp b/Steamhammer/Source/Squad.cpp index bc1b3f2..b603fbd 100644 --- a/Steamhammer/Source/Squad.cpp +++ b/Steamhammer/Source/Squad.cpp @@ -84,7 +84,7 @@ void Squad::update() return; } - // This is a non-empty combat squad, so it has a meaningful vanguard unit. + // This is a non-empty combat squad, so it may have a meaningful vanguard unit. _vanguard = unitClosestToEnemy(_units); if (_order.getType() == SquadOrderTypes::Load) @@ -102,23 +102,27 @@ void Squad::update() // Maybe stim marines and firebats. stimIfNeeded(); - // Detectors. - _microDetectors.setUnitClosestToEnemy(_vanguard); - _microDetectors.setSquadSize(_units.size()); - _microDetectors.go(); + // Detectors. + _microDetectors.setUnitClosestToEnemy(_vanguard); + _microDetectors.setSquadSize(_units.size()); + _microDetectors.go(_units); - // High templar stay home until they merge to archons, all that's supported so far. + // High templar stay home until they merge to archons, all that's supported so far. _microHighTemplar.update(); + // Queens don't go into clusters, but act independently. + _microQueens.update(_vanguard); + // Finish choosing the units. BWAPI::Unitset unitsToCluster; for (BWAPI::Unit unit : _units) { if (unit->getType().isDetector() || unit->getType().spaceProvided() > 0 || - unit->getType() == BWAPI::UnitTypes::Protoss_High_Templar) + unit->getType() == BWAPI::UnitTypes::Protoss_High_Templar || + unit->getType() == BWAPI::UnitTypes::Zerg_Queen) { - // Don't cluster detectors, transports, or high templar. + // Don't cluster detectors, transports, high templar, queens. // They are handled separately above. } else if (unreadyUnit(unit)) @@ -136,10 +140,11 @@ void Squad::update() for (UnitCluster & cluster : _clusters) { setClusterStatus(cluster); + microSpecialUnits(cluster); } // It can get slow in late game when there are many clusters, so cut down the update frequency. - const int nPhases = std::max(1, std::min(4, int(_clusters.size() / 8))); + const int nPhases = std::max(1, std::min(4, int(_clusters.size() / 12))); int phase = BWAPI::Broodwar->getFrameCount() % nPhases; for (const UnitCluster & cluster : _clusters) { @@ -201,8 +206,41 @@ void Squad::setClusterStatus(UnitCluster & cluster) drawCluster(cluster); } +// Special-case units which are clustered, but arrange their own movement +// instead of accepting the cluster's movement commands. +// Currently, this is medics and defilers. +// Queens are not clustered. +void Squad::microSpecialUnits(const UnitCluster & cluster) +{ + // Medics and defilers try to get near the front line. + static int spellPhase = 0; + spellPhase = (spellPhase + 1) % 8; + if (spellPhase == 1) + { + // The vanguard is chosen among combat units only, so a non-combat unit sent toward + // the vanguard may either advance or retreat--either way, that's probably what we want. + BWAPI::Unit vanguard = unitClosestToEnemy(cluster.units); // cluster vanguard + if (!vanguard) + { + vanguard = _vanguard; // squad vanguard + } + + _microDefilers.updateMovement(cluster, vanguard); + _microMedics.update(cluster, vanguard); + } + else if (spellPhase == 5) + { + _microDefilers.updateSwarm(cluster); + } + else if (spellPhase == 7) + { + _microDefilers.updatePlague(cluster); + } +} + // Take cluster combat actions. These can depend on the status of other clusters. // This handles cluster status of Attack and Regroup. Others are handled by setClusterStatus(). +// This takes no action for special units; see microSpecialUnits(). void Squad::clusterCombat(const UnitCluster & cluster) { if (cluster.status == ClusterStatus::Attack) @@ -233,40 +271,10 @@ void Squad::clusterCombat(const UnitCluster & cluster) _microRanged.regroup(regroupPosition, cluster); _microScourge.regroup(regroupPosition, cluster); _microTanks.regroup(regroupPosition, cluster); - } - else - { - // It's not a combat status. - return; - } - - // The rest is for any combat status. - - // Lurkers never regroup, always execute their order. - // NOTE It is because regrouping works poorly. It retreats and unburrows them too often. - _microLurkers.execute(cluster); - - // Medics and defilers try to get near the front line. - static int defilerPhase = 0; - defilerPhase = (defilerPhase + 1) % 8; - if (defilerPhase == 3) - { - BWAPI::Unit vanguard = unitClosestToEnemy(cluster.units); // cluster vanguard (not squad vanguard) - // Defilers. - _microDefilers.updateMovement(cluster, vanguard); - - // Medics. - BWAPI::Position medicGoal = vanguard ? vanguard->getPosition() : cluster.center; - _microMedics.update(cluster, medicGoal); - } - else if (defilerPhase == 5) - { - _microDefilers.updateSwarm(cluster); - } - else if (defilerPhase == 7) - { - _microDefilers.updatePlague(cluster); + // Lurkers never regroup, always execute their order. + // NOTE It is because regrouping works poorly. It retreats and unburrows them too often. + _microLurkers.execute(cluster); } } @@ -340,15 +348,21 @@ void Squad::moveCluster(const UnitCluster & cluster, const BWAPI::Position & des { for (BWAPI::Unit unit : cluster.units) { - if (!UnitUtil::MobilizeUnit(unit)) + // Only move units which don't arrange their own movement. + // Queens do their own movement, but are not clustered and won't turn up here. + if (unit->getType() != BWAPI::UnitTypes::Terran_Medic && + !_microDefilers.getUnits().contains(unit)) // defilers plus defiler food { - if (lazy) - { - the.micro.MoveNear(unit, destination); - } - else + if (!UnitUtil::MobilizeUnit(unit)) { - the.micro.Move(unit, destination); + if (lazy) + { + the.micro.MoveNear(unit, destination); + } + else + { + the.micro.Move(unit, destination); + } } } } @@ -484,6 +498,7 @@ void Squad::addUnitsToMicroManagers() BWAPI::Unitset transportUnits; BWAPI::Unitset lurkerUnits; //BWAPI::Unitset mutaUnits; + BWAPI::Unitset queenUnits; BWAPI::Unitset overlordUnits; BWAPI::Unitset tankUnits; BWAPI::Unitset medicUnits; @@ -493,12 +508,15 @@ void Squad::addUnitsToMicroManagers() // First grab the defilers, so we know how many there are. // Assign the minimum number of zerglings as food--check each defiler's energy level. - for (const auto unit : _units) + // Remember where one of the defilers is, so we can assign nearby zerglings as food. + BWAPI::Position defilerPos = BWAPI::Positions::None; + for (BWAPI::Unit unit : _units) { if (unit->getType() == BWAPI::UnitTypes::Zerg_Defiler && unit->isCompleted() && unit->exists() && unit->getHitPoints() > 0 && unit->getPosition().isValid()) { defilerUnits.insert(unit); + defilerPos = unit->getPosition(); if (BWAPI::Broodwar->self()->hasResearched(BWAPI::TechTypes::Consume)) { defilerFoodWanted += std::max(0, (199 - unit->getEnergy()) / 50); @@ -506,17 +524,18 @@ void Squad::addUnitsToMicroManagers() } } - for (const auto unit : _units) + for (BWAPI::Unit unit : _units) { if (unit->isCompleted() && unit->exists() && unit->getHitPoints() > 0 && unit->getPosition().isValid()) { - if (defilerFoodWanted > 0 && unit->getType() == BWAPI::UnitTypes::Zerg_Zergling) + /* if (defilerFoodWanted > 0 && unit->getType() == BWAPI::UnitTypes::Zerg_Zergling) { // If no defiler food is wanted, the zergling falls through to the melee micro manager. defilerUnits.insert(unit); --defilerFoodWanted; } - else if (_name == "Overlord" && unit->getType() == BWAPI::UnitTypes::Zerg_Overlord) + else */ + if (_name == "Overlord" && unit->getType() == BWAPI::UnitTypes::Zerg_Overlord) { // Special case for the Overlord squad: All overlords under control of MicroOverlords. overlordUnits.insert(unit); @@ -543,6 +562,10 @@ void Squad::addUnitsToMicroManagers() { scourgeUnits.insert(unit); } + else if (unit->getType() == BWAPI::UnitTypes::Zerg_Queen) + { + queenUnits.insert(unit); + } else if (unit->getType() == BWAPI::UnitTypes::Terran_Medic) { medicUnits.insert(unit); @@ -563,7 +586,7 @@ void Squad::addUnitsToMicroManagers() transportUnits.insert(unit); } // NOTE This excludes spellcasters (except arbiters, which have a regular weapon too). - else if ((unit->getType().groundWeapon().maxRange() > 32) || + else if (unit->getType().groundWeapon().maxRange() > 32 || unit->getType() == BWAPI::UnitTypes::Protoss_Reaver || unit->getType() == BWAPI::UnitTypes::Protoss_Carrier) { @@ -578,14 +601,32 @@ void Squad::addUnitsToMicroManagers() meleeUnits.insert(unit); } // Melee units include firebats, which have range 32. - else if (unit->getType().groundWeapon().maxRange() <= 32) + else if (unit->getType().groundWeapon().maxRange() <= 32 && // melee range + unit->getType().groundWeapon().maxRange() > 0) // but can attack: not a spellcaster { meleeUnits.insert(unit); } - // NOTE Some units may fall through and not be assigned. + // NOTE Some units may fall through and not be assigned. It's intentional. } } + // If we want defiler food, find the nearest zerglings and pull them out of meleeUnits. + while (defilerFoodWanted > 0) + { + BWAPI::Unit food = NearestOf(defilerPos, meleeUnits, BWAPI::UnitTypes::Zerg_Zergling); + if (food) + { + defilerUnits.insert(food); + meleeUnits.erase(food); + --defilerFoodWanted; + } + else + { + // No zerglings left in meleeUnits (though there may be other unit types). + break; + } + } + _microAirToAir.setUnits(airToAirUnits); _microMelee.setUnits(meleeUnits); _microRanged.setUnits(rangedUnits); @@ -596,6 +637,7 @@ void Squad::addUnitsToMicroManagers() _microMedics.setUnits(medicUnits); //_microMutas.setUnits(mutaUnits); _microScourge.setUnits(scourgeUnits); + _microQueens.setUnits(queenUnits); _microOverlords.setUnits(overlordUnits); _microTanks.setUnits(tankUnits); _microTransports.setUnits(transportUnits); @@ -702,13 +744,8 @@ BWAPI::Position Squad::calcRegroupPosition(const UnitCluster & cluster) const BWAPI::Unit nearest = nearbyStaticDefense(vanguard->getPosition()); if (nearest) { - // TODO this should be better but has a bug that can cause retreat toward the enemy - // It's better to retreat to behind the static defense. BWAPI::Position behind = DistanceAndDirection(nearest->getPosition(), cluster.center, -128); return behind; - - // TODO old way, tested but weak - return nearest->getPosition(); } } diff --git a/Steamhammer/Source/Squad.h b/Steamhammer/Source/Squad.h index bdc07a5..12a80a6 100644 --- a/Steamhammer/Source/Squad.h +++ b/Steamhammer/Source/Squad.h @@ -14,6 +14,7 @@ #include "MicroLurkers.h" #include "MicroMedics.h" #include "MicroMutas.h" +#include "MicroQueens.h" #include "MicroScourge.h" #include "MicroOverlords.h" #include "MicroTanks.h" @@ -55,6 +56,7 @@ class Squad MicroLurkers _microLurkers; MicroMedics _microMedics; //MicroMutas _microMutas; + MicroQueens _microQueens; MicroScourge _microScourge; MicroOverlords _microOverlords; MicroTanks _microTanks; @@ -73,6 +75,7 @@ class Squad void setAllUnits(); void setClusterStatus(UnitCluster & cluster); + void microSpecialUnits(const UnitCluster & cluster); void clusterCombat(const UnitCluster & cluster); bool noCombatUnits(const UnitCluster & cluster) const; bool notNearEnemy(const UnitCluster & cluster); diff --git a/Steamhammer/Source/StrategyBossZerg.cpp b/Steamhammer/Source/StrategyBossZerg.cpp index f8eed31..0eb251d 100644 --- a/Steamhammer/Source/StrategyBossZerg.cpp +++ b/Steamhammer/Source/StrategyBossZerg.cpp @@ -35,6 +35,9 @@ StrategyBossZerg::StrategyBossZerg() , enemyGroundArmySize(0) // game starts with no army , enemyAntigroundArmySize(0) // game starts with no army , defilerScore(0) + , _wantDefensiveSpire(false) + , _lastQueenAliveFrame(0) // 0 means no queen ever + , _recommendParasite(false) { resetTechScores(); setUnitMix(BWAPI::UnitTypes::Zerg_Drone, BWAPI::UnitTypes::None); @@ -157,10 +160,16 @@ void StrategyBossZerg::updateGameState() nHydras = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Hydralisk); nLurkers = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Lurker); nMutas = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Mutalisk); + nQueens = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Queen); nGuardians = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Guardian); nDevourers = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Devourer); nDefilers = UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Defiler); + if (nQueens > 0) + { + _lastQueenAliveFrame = BWAPI::Broodwar->getFrameCount(); + } + // Tech stuff. It has to be completed for the tech to be available. nEvo = UnitUtil::GetCompletedUnitCount(BWAPI::UnitTypes::Zerg_Evolution_Chamber); hasPool = UnitUtil::GetCompletedUnitCount(BWAPI::UnitTypes::Zerg_Spawning_Pool) > 0; @@ -347,6 +356,30 @@ bool StrategyBossZerg::enemySeemsToBeDead(const PlayerSnapshot & snap) const return true; } +// When will our spire finish? +// We compare with enemy spire timing to decide whether we need spores. +int StrategyBossZerg::getOurSpireTiming() const +{ + if (hasSpire) + { + // The spire is complete, return an early time. + return 1; + } + + // Assume that there is at most one spire. + for (BWAPI::Unit unit : _self->getUnits()) + { + // If it is a greater spire, then we have spire tech and it's handled above. + if (unit->getType() == BWAPI::UnitTypes::Zerg_Spire) + { + return BWAPI::Broodwar->getFrameCount() + unit->getRemainingBuildTime(); + } + } + + // We haven't started one. Return a distant future time. + return 999999; +} + // How many of our eggs will hatch into the given unit type? // This does not adjust for zerglings or scourge, which are 2 to an egg. int StrategyBossZerg::numInEgg(BWAPI::UnitType type) const @@ -377,20 +410,43 @@ int StrategyBossZerg::mineralsBackOnCancel(BWAPI::UnitType type) const return int(std::floor(0.75 * type.mineralPrice())); } -// Severe emergency: We are out of drones and/or hatcheries. +// Extreme emergency: We are out of drones and/or hatcheries. // Cancel items to release their resources. -// TODO pay attention to priority: the least essential first -// TODO cancel research void StrategyBossZerg::cancelStuff(int mineralsNeeded) { int mineralsSoFar = _self->minerals(); - for (BWAPI::Unit u : _self->getUnits()) + if (mineralsSoFar >= mineralsNeeded) { - if (mineralsSoFar >= mineralsNeeded) + return; + } + + // Upgrades, in order from least to most important. + // ZvZ is the key matchup--the one where we're most likely to be able to recover. + static const std::vector upgrades = + { BWAPI::UpgradeTypes::Pneumatized_Carapace + , BWAPI::UpgradeTypes::Zerg_Missile_Attacks + , BWAPI::UpgradeTypes::Grooved_Spines + , BWAPI::UpgradeTypes::Muscular_Augments + , BWAPI::UpgradeTypes::Zerg_Carapace + , BWAPI::UpgradeTypes::Zerg_Melee_Attacks + , BWAPI::UpgradeTypes::Metabolic_Boost + }; + for (BWAPI::UpgradeType upgrade : upgrades) + { + if (_self->isUpgrading(upgrade)) { - return; + mineralsSoFar += upgrade.mineralPrice(); // ignore possible higher-level upgrades + cancelUpgrade(upgrade); + if (mineralsSoFar >= mineralsNeeded) + { + return; + } } + } + + for (BWAPI::Unit u : _self->getUnits()) + { if (u->getType() == BWAPI::UnitTypes::Zerg_Egg && u->getBuildType() == BWAPI::UnitTypes::Zerg_Overlord) { if (_existingSupply - _supplyUsed >= 6) // enough to add 3 drones @@ -399,17 +455,24 @@ void StrategyBossZerg::cancelStuff(int mineralsNeeded) u->cancelMorph(); } } - else if (u->getType() == BWAPI::UnitTypes::Zerg_Egg && u->getBuildType() != BWAPI::UnitTypes::Zerg_Drone || - u->getType() == BWAPI::UnitTypes::Zerg_Lair && !u->isCompleted() || - u->getType() == BWAPI::UnitTypes::Zerg_Creep_Colony && !u->isCompleted() || - u->getType() == BWAPI::UnitTypes::Zerg_Evolution_Chamber && !u->isCompleted() || - u->getType() == BWAPI::UnitTypes::Zerg_Hydralisk_Den && !u->isCompleted() || - u->getType() == BWAPI::UnitTypes::Zerg_Queens_Nest && !u->isCompleted() || - u->getType() == BWAPI::UnitTypes::Zerg_Hatchery && !u->isCompleted() && nHatches > 1) + else if (u->getType() == BWAPI::UnitTypes::Zerg_Egg && u->getBuildType() != BWAPI::UnitTypes::Zerg_Drone) { mineralsSoFar += u->getType().mineralPrice(); u->cancelMorph(); } + else if (u->getType() == BWAPI::UnitTypes::Zerg_Hatchery && !u->isCompleted() && nHatches > 0 || + u->getType() != BWAPI::UnitTypes::Zerg_Hatchery && u->getType().isBuilding() && !u->isCompleted()) + { + // Canceling a building (including a morphed building like a lair) + // returns 3/4 of the original cost. + // BWAPI::Broodwar->printf("emergency cancel %s", UnitTypeName(u).c_str()); + mineralsSoFar += mineralsBackOnCancel(u->getType()); + u->cancelMorph(); + } + if (mineralsSoFar >= mineralsNeeded) + { + return; + } } } @@ -427,7 +490,10 @@ void StrategyBossZerg::cancelForSpawningPool() // Cancel buildings in the building manager's queue. // They may or may not have started yet. We don't find out if this recovers resources. - BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Hatchery); + if (nHatches > 0) + { + BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Hatchery); + } BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Extractor); BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Evolution_Chamber); @@ -456,6 +522,7 @@ void StrategyBossZerg::cancelForSpawningPool() // Second loop: Cancel what needs it. // When you cancel a building, you get back 75% of its mineral cost, rounded down. bool cancelHatchery = + nHatches > 0 && hatcheries > 0 && extractors * mineralsBackOnCancel(BWAPI::UnitTypes::Zerg_Extractor) + evos * mineralsBackOnCancel(BWAPI::UnitTypes::Zerg_Evolution_Chamber) < mineralsNeeded; for (BWAPI::Unit u : _self->getUnits()) @@ -464,7 +531,8 @@ void StrategyBossZerg::cancelForSpawningPool() { if (u->getType() == BWAPI::UnitTypes::Zerg_Hatchery && u->canCancelMorph()) { - u->cancelMorph(); + // BWAPI::Broodwar->printf("cancel hatchery for spawning pool"); + u->cancelMorph(); break; // we only need to cancel one } } @@ -486,6 +554,20 @@ void StrategyBossZerg::cancelForSpawningPool() } } +// Find what building is performing this upgrade and cancel it. +void StrategyBossZerg::cancelUpgrade(BWAPI::UpgradeType upgrade) +{ + for (BWAPI::Unit unit : _self->getUnits()) + { + if (unit->getUpgrade() == upgrade) + { + unit->cancelUpgrade(); + return; + } + } + UAB_ASSERT(false, "no upgrade"); +} + // The next item in the queue is useless and can be dropped. // Top goal: Do not freeze the production queue by asking the impossible. // But also try to reduce wasted production. @@ -657,9 +739,13 @@ bool StrategyBossZerg::nextInQueueIsUseless(BuildOrderQueue & queue) const // one hatchery is built, causing the next hatchery to be dropped incorrectly. // The error is in BuildingManager. int hatchCount = nHatches + BuildingManager::Instance().getNumUnstarted(BWAPI::UnitTypes::Zerg_Hatchery); + if (nCompletedHatches + hatchCount == 0) + { + return false; // don't cancel our only hatchery! + } + return nDrones < 3 * (1 + hatchCount) - 1 && - minerals <= 300 + 150 * nCompletedHatches && // new hatchery plus minimum production from each existing - nCompletedHatches > 0; // don't cancel our only hatchery! + minerals <= 300 + 150 * nCompletedHatches; // new hatchery plus minimum production from each existing } if (nextInQueue == BWAPI::UnitTypes::Zerg_Lair) { @@ -712,7 +798,7 @@ bool StrategyBossZerg::nextInQueueIsUseless(BuildOrderQueue & queue) const nGas == 0 && gas < 350 || nDrones < 10 || UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Defiler_Mound) > 0 || - isBeingBuilt(BWAPI::UnitTypes::Zerg_Spawning_Pool); + isBeingBuilt(BWAPI::UnitTypes::Zerg_Defiler_Mound); } return false; @@ -779,6 +865,11 @@ bool StrategyBossZerg::nextInQueueIsUseless(BuildOrderQueue & queue) const return nMutas == 0 || !hasGreaterSpire && UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Greater_Spire) == 0; } + if (nextInQueue == BWAPI::UnitTypes::Zerg_Defiler) + { + // We lost the tech. + return UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Defiler_Mound) == 0; + } return false; } @@ -810,7 +901,7 @@ bool StrategyBossZerg::needDroneNext() const { return nDrones < maxDrones && - (enoughArmy() && !_emergencyGroundDefense || Random::Instance().flag(0.1)) && + (enoughArmy() && !_emergencyGroundDefense || Random::Instance().flag(std::min(_economyRatio, 0.15))) && double(_economyDrones) / double(1 + _economyTotal) < _economyRatio; } @@ -983,8 +1074,9 @@ bool StrategyBossZerg::takeUrgentAction(BuildOrderQueue & queue) // Action: If we need money for a spawning pool, cancel any hatchery, extractor, or evo chamber. if (outOfBook && !hasPool && + (minerals < 150 || minerals < 200 && nDrones <= 6) && UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Spawning_Pool) == 0 && - (minerals < 150 || minerals < 200 && nDrones <= 6)) + BuildingManager::Instance().getNumUnstarted(BWAPI::UnitTypes::Zerg_Spawning_Pool) == 0) { cancelForSpawningPool(); } @@ -1063,7 +1155,8 @@ bool StrategyBossZerg::takeUrgentAction(BuildOrderQueue & queue) BuildingManager::Instance().cancelQueuedBuildings(); if (nHatches == 0) { - // No hatcheries either. Queue drones for a hatchery and mining. + // No hatcheries either. Queue drones for a hatchery and mining, and hope. + // NOTE Can't cancel all queued buildings. One is the hatchery that we need. ProductionManager::Instance().goOutOfBookAndClearQueue(); queue.queueAsLowestPriority(BWAPI::UnitTypes::Zerg_Drone); queue.queueAsLowestPriority(BWAPI::UnitTypes::Zerg_Drone); @@ -1076,6 +1169,7 @@ bool StrategyBossZerg::takeUrgentAction(BuildOrderQueue & queue) { // Queue one drone to mine minerals. ProductionManager::Instance().goOutOfBookAndClearQueue(); + BuildingManager::Instance().cancelQueuedBuildings(); queue.queueAsLowestPriority(BWAPI::UnitTypes::Zerg_Drone); cancelStuff(50); } @@ -1121,7 +1215,6 @@ bool StrategyBossZerg::takeUrgentAction(BuildOrderQueue & queue) } // There are no drones on minerals. Turn off gas collection. - // TODO more efficient test in WorkerMan if (_lastUpdateFrame >= 24 && // give it time! WorkerManager::Instance().isCollectingGas() && nMineralPatches > 0 && @@ -1135,6 +1228,7 @@ bool StrategyBossZerg::takeUrgentAction(BuildOrderQueue & queue) WorkerManager::Instance().setCollectGas(false); if (nHatches >= 2) { + // BWAPI::Broodwar->printf("no drones on minerals, cancel hatchery"); BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Hatchery); } return true; @@ -1168,18 +1262,12 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) (!outOfBook || queue.size() < 5)) // don't loop beyond this { // Not too much, and not too much at once. They cost a lot of gas. - int nScourgeNeeded = std::min(18, InformationManager::Instance().nScourgeNeeded()); + int nScourgeNeeded = std::min(12, InformationManager::Instance().nScourgeNeeded()); int nToMake = 0; if (nScourgeNeeded > totalScourge && nLarvas > 0) { int nPairs = std::min(1 + gas / 75, (nScourgeNeeded - totalScourge + 1) / 2); - int limit = 3; // how many pairs at a time, max? - if (nLarvas > 6 && gas > 6 * 75) - { - // Allow more if we have plenty of resources. - limit = 6; - } - nToMake = std::min(nPairs, limit); + nToMake = std::min(nPairs, 3); } for (int i = 0; i < nToMake; ++i) { @@ -1196,12 +1284,13 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) // This ties in via ELSE with the next check! if (outOfBook && WorkerManager::Instance().isCollectingGas() && - gas >= queueGas && gas > 300 && gas > 3 * _self->minerals() && // raw mineral count, not adjusted for building reserve + gas >= queueGas && nDrones <= maxDrones - nGasDrones && // no drones will become idle TODO this apparently doesn't work right WorkerManager::Instance().getNumIdleWorkers() == 0) // no drones are already idle (redundant double-check) { + //BWAPI::Broodwar->printf("excess gas"); WorkerManager::Instance().setCollectGas(false); // And keep going. } @@ -1214,7 +1303,7 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) if (nGas == 0 || nDrones < 9 && _self->deadUnitCount(BWAPI::UnitTypes::Zerg_Drone) > 1) // it's OK to lose the scout { // Emergency. Give up and clear the queue. - // BWAPI::Broodwar->printf("need more gas - breaking out"); + //BWAPI::Broodwar->printf("need more gas - breaking out"); ProductionManager::Instance().goOutOfBookAndClearQueue(); return; } @@ -1228,6 +1317,7 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) WorkerManager::Instance().isCollectingGas()) { // Deadlock. Can't get gas. Give up and clear the queue. + //BWAPI::Broodwar->printf("gas deadlock, clear queue"); ProductionManager::Instance().goOutOfBookAndClearQueue(); return; } @@ -1292,6 +1382,7 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) unit->isCompleted() && (!_emergencyGroundDefense || unit->getHitPoints() >= 130)) // not during attack if it will end up weak { + // NOTE The colony that gets morphed may not be the one whose stats we just checked. queue.queueAsHighestPriority(BWAPI::UnitTypes::Zerg_Sunken_Colony); } } @@ -1327,10 +1418,13 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) (hasSpire || findRemainingBuildTime(BWAPI::UnitTypes::Zerg_Spire) >= 20 * 24)) // spire not about to finish { MacroLocation loc = MacroLocation::Macro; - if (nHatches % 2 != 0 && nFreeBases > 2 && Random::Instance().flag(0.5)) + if (nBases <= 1 && nHatches >= 2 && nFreeBases > 0 || + nBases <= 2 && nHatches >= 3 && nFreeBases > 0 || + nHatches % 2 != 0 && nFreeBases > 2 && Random::Instance().flag(0.5)) { - // Expand with some macro hatcheries unless it's late game. - loc = MacroLocation::MinOnly; + // Expand with some macro hatcheries. + // If we're expanding and have only one gas, insist on a second gas base. + loc = nGas >= 2 ? MacroLocation::MinOnly : MacroLocation::Expo; } queue.queueAsHighestPriority(MacroAct(BWAPI::UnitTypes::Zerg_Hatchery, loc)); // And keep going. @@ -1425,15 +1519,19 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) else { // ZvZ - int enemyMutas = + const int enemyMutas = InformationManager::Instance().getNumUnits(BWAPI::UnitTypes::Zerg_Mutalisk, _enemy); - if (enemyMutas > nMutas) + // When did, or will, the enemy spire finish? If before now, pretend it is now. + const int enemySpireTime = std::max( + BWAPI::Broodwar->getFrameCount(), + InformationManager::Instance().getEnemyBuildingTiming(BWAPI::UnitTypes::Zerg_Spire) + ); + if (enemyMutas > nMutas || getOurSpireTiming() > enemySpireTime + 30 * 24) { spores = nBases > 1 ? 2 : 1; } - } - - // BWAPI::Broodwar->printf("spores %d ~ nSpores %d", spores, nSpores); + //BWAPI::Broodwar->printf("%d vs %d : spores %d -> %d", nMutas, enemyMutas, spores, nSpores); + } if (spores > nSpores) { @@ -1463,15 +1561,15 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) } } } - + // Prepare the evo as soon as the enemy has the tech, so the spores can go up ASAP. + // In ZvZ, don't make an evo unless we expect to need it. if (nEvo == 0 && nDrones >= 9 && hasPool && + (_enemyRace != BWAPI::Races::Zerg || spores >= nSpores) && !queue.anyInQueue(BWAPI::UnitTypes::Zerg_Evolution_Chamber) && UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Evolution_Chamber) == 0 && !isBeingBuilt(BWAPI::UnitTypes::Zerg_Evolution_Chamber)) { - // BWAPI::Broodwar->printf("air defense evo"); - queue.queueAsHighestPriority(BWAPI::UnitTypes::Zerg_Evolution_Chamber); } @@ -1480,7 +1578,7 @@ void StrategyBossZerg::makeUrgentReaction(BuildOrderQueue & queue) { // Also put up a spire if possible. if (!hasSpire && hasLairTech && outOfBook && - minerals >= 200 && gas >= 150 && nGas > 0 && nDrones > 9 && + nGas > 0 && nDrones > 9 && !queue.anyInQueue(BWAPI::UnitTypes::Zerg_Spire) && UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Spire) == 0 && !isBeingBuilt(BWAPI::UnitTypes::Zerg_Spire)) @@ -1633,7 +1731,10 @@ void StrategyBossZerg::checkGroundDefenses(BuildOrderQueue & queue) int enemyAcademyUnits = 0; // count firebats and medics int enemyVultures = 0; bool enemyHasDrop = false; - for (const auto & kv : InformationManager::Instance().getUnitData(_enemy).getUnits()) + + _wantDefensiveSpire = false; // also update this global + + for (const auto & kv : InformationManager::Instance().getUnitData(_enemy).getUnits()) { const UnitInfo & ui(kv.second); @@ -1642,10 +1743,10 @@ void StrategyBossZerg::checkGroundDefenses(BuildOrderQueue & queue) !ui.type.isFlyer()) { int power = ui.type.supplyRequired(); - // Some types count a little more. + // Some types count more. if (ui.type == BWAPI::UnitTypes::Protoss_Dark_Templar) { - power += 1; + power += 4; } else if (ui.type == BWAPI::UnitTypes::Protoss_Dragoon) { @@ -1682,7 +1783,15 @@ void StrategyBossZerg::checkGroundDefenses(BuildOrderQueue & queue) else if (ui.type == BWAPI::UnitTypes::Terran_Dropship || ui.type == BWAPI::UnitTypes::Protoss_Shuttle) { enemyHasDrop = true; + _wantDefensiveSpire = true; } + else if (ui.type == BWAPI::UnitTypes::Terran_Science_Vessel || + ui.type == BWAPI::UnitTypes::Protoss_Arbiter || + ui.type == BWAPI::UnitTypes::Protoss_Carrier) + { + // NOTE Other cases are taken care of by the reaction to overlord hunters. + _wantDefensiveSpire = true; + } } } if (enemyDragoons >= 4) @@ -1770,15 +1879,14 @@ void StrategyBossZerg::checkGroundDefenses(BuildOrderQueue & queue) (enemyVultures > 0 || enemyHasDrop) && totalSunkens == 0; const bool inProgress = - BuildingManager::Instance().getNumUnstarted(BWAPI::UnitTypes::Zerg_Sunken_Colony) > 0 || BuildingManager::Instance().getNumUnstarted(BWAPI::UnitTypes::Zerg_Creep_Colony) > 0 || UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Creep_Colony) > 0 || UnitUtil::GetUncompletedUnitCount(BWAPI::UnitTypes::Zerg_Sunken_Colony) > 0; if (makeOne && !inProgress) { - if (nDrones < 24 || - nDrones < 32 && _enemyRace != BWAPI::Races::Zerg) + if (nDrones < 16 || + nDrones < 24 && _enemyRace != BWAPI::Races::Zerg) { // Only fill up the drone count if we're not high yet. queue.queueAsHighestPriority(BWAPI::UnitTypes::Zerg_Drone); @@ -2002,6 +2110,16 @@ void StrategyBossZerg::setAvailableTechUnits(std::array= 2; } +// Set flags to recommend that certain abilities are worth having. +// So far, this only looks for juicy protoss parasite targets, a hint to get a queen. +void StrategyBossZerg::recommendResearch(const PlayerSnapshot & snap) +{ + _recommendParasite = _enemyRace == BWAPI::Races::Protoss && + (snap.getCount(BWAPI::UnitTypes::Protoss_Arbiter) > 0 || + snap.getCount(BWAPI::UnitTypes::Protoss_Carrier) > 0 || + snap.getCount(BWAPI::UnitTypes::Protoss_Shuttle) > 0); +} + void StrategyBossZerg::vProtossTechScores(const PlayerSnapshot & snap) { _wantAirArmor = snap.getCount(BWAPI::UnitTypes::Protoss_Corsair) >= 5; @@ -2318,9 +2436,13 @@ void StrategyBossZerg::vZergTechScores(const PlayerSnapshot & snap) techScores[int(TechUnit::Lurkers)] += count; } } - else if (type == BWAPI::UnitTypes::Zerg_Lurker) + else if (type == BWAPI::UnitTypes::Zerg_Hydralisk) + { + techScores[int(TechUnit::Mutalisks)] += count * 2; + } + else if (type == BWAPI::UnitTypes::Zerg_Lurker) { - techScores[int(TechUnit::Mutalisks)] += count * 3; + techScores[int(TechUnit::Mutalisks)] += count * 4; techScores[int(TechUnit::Guardians)] += count * 3; } else if (type == BWAPI::UnitTypes::Zerg_Mutalisk) @@ -2333,8 +2455,8 @@ void StrategyBossZerg::vZergTechScores(const PlayerSnapshot & snap) else if (type == BWAPI::UnitTypes::Zerg_Scourge) { techScores[int(TechUnit::Ultralisks)] += count; - techScores[int(TechUnit::Guardians)] -= count; - techScores[int(TechUnit::Devourers)] -= count; + techScores[int(TechUnit::Guardians)] -= count * 2; + techScores[int(TechUnit::Devourers)] -= count * 2; } else if (type == BWAPI::UnitTypes::Zerg_Guardian) { @@ -2368,6 +2490,8 @@ void StrategyBossZerg::calculateTechScores(int lookaheadFrames) return; } + recommendResearch(snap); + if (_enemyRace == BWAPI::Races::Protoss) { vProtossTechScores(snap); @@ -2704,7 +2828,7 @@ void StrategyBossZerg::chooseUnitMix() void StrategyBossZerg::chooseAuxUnit() { const int maxAuxGuardians = 8; - const int maxAuxDevourers = 4; + const int maxAuxDevourers = std::min(4, nMutas / 2); // The default is no aux unit. _auxUnit = BWAPI::UnitTypes::None; @@ -2853,7 +2977,7 @@ void StrategyBossZerg::produceUnits(int & mineralsLeft, int & gasLeft) if (_gasUnit == BWAPI::UnitTypes::None || gas < _gasUnit.gasPrice() || double(numMineralUnits) / double(numGasUnits) < 0.2 || - _gasUnit == BWAPI::UnitTypes::Zerg_Devourer && nDevourers >= maxDevourers) + _gasUnit == BWAPI::UnitTypes::Zerg_Devourer && nDevourers >= std::min(maxDevourers, nMutas / 2)) { // Only the mineral unit. while (larvasLeft >= 0 && mineralsLeft >= 0 && gasLeft >= 0) @@ -3040,8 +3164,8 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool // Make a spire. Make it earlier in ZvZ. if (!hasSpire && hasLairTech && nGas > 0 && - airTechUnit(_techTarget) && - (!hiveTechUnit(_techTarget) || UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Hive) > 0) && + (_wantDefensiveSpire || airTechUnit(_techTarget)) && + (_wantDefensiveSpire || !hiveTechUnit(_techTarget) || UnitUtil::GetAllUnitCount(BWAPI::UnitTypes::Zerg_Hive) > 0) && (nDrones >= 13 || _enemyRace == BWAPI::Races::Zerg && nDrones >= 9) && hasEnoughUnits && (!_emergencyGroundDefense || gasLeft >= 75) && @@ -3099,7 +3223,7 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool } // Make a queen's nest. - // Make it later versus zerg. TODO Also against terran going mech. + // Make it later versus zerg. // Wait until pneumatized carapace is done (except versus zerg, when we don't get that), // because the bot has often been getting a queen's nest too early. if (!hasQueensNest && hasLair && nGas >= 2 && nDrones >= 24 && @@ -3165,8 +3289,8 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool gasLeft -= 150; } } - - // We want to expand. + + // We want t` expand. // Division of labor: Expansions are here, macro hatcheries are "urgent production issues". // However, macro hatcheries may be placed at expansions. // Don't expand if we're getting rushed and it's not safe. @@ -3246,10 +3370,10 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool { addExtractor = true; } - // E. If we're aiming for lair tech, get at least 2 extractors, if available. - // If for hive tech, get at least 3. + // E. If we have lair tech, get at least 2 extractors. + // If aiming for hive tech, get at least 3. // Doesn't break 1-base tech strategies, because then the geyser is not available. - else if (lairTechUnit(_techTarget) && nGas < 2 && nDrones >= 12 || + else if (hasLairTech && nGas < 2 && nDrones >= 12 || hiveTechUnit(_techTarget) && nGas < 3 && nDrones >= 18) { addExtractor = true; @@ -3292,7 +3416,6 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool // If we're in reasonable shape, get carapace upgrades. // On islands, we go air so don't get ground upgrades before hive. - // Coordinate upgrades with the nextInQueueIsUseless() check. if (nEvo > 0 && nDrones >= 12 && nGas > 0 && hasPool && !_emergencyGroundDefense && hasEnoughUnits && @@ -3303,7 +3426,7 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool armorUps == 1 && hasLairTech || armorUps == 2 && hasHiveTech) { - // But delay if we're going mutas and don't have many yet. They want the resources. + // But delay if we're going mutas and don't have many. They want the resources. if (!(hasSpire && _gasUnit == BWAPI::UnitTypes::Zerg_Mutalisk && nMutas < 6)) { produce(BWAPI::UpgradeTypes::Zerg_Carapace); @@ -3314,7 +3437,6 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool } // If we have 2 evos, or if carapace upgrades are done, also get melee attack. - // Coordinate upgrades with the nextInQueueIsUseless() check. if ((nEvo >= 2 || nEvo > 0 && armorUps == 3) && nDrones >= 14 && nGas >= 2 && hasPool && (hasDen || hasSpire || hasUltra) && !_emergencyGroundDefense && hasEnoughUnits && @@ -3371,6 +3493,28 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool */ } + // Possibly make one queen, if we already have a queen's nest anyway. + if (hasQueensNest && nQueens == 0 && + // Not if we're short of other units. + !_emergencyGroundDefense && !_emergencyNow && enoughArmy() && + // Not if the enemy is apparently on their last legs. + enemyGroundArmySize > 0 && enemyAntigroundArmySize > 0 && Bases::Instance().baseCount(_enemy) >= 2 && + // Not if we formerly had a queen and it died too recently. Leave a gap in time. + // (If we never had a queen, _lastQueenAliveFrame is 0 and the check is "is the game long enough?") + BWAPI::Broodwar->getFrameCount() - _lastQueenAliveFrame > 3*60*24) + { + // Always versus terran. We may be able to infest a command center. + // Only versus protoss if they have a unit we may want to parasite. + // Never versus zerg, though it could make sense if we get to hive tech. + if (_enemyRace == BWAPI::Races::Terran || + _enemyRace == BWAPI::Races::Protoss && _recommendParasite) + { + produce(BWAPI::UnitTypes::Zerg_Queen); + mineralsLeft -= 100; + gasLeft -= 100; + } + } + // In the late game, add defilers. // Add them later if the defilerScore is negative--currently, only terran sets it other than 0. // Get defilers and consume even if we are in a state of emergency. We need defilers then! @@ -3405,8 +3549,10 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool mineralsLeft -= 200; gasLeft -= 200; } - else if (_self->hasResearched(BWAPI::TechTypes::Plague) && - !_self->getUpgradeLevel(BWAPI::UpgradeTypes::Metasynaptic_Node) == 0 && + else if (nDrones >= 50 && + enoughArmy() && enoughGroundArmy() && + _self->hasResearched(BWAPI::TechTypes::Plague) && + _self->getUpgradeLevel(BWAPI::UpgradeTypes::Metasynaptic_Node) == 0 && !_self->isUpgrading(BWAPI::UpgradeTypes::Metasynaptic_Node)) { produce(BWAPI::UpgradeTypes::Metasynaptic_Node); @@ -3418,10 +3564,10 @@ void StrategyBossZerg::produceOtherStuff(int & mineralsLeft, int & gasLeft, bool // Make only one defiler. Defiler micro is computationally expensive. // Special case: Treat defilers as if they were tech, not units. - if (hasDefilerUps && nDefilers == 0 && !_emergencyGroundDefense && nGas >= 3) + if (hasDefilerUps && nDefilers == 0 && !_emergencyGroundDefense && nGas >= 2) { produce(BWAPI::UnitTypes::Zerg_Defiler); - mineralsLeft -= 50; + mineralsLeft -= 50; gasLeft -= 150; nLarvas -= 1; } @@ -3577,6 +3723,7 @@ void StrategyBossZerg::handleUrgentProductionIssues(BuildOrderQueue & queue) // We only cancel a hatchery in case of dire emergency. Get the scout drone back home. ScoutManager::Instance().releaseWorkerScout(); // Also cancel hatcheries already sent away for. + // BWAPI::Broodwar->printf("cancel useless hatchery"); BuildingManager::Instance().cancelBuildingType(BWAPI::UnitTypes::Zerg_Hatchery); } else if (nextInQueue == BWAPI::UnitTypes::Zerg_Lair || diff --git a/Steamhammer/Source/StrategyBossZerg.h b/Steamhammer/Source/StrategyBossZerg.h index 459210f..c421805 100644 --- a/Steamhammer/Source/StrategyBossZerg.h +++ b/Steamhammer/Source/StrategyBossZerg.h @@ -92,6 +92,7 @@ class StrategyBossZerg int nHydras; int nLurkers; int nMutas; + int nQueens; int nGuardians; int nDevourers; int nDefilers; @@ -128,6 +129,11 @@ class StrategyBossZerg int enemyAntigroundArmySize; int defilerScore; + bool _wantDefensiveSpire; + + int _lastQueenAliveFrame; // last frame we had a living queen (now if one is still alive) + bool _recommendParasite; + // For choosing the tech target and the unit mix. std::array techScores; @@ -139,12 +145,15 @@ class StrategyBossZerg bool enoughGroundArmy() const; bool enemySeemsToBeDead(const PlayerSnapshot & snap) const; + int getOurSpireTiming() const; + int numInEgg(BWAPI::UnitType) const; bool isBeingBuilt(const BWAPI::UnitType unitType) const; int mineralsBackOnCancel(BWAPI::UnitType type) const; void cancelStuff(int mineralsNeeded); void cancelForSpawningPool(); + void cancelUpgrade(BWAPI::UpgradeType upgrade); bool nextInQueueIsUseless(BuildOrderQueue & queue) const; void leaveBook(); @@ -176,6 +185,7 @@ class StrategyBossZerg void resetTechScores(); void setAvailableTechUnits(std::array & available); + void recommendResearch(const PlayerSnapshot & snap); void vProtossTechScores(const PlayerSnapshot & snap); void vTerranTechScores(const PlayerSnapshot & snap); void vZergTechScores(const PlayerSnapshot & snap); diff --git a/Steamhammer/Source/The.cpp b/Steamhammer/Source/The.cpp index b121be2..6a25190 100644 --- a/Steamhammer/Source/The.cpp +++ b/Steamhammer/Source/The.cpp @@ -18,11 +18,21 @@ void The::initialize() vWalkRoom.initialize(); // depends on edgeRange tileRoom.initialize(); // depends on vWalkRoom zone.initialize(); // depends on tileRoom - //regions.initialize(); // depends on everything before ops.initialize(); } +void The::update() +{ + int now = BWAPI::Broodwar->getFrameCount(); + + if (now > 45 * 24 && now % 10 == 0) + { + groundAttacks.update(); + airAttacks.update(); + } +} + The & The::Root() { static The root; diff --git a/Steamhammer/Source/The.h b/Steamhammer/Source/The.h index 9267588..7b5e62d 100644 --- a/Steamhammer/Source/The.h +++ b/Steamhammer/Source/The.h @@ -1,5 +1,6 @@ #pragma once +#include "GridAttacks.h" #include "GridInset.h" #include "GridRoom.h" #include "GridTileRoom.h" @@ -24,14 +25,23 @@ namespace UAlbertaBot BWAPI::Player enemy() const { return BWAPI::Broodwar->enemy(); }; BWAPI::Player neutral() const { return BWAPI::Broodwar->neutral(); }; + // Map information. GridRoom vWalkRoom; GridTileRoom tileRoom; GridInset inset; GridZone zone; + MapPartitions partitions; + + // Managers. Micro micro; OpsBoss ops; - MapPartitions partitions; - //Regions regions; + + // Varying during the game. + GroundAttacks groundAttacks; + AirAttacks airAttacks; + + // Update the varying values. + void update(); static The & Root(); }; diff --git a/Steamhammer/Source/UnitData.cpp b/Steamhammer/Source/UnitData.cpp index 621d763..74ff33c 100644 --- a/Steamhammer/Source/UnitData.cpp +++ b/Steamhammer/Source/UnitData.cpp @@ -5,6 +5,75 @@ using namespace UAlbertaBot; // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +UnitInfo::UnitInfo() + : unitID(0) + , updateFrame(0) + , lastHP(0) + , lastShields(0) + , player(nullptr) + , unit(nullptr) + , lastPosition(BWAPI::Positions::None) + , goneFromLastPosition(false) + , burrowed(false) + , type(BWAPI::UnitTypes::None) + , completeBy(0) + , completed(false) +{ +} + +UnitInfo::UnitInfo(BWAPI::Unit unit) + : unitID(unit->getID()) + , updateFrame(BWAPI::Broodwar->getFrameCount()) + , lastHP(unit->getHitPoints()) + , lastShields(unit->getShields()) + , player(unit->getPlayer()) + , unit(unit) + , lastPosition(unit->getPosition()) + , goneFromLastPosition(false) + , burrowed(false) + , type(unit->getType()) + , completeBy(predictCompletion(unit)) + , completed(unit->isCompleted()) +{ +} + +const int UnitInfo::predictCompletion(BWAPI::Unit building) const +{ + if (unit->isCompleted()) + { + return BWAPI::Broodwar->getFrameCount(); + } + + if (unit->getType().isBuilding()) + { + // Interpolate the HP to predict the completion time. + // This only works for buildings. + // NOTE Buildings generally finish later than predicted. + // The prediction approaches the truth as construction progresses. + // NOTE If the building was damanged by attack, the prediction will be even worse. + double hpRatio = unit->getHitPoints() / double(unit->getType().maxHitPoints()); + return BWAPI::Broodwar->getFrameCount() + int((1.0 - hpRatio) * unit->getType().buildTime()); + } + + // Assume the unit is only now starting. This will often be wrong. + return BWAPI::Broodwar->getFrameCount() + unit->getType().buildTime(); +} + +const bool UnitInfo::operator == (BWAPI::Unit unit) const +{ + return unitID == unit->getID(); +} + +const bool UnitInfo::operator == (const UnitInfo & rhs) const +{ + return unitID == rhs.unitID; +} + +const bool UnitInfo::operator < (const UnitInfo & rhs) const +{ + return unitID < rhs.unitID; +} + // These routines estimate HP and/or shields of the unit, which may not have been seen for some time. // Account for shield regeneration and zerg regeneration, but not terran healing or repair or burning. // Regeneration rates are calculated from info at http://www.starcraftai.com/wiki/Regeneration @@ -75,7 +144,7 @@ UnitData::UnitData() } // An enemy unit which is not visible, but whose lastPosition can be seen, is known -// not to be at its lastPosition. Flag it. +// to be gone from its lastPosition. Flag it. // A complication: A burrowed unit may still be at its last position. Try to keep track. // Called from InformationManager with the enemy UnitData. void UnitData::updateGoneFromLastPosition() @@ -99,7 +168,7 @@ void UnitData::updateGoneFromLastPosition() if (ui.type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine) { // Burrowed spider mines are tricky. If the mine is detected, isBurrowed() is true. - // But we can't tell when the spider mine is burrowing or upburrowing; its order + // But we can't tell when the spider mine is burrowing or unburrowing; its order // is always BWAPI::Orders::VultureMine. So we assume that a mine which goes out // of vision has burrowed and is undetected. It can be wrong. ui.burrowed = true; @@ -116,26 +185,47 @@ void UnitData::updateGoneFromLastPosition() void UnitData::updateUnit(BWAPI::Unit unit) { - if (!unit) { return; } + if (!unit || !unit->isVisible()) { return; } if (unitMap.find(unit) == unitMap.end()) { ++numUnits[unit->getType().getID()]; - unitMap[unit] = UnitInfo(); + unitMap[unit] = UnitInfo(unit); } + else + { + UnitInfo & ui = unitMap[unit]; + + ui.unitID = unit->getID(); + ui.updateFrame = BWAPI::Broodwar->getFrameCount(); + ui.lastHP = unit->getHitPoints(); + ui.lastShields = unit->getShields(); + ui.player = unit->getPlayer(); + ui.unit = unit; + ui.lastPosition = unit->getPosition(); + ui.goneFromLastPosition = false; + ui.burrowed = unit->isBurrowed() || unit->getOrder() == BWAPI::Orders::Burrowing; + ui.type = unit->getType(); - UnitInfo & ui = unitMap[unit]; - ui.unit = unit; - ui.updateFrame = BWAPI::Broodwar->getFrameCount(); - ui.player = unit->getPlayer(); - ui.lastPosition = unit->getPosition(); - ui.goneFromLastPosition = false; - ui.burrowed = unit->isBurrowed() || unit->getOrder() == BWAPI::Orders::Burrowing; - ui.lastHP = unit->getHitPoints(); - ui.lastShields = unit->getShields(); - ui.unitID = unit->getID(); - ui.type = unit->getType(); - ui.completed = unit->isCompleted(); + // Update ui.completeBy before ui.completed. + if (!ui.completed && ui.type.isBuilding()) // other units keep their earliest predictions + { + if (unit->isCompleted()) + { + if (ui.completeBy + 100 > BWAPI::Broodwar->getFrameCount()) + { + // This is the true completion time, or not far off. + ui.completeBy = BWAPI::Broodwar->getFrameCount(); + } + // Otherwise it has been a long time, so keep the older predicted completion time. + } + else + { + ui.completeBy = ui.predictCompletion(unit); + } + } + ui.completed = unit->isCompleted(); + } } void UnitData::removeUnit(BWAPI::Unit unit) diff --git a/Steamhammer/Source/UnitData.h b/Steamhammer/Source/UnitData.h index c5ce5c4..44c2d15 100644 --- a/Steamhammer/Source/UnitData.h +++ b/Steamhammer/Source/UnitData.h @@ -16,53 +16,20 @@ struct UnitInfo BWAPI::Player player; BWAPI::Unit unit; BWAPI::Position lastPosition; - bool goneFromLastPosition; // last position was seen, and it wasn't there - bool burrowed; // believed to be burrowed (or burrowing) at this position + bool goneFromLastPosition; // last position was seen, and it wasn't there + bool burrowed; // believed to be burrowed (or burrowing) at this position BWAPI::UnitType type; - bool completed; - - UnitInfo() - : unitID(0) - , updateFrame(0) - , lastHP(0) - , lastShields(0) - , player(nullptr) - , unit(nullptr) - , lastPosition(BWAPI::Positions::None) - , goneFromLastPosition(false) - , type(BWAPI::UnitTypes::None) - , completed(false) - { - } - - UnitInfo(BWAPI::Unit unit) - : unitID(unit->getID()) - , updateFrame(BWAPI::Broodwar->getFrameCount()) - , lastHP(unit->getHitPoints()) - , lastShields(unit->getShields()) - , player(unit->getPlayer()) - , unit(unit) - , lastPosition(unit->getPosition()) - , goneFromLastPosition(false) - , type(unit->getType()) - , completed(unit->isCompleted()) - { - } - - const bool operator == (BWAPI::Unit unit) const - { - return unitID == unit->getID(); - } - - const bool operator == (const UnitInfo & rhs) const - { - return unitID == rhs.unitID; - } - - const bool operator < (const UnitInfo & rhs) const - { - return unitID < rhs.unitID; - } + int completeBy; // past frame known or future frame predicted + bool completed; + + UnitInfo(); + UnitInfo(BWAPI::Unit unit); + + const int predictCompletion(BWAPI::Unit building) const; + + const bool operator == (BWAPI::Unit unit) const; + const bool operator == (const UnitInfo & rhs) const; + const bool operator < (const UnitInfo & rhs) const; int estimateHP() const; int estimateShields() const; diff --git a/Steamhammer/Source/UnitUtil.cpp b/Steamhammer/Source/UnitUtil.cpp index 101bdf0..c9ac90b 100644 --- a/Steamhammer/Source/UnitUtil.cpp +++ b/Steamhammer/Source/UnitUtil.cpp @@ -141,6 +141,20 @@ bool UnitUtil::IsCombatUnit(BWAPI::Unit unit) return unit && unit->isCompleted() && IsCombatUnit(unit->getType()); } +bool UnitUtil::IsSuicideUnit(BWAPI::UnitType type) +{ + return + type == BWAPI::UnitTypes::Terran_Vulture_Spider_Mine || + type == BWAPI::UnitTypes::Protoss_Scarab || + type == BWAPI::UnitTypes::Zerg_Scourge || + type == BWAPI::UnitTypes::Zerg_Infested_Terran; +} + +bool UnitUtil::IsSuicideUnit(BWAPI::Unit unit) +{ + return IsSuicideUnit(unit->getType()); +} + // Check whether a unit variable points to a unit we control. // This is called only on units that we believe are ours (we may be wrong if it was mind controlled). bool UnitUtil::IsValidUnit(BWAPI::Unit unit) @@ -203,6 +217,8 @@ bool UnitUtil::TypeCanAttackGround(BWAPI::UnitType attacker) attacker == BWAPI::UnitTypes::Protoss_Reaver; } +// Does the unit type have any attack? +// This test has a different meaning than CanAttack(unit, target) above. bool UnitUtil::TypeCanAttack(BWAPI::UnitType type) { return TypeCanAttackGround(type) || TypeCanAttackAir(type); @@ -457,6 +473,45 @@ int UnitUtil::GetWeaponDamageToWorker(BWAPI::Unit attacker) return damage; } +// Check whether the unit type can hit targets that are covered by dark swarm. +// A ranged unit can do no direct damage under swarm, only splash damage. +// NOTE Spells work under dark swarm. This routine doesn't handle that. +bool UnitUtil::HitsUnderSwarm(BWAPI::UnitType type) +{ + if (type.isWorker() || type.isBuilding()) + { + // Workers cannot cause damage under swarm, though they have melee range. + // No defensive buildings can hit under dark swarm, except a bunker containing firebats. + // We ignore the bunker exception. + return false; + } + + if (type == BWAPI::UnitTypes::Terran_Siege_Tank_Siege_Mode || + type == BWAPI::UnitTypes::Protoss_Reaver || + type == BWAPI::UnitTypes::Protoss_Archon || + type == BWAPI::UnitTypes::Zerg_Lurker) + { + // Ranged units that do splash damage. + return true; + } + + if (type.groundWeapon() == BWAPI::WeaponTypes::None || + type.groundWeapon().maxRange() > 32) + { + // Units that can't attack ground, plus non-splash ranged units. + return false; + } + + // Spider mines, firebats, zealots, zerglings, etc. + return true; +} + +// Check whether the unit can hit targets that are covered by dark swarm. +bool UnitUtil::HitsUnderSwarm(BWAPI::Unit unit) +{ + return HitsUnderSwarm(unit->getType()); +} + // The unit has an order that might lead it to attack. // NOTE The list may be incomplete. It also deliberately ignores spell casting. // NOTE A spider mine has order VultureMine no matter what it is doing. @@ -472,6 +527,22 @@ bool UnitUtil::AttackOrder(BWAPI::Unit unit) order == BWAPI::Orders::ScarabAttack; } +// Return the unit or building's detection range, 0 if it is not a detector. +// Detection range is independent of sight range. To detect a cloaked enemy, you +// need to see it also: Any unit in sight range of it, plus a detector in detection range. +int UnitUtil::GetDetectionRange(BWAPI::UnitType type) +{ + if (type.isDetector()) + { + if (type.isBuilding()) + { + return 7; + } + return 11; + } + return 0; +} + // All our units, whether completed or not. int UnitUtil::GetAllUnitCount(BWAPI::UnitType type) { diff --git a/Steamhammer/Source/UnitUtil.h b/Steamhammer/Source/UnitUtil.h index b33680c..6eb611f 100644 --- a/Steamhammer/Source/UnitUtil.h +++ b/Steamhammer/Source/UnitUtil.h @@ -21,8 +21,10 @@ namespace UnitUtil bool IsCombatSimUnit(BWAPI::UnitType type); bool IsCombatUnit(BWAPI::UnitType type); bool IsCombatUnit(BWAPI::Unit unit); + bool IsSuicideUnit(BWAPI::UnitType type); + bool IsSuicideUnit(BWAPI::Unit unit); bool IsValidUnit(BWAPI::Unit unit); - + // Damage per frame (formerly CalculateLDT()). double DPF(BWAPI::Unit attacker, BWAPI::Unit target); double GroundDPF(BWAPI::Player player, BWAPI::UnitType type); @@ -52,8 +54,13 @@ namespace UnitUtil int FramesToReachAttackRange(BWAPI::Unit attacker, BWAPI::Unit target); int GetWeaponDamageToWorker(BWAPI::Unit attacker); + bool HitsUnderSwarm(BWAPI::UnitType type); + bool HitsUnderSwarm(BWAPI::Unit unit); + bool AttackOrder(BWAPI::Unit unit); + int GetDetectionRange(BWAPI::UnitType type); + int GetAllUnitCount(BWAPI::UnitType type); int GetCompletedUnitCount(BWAPI::UnitType type); int GetUncompletedUnitCount(BWAPI::UnitType type); diff --git a/Steamhammer/Source/WorkerData.cpp b/Steamhammer/Source/WorkerData.cpp index 642b3f5..2530847 100644 --- a/Steamhammer/Source/WorkerData.cpp +++ b/Steamhammer/Source/WorkerData.cpp @@ -450,16 +450,13 @@ BWAPI::UnitType WorkerData::getWorkerBuildingType(BWAPI::Unit unit) return BWAPI::UnitTypes::None; } -int WorkerData::getNumAssignedWorkers(BWAPI::Unit unit) +int WorkerData::getNumAssignedWorkers(BWAPI::Unit unit) const { if (!unit) { return 0; } - std::map::iterator it; - - // if the worker is mining, set the iterator to the mineral map if (unit->getType().isResourceDepot()) { - it = depotWorkerCount.find(unit); + auto it = depotWorkerCount.find(unit); // if there is an entry, return it if (it != depotWorkerCount.end()) @@ -469,21 +466,17 @@ int WorkerData::getNumAssignedWorkers(BWAPI::Unit unit) } else if (unit->getType().isRefinery()) { - it = refineryWorkerCount.find(unit); + auto it = refineryWorkerCount.find(unit); // if there is an entry, return it if (it != refineryWorkerCount.end()) { return it->second; } - // otherwise, we are only calling this on completed refineries, so set it - else - { - refineryWorkerCount[unit] = 0; - } } - // when all else fails, return 0 + // Oops, it's something else. + // This may be an error, but we'll ignore that and just return 0. return 0; } diff --git a/Steamhammer/Source/WorkerData.h b/Steamhammer/Source/WorkerData.h index 290f9e5..f7b29a4 100644 --- a/Steamhammer/Source/WorkerData.h +++ b/Steamhammer/Source/WorkerData.h @@ -62,7 +62,7 @@ class WorkerData bool depotIsFull(BWAPI::Unit depot); int getMineralsNearDepot(BWAPI::Unit depot); - int getNumAssignedWorkers(BWAPI::Unit unit); + int getNumAssignedWorkers(BWAPI::Unit unit) const; WorkerJob getWorkerJob(BWAPI::Unit unit); BWAPI::Unit getWorkerResource(BWAPI::Unit unit); diff --git a/Steamhammer/Source/WorkerManager.cpp b/Steamhammer/Source/WorkerManager.cpp index cdd2c8e..027493b 100644 --- a/Steamhammer/Source/WorkerManager.cpp +++ b/Steamhammer/Source/WorkerManager.cpp @@ -596,12 +596,14 @@ void WorkerManager::finishedWithWorker(BWAPI::Unit unit) } // Find a worker to be reassigned to gas duty. +// Transferring across the map is bad. Try not to do that. BWAPI::Unit WorkerManager::getGasWorker(BWAPI::Unit refinery) { UAB_ASSERT(refinery, "Refinery was null"); BWAPI::Unit closestWorker = nullptr; int closestDistance = 0; + bool workerInBase = false; for (const auto unit : workerData.getWorkers()) { @@ -609,14 +611,19 @@ BWAPI::Unit WorkerManager::getGasWorker(BWAPI::Unit refinery) if (isFree(unit)) { - // Don't waste minerals. It's OK (and unlikely) to already be carrying gas. + int distance = unit->getDistance(refinery); + if (distance <= thisBaseRange) + { + workerInBase = true; + } + + // Don't waste minerals. It's OK (though unlikely) to already be carrying gas. if (unit->isCarryingMinerals() || // doesn't have minerals and unit->getOrder() == BWAPI::Orders::MiningMinerals) // isn't about to get them { continue; } - int distance = unit->getDistance(refinery); if (!closestWorker || distance < closestDistance) { closestWorker = unit; @@ -625,6 +632,20 @@ BWAPI::Unit WorkerManager::getGasWorker(BWAPI::Unit refinery) } } + if (closestWorker && closestDistance <= thisBaseRange) + { + // This owrker is in the base and is free. + return closestWorker; + } + + if (workerInBase) + { + // There is a free worker in the base, but it has minerals + // We'll wait until it returns the minerals. + return nullptr; + } + + // There are no free workers in the base. We may transfer one from elsewhere. return closestWorker; } @@ -689,7 +710,6 @@ BWAPI::Unit WorkerManager::getBuilder(const Building & b) b.finalPosition.isValid() ? b.finalPosition : b.desiredPosition ); UAB_ASSERT(pos.isValid(), "bad position"); - const int thisBaseRange = 10 * 32; BWAPI::Unit builder = getUnencumberedWorker(pos, thisBaseRange); if (builder) @@ -959,6 +979,12 @@ int WorkerManager::getMaxWorkers() const ); } +// The number of workers assigned to this resource depot to mine minerals, or to this refinery to mine gas. +int WorkerManager::getNumWorkers(BWAPI::Unit jobUnit) const +{ + return workerData.getNumAssignedWorkers(jobUnit); +} + // Mine out any blocking minerals that the worker runs headlong into. bool WorkerManager::maybeMineMineralBlocks(BWAPI::Unit worker) { @@ -966,7 +992,7 @@ bool WorkerManager::maybeMineMineralBlocks(BWAPI::Unit worker) if (worker->isGatheringMinerals() && worker->getTarget() && - worker->getTarget()->getInitialResources() <= 16) + worker->getTarget()->getInitialResources() <= 64) { // Still busy mining the block. return true; @@ -974,7 +1000,7 @@ bool WorkerManager::maybeMineMineralBlocks(BWAPI::Unit worker) for (const auto patch : worker->getUnitsInRadius(64, BWAPI::Filter::IsMineralField)) { - if (patch->getInitialResources() <= 16) // any patch we can mine out quickly + if (patch->getInitialResources() <= 64) // any patch we can mine out quickly { // Go start mining. worker->gather(patch); diff --git a/Steamhammer/Source/WorkerManager.h b/Steamhammer/Source/WorkerManager.h index e4f99a6..1c7a88f 100644 --- a/Steamhammer/Source/WorkerManager.h +++ b/Steamhammer/Source/WorkerManager.h @@ -26,6 +26,11 @@ class WorkerManager void handleRepairWorkers(); void handleMineralWorkers(); + // A worker within this distance of a target location is considered to be + // "in the same base" as the target. Used by worker selection routines to avoid + // transferring workers between bases. + static const int thisBaseRange = 10 * 32; + BWAPI::Unit findEnemyTargetForWorker(BWAPI::Unit worker) const; BWAPI::Unit findEscapeMinerals(BWAPI::Unit worker) const; bool defendSelf(BWAPI::Unit worker, BWAPI::Unit resource); @@ -59,6 +64,8 @@ class WorkerManager int getNumIdleWorkers() const; int getMaxWorkers() const; + int getNumWorkers(BWAPI::Unit jobUnit) const; + void setScoutWorker(BWAPI::Unit worker); // NOTE _collectGas == false allows that a little more gas may still be collected. diff --git a/Steamhammer/VisualStudio/UAlbertaBot.vcxproj b/Steamhammer/VisualStudio/UAlbertaBot.vcxproj index 2626efc..de36121 100644 --- a/Steamhammer/VisualStudio/UAlbertaBot.vcxproj +++ b/Steamhammer/VisualStudio/UAlbertaBot.vcxproj @@ -118,6 +118,7 @@ + @@ -157,6 +158,7 @@ + @@ -188,6 +190,7 @@ + @@ -228,6 +231,7 @@ + diff --git a/Steamhammer/VisualStudio/UAlbertaBot.vcxproj.filters b/Steamhammer/VisualStudio/UAlbertaBot.vcxproj.filters index 7a61ad3..fb6161b 100644 --- a/Steamhammer/VisualStudio/UAlbertaBot.vcxproj.filters +++ b/Steamhammer/VisualStudio/UAlbertaBot.vcxproj.filters @@ -166,6 +166,8 @@ + + @@ -300,5 +302,7 @@ + + \ No newline at end of file