////////////////////////////////////////////////////////////////////////// // // This file is part of the BWEM Library. // BWEM is free software, licensed under the MIT/X11 License. // A copy of the license is provided with the library in the LICENSE file. // Copyright (c) 2015, 2017, Igor Dimitrijevic // ////////////////////////////////////////////////////////////////////////// #include "area.h" #include "mapImpl.h" #include "graph.h" #include "neutral.h" #include "winutils.h" #include using namespace BWAPI; using namespace BWAPI::UnitTypes::Enum; namespace { auto & bw = Broodwar; } using namespace std; namespace BWEM { using namespace detail; using namespace BWAPI_ext; ////////////////////////////////////////////////////////////////////////////////////////////// // // // class Area // // ////////////////////////////////////////////////////////////////////////////////////////////// Area::Area(Graph * pGraph, id areaId, WalkPosition top, int miniTiles) : m_pGraph(pGraph), m_id(areaId), m_top(top), m_miniTiles(miniTiles) { bwem_assert(areaId > 0); auto & topMiniTile = GetMap()->GetMiniTile(top); bwem_assert(topMiniTile.AreaId() == areaId); m_maxAltitude = topMiniTile.Altitude(); } Area::Area(const Area & Other) : m_pGraph(Other.m_pGraph) { bwem_assert(false); } Map * Area::GetMap() const { return m_pGraph->GetMap(); } TilePosition Area::BoundingBoxSize() const { return m_bottomRight - m_topLeft + 1; } const std::vector & Area::ChokePoints(const Area * pArea) const { auto it = m_ChokePointsByArea.find(pArea); bwem_assert(it != m_ChokePointsByArea.end()); return *it->second; } void Area::AddGeyser(Geyser * pGeyser) { bwem_assert(pGeyser && !contains(m_Geysers, pGeyser)); m_Geysers.push_back(pGeyser); } void Area::AddMineral(Mineral * pMineral) { bwem_assert(pMineral && !contains(m_Minerals, pMineral)); m_Minerals.push_back(pMineral); } void Area::OnMineralDestroyed(const Mineral * pMineral) { bwem_assert(pMineral); auto iMineral = find(m_Minerals.begin(), m_Minerals.end(), pMineral); if (iMineral != m_Minerals.end()) fast_erase(m_Minerals, distance(m_Minerals.begin(), iMineral)); // let's examine the bases even if pMineral was not found in this Area, // which could arise if Minerals were allowed to be assigned to neighbouring Areas. for (Base & base : Bases()) base.OnMineralDestroyed(pMineral); } void Area::AddChokePoints(Area * pArea, vector * pChokePoints) { bwem_assert(!m_ChokePointsByArea[pArea] && pChokePoints); m_ChokePointsByArea[pArea] = pChokePoints; for (const auto & cp : *pChokePoints) m_ChokePoints.push_back(&cp); } vector Area::ComputeDistances(const ChokePoint * pStartCP, const vector & TargetCPs) const { bwem_assert(!contains(TargetCPs, pStartCP)); TilePosition start = GetMap()->BreadthFirstSearch(TilePosition(pStartCP->PosInArea(ChokePoint::middle, this)), [this](const Tile & tile, TilePosition) { return tile.AreaId() == Id(); }, // findCond [](const Tile &, TilePosition) { return true; }); // visitCond vector Targets; for (const ChokePoint * cp : TargetCPs) Targets.push_back(GetMap()->BreadthFirstSearch(TilePosition(cp->PosInArea(ChokePoint::middle, this)), [this](const Tile & tile, TilePosition) { return tile.AreaId() == Id(); }, // findCond [](const Tile &, TilePosition) { return true; })); // visitCond return ComputeDistances(start, Targets); } // Returns Distances such that Distances[i] == ground_distance(start, Targets[i]) in pixels // Note: same algorithm than Graph::ComputeDistances (derived from Dijkstra) vector Area::ComputeDistances(TilePosition start, const vector & Targets) const { const Map * pMap = GetMap(); vector Distances(Targets.size()); Tile::UnmarkAll(); multimap ToVisit; // a priority queue holding the tiles to visit ordered by their distance to start. ToVisit.emplace(0, start); int remainingTargets = Targets.size(); while (!ToVisit.empty()) { int currentDist = ToVisit.begin()->first; TilePosition current = ToVisit.begin()->second; const Tile & currentTile = pMap->GetTile(current, check_t::no_check); bwem_assert(currentTile.InternalData() == currentDist); ToVisit.erase(ToVisit.begin()); currentTile.SetInternalData(0); // resets Tile::m_internalData for future usage currentTile.SetMarked(); for (int i = 0 ; i < (int)Targets.size() ; ++i) if (current == Targets[i]) { Distances[i] = int(0.5 + currentDist * 32 / 10000.0); --remainingTargets; } if (!remainingTargets) break; for (TilePosition delta : { TilePosition(-1, -1), TilePosition(0, -1), TilePosition(+1, -1), TilePosition(-1, 0), TilePosition(+1, 0), TilePosition(-1, +1), TilePosition(0, +1), TilePosition(+1, +1)}) { const bool diagonalMove = (delta.x != 0) && (delta.y != 0); const int newNextDist = currentDist + (diagonalMove ? 14142 : 10000); TilePosition next = current + delta; if (pMap->Valid(next)) { const Tile & nextTile = pMap->GetTile(next, check_t::no_check); if (!nextTile.Marked()) { if (nextTile.InternalData()) // next already in ToVisit { if (newNextDist < nextTile.InternalData()) // nextNewDist < nextOldDist { // To update next's distance, we need to remove-insert it from ToVisit: auto range = ToVisit.equal_range(nextTile.InternalData()); auto iNext = find_if(range.first, range.second, [next] (const pair & e) { return e.second == next; }); bwem_assert(iNext != range.second); ToVisit.erase(iNext); nextTile.SetInternalData(newNextDist); // nextTile.SetPtr(const_cast(¤tTile)); // note: we won't use this backward trace ToVisit.emplace(newNextDist, next); } } else if ((nextTile.AreaId() == Id()) || (nextTile.AreaId() == -1)) { nextTile.SetInternalData(newNextDist); // nextTile.SetPtr(const_cast(¤tTile)); // note: we won't use this backward trace ToVisit.emplace(newNextDist, next); } } } } } bwem_assert(!remainingTargets); // Reset Tile::m_internalData for future usage for (auto e : ToVisit) pMap->GetTile(e.second, check_t::no_check).SetInternalData(0); return Distances; } void Area::UpdateAccessibleNeighbours() { m_AccessibleNeighbours.clear(); for (auto it : ChokePointsByArea()) if (any_of(it.second->begin(), it.second->end(), [](const ChokePoint & cp){ return !cp.Blocked(); })) m_AccessibleNeighbours.push_back(it.first); } // Called for each tile t of this Area void Area::AddTileInformation(const BWAPI::TilePosition t, const Tile & tile) { ++m_tiles; if (tile.Buildable()) ++m_buildableTiles; if (tile.GroundHeight() == 1) ++m_highGroundTiles; if (tile.GroundHeight() == 2) ++m_veryHighGroundTiles; if (t.x < m_topLeft.x) m_topLeft.x = t.x; if (t.y < m_topLeft.y) m_topLeft.y = t.y; if (t.x > m_bottomRight.x) m_bottomRight.x = t.x; if (t.y > m_bottomRight.y) m_bottomRight.y = t.y; } // Called after AddTileInformation(t) has been called for each tile t of this Area void Area::PostCollectInformation() { } // Calculates the score >= 0 corresponding to the placement of a Base Command Center at 'location'. // The more there are ressources nearby, the higher the score is. // The function assumes the distance to the nearby ressources has already been computed (in InternalData()) for each tile around. // The job is therefore made easier : just need to sum the InternalData() values. // Returns -1 if the location is impossible. int Area::ComputeBaseLocationScore(TilePosition location) const { const Map * pMap = GetMap(); const TilePosition dimCC = UnitType(Terran_Command_Center).tileSize(); int sumScore = 0; for (int dy = 0 ; dy < dimCC.y ; ++dy) for (int dx = 0 ; dx < dimCC.x ; ++dx) { const Tile & tile = pMap->GetTile(location + TilePosition(dx, dy), check_t::no_check); if (!tile.Buildable()) return -1; if (tile.InternalData() == -1) return -1; // The special value InternalData() == -1 means there is some ressource at maximum 3 tiles, which Starcraft rules forbid. // Unfortunately, this is guaranteed only for the ressources in this Area, which is the very reason of ValidateBaseLocation if (tile.AreaId() != Id()) return -1; if (tile.GetNeutral() && tile.GetNeutral()->IsStaticBuilding()) return -1; sumScore += tile.InternalData(); } return sumScore; } // Checks if 'location' is a valid location for the placement of a Base Command Center. // If the location is valid except for the presence of Mineral patches of less than 9 (see Andromeda.scx), // the function returns true, and these Minerals are reported in BlockingMinerals // The function is intended to be called after ComputeBaseLocationScore, as it is more expensive. // See also the comments inside ComputeBaseLocationScore. bool Area::ValidateBaseLocation(TilePosition location, vector & BlockingMinerals) const { const Map * pMap = GetMap(); const TilePosition dimCC = UnitType(Terran_Command_Center).tileSize(); BlockingMinerals.clear(); for (int dy = -3 ; dy < dimCC.y + 3 ; ++dy) for (int dx = -3 ; dx < dimCC.x + 3 ; ++dx) { TilePosition t = location + TilePosition(dx, dy); if (pMap->Valid(t)) { const Tile & tile = pMap->GetTile(t, check_t::no_check); if (Neutral * n = tile.GetNeutral()) { if (n->IsGeyser()) return false; if (Mineral * m = n->IsMineral()) if (m->InitialAmount() <= 8) BlockingMinerals.push_back(m); else return false; } } } // checks the distance to the Bases already created: for (const Base & base : Bases()) if (roundedDist(base.Location(), location) < min_tiles_between_Bases) return false; return true; } // Fills in m_Bases with good locations in this Area. // The algorithm repeatedly searches the best possible location L (near ressources) // When it finds one, the nearby ressources are assigned to L, which makes the remaining ressources decrease. // This causes the algorithm to always terminate due to the lack of remaining ressources. // To efficiently compute the distances to the ressources, with use Potiential Fields in the InternalData() value of the Tiles. void Area::CreateBases() { const TilePosition dimCC = UnitType(Terran_Command_Center).tileSize(); const Map * pMap = GetMap(); // Initialize the RemainingRessources with all the Minerals and Geysers in this Area satisfying some conditions: vector RemainingRessources; for (Mineral * m : Minerals()) if ((m->InitialAmount() >= 40) && !m->Blocking()) RemainingRessources.push_back(m); for (Geyser * g : Geysers()) if ((g->InitialAmount() >= 300) && !g->Blocking()) RemainingRessources.push_back(g); m_Bases.reserve(min(100, (int)RemainingRessources.size())); while (!RemainingRessources.empty()) { // 1) Calculate the SearchBoundingBox (needless to search too far from the RemainingRessources): TilePosition topLeftRessources = {numeric_limits::max(), numeric_limits::max()}; TilePosition bottomRightRessources = {numeric_limits::min(), numeric_limits::min()}; for (const Ressource * r : RemainingRessources) { makeBoundingBoxIncludePoint(topLeftRessources, bottomRightRessources, r->TopLeft()); makeBoundingBoxIncludePoint(topLeftRessources, bottomRightRessources, r->BottomRight()); } TilePosition topLeftSearchBoundingBox = topLeftRessources - dimCC - max_tiles_between_CommandCenter_and_ressources; TilePosition bottomRightSearchBoundingBox = bottomRightRessources + 1 + max_tiles_between_CommandCenter_and_ressources; makePointFitToBoundingBox(topLeftSearchBoundingBox, TopLeft(), BottomRight() - dimCC + 1); makePointFitToBoundingBox(bottomRightSearchBoundingBox, TopLeft(), BottomRight() - dimCC + 1); // 2) Mark the Tiles with their distances from each remaining Ressource (Potential Fields >= 0) for (const Ressource * r : RemainingRessources) for (int dy = -dimCC.y-max_tiles_between_CommandCenter_and_ressources ; dy < r->Size().y + dimCC.y+max_tiles_between_CommandCenter_and_ressources ; ++dy) for (int dx = -dimCC.x-max_tiles_between_CommandCenter_and_ressources ; dx < r->Size().x + dimCC.x+max_tiles_between_CommandCenter_and_ressources ; ++dx) { TilePosition t = r->TopLeft() + TilePosition(dx, dy); if (pMap->Valid(t)) { const Tile & tile = pMap->GetTile(t, check_t::no_check); int dist = (distToRectangle(center(t), r->TopLeft(), r->Size())+16)/32; int score = max(max_tiles_between_CommandCenter_and_ressources + 3 - dist, 0); if (r->IsGeyser()) score *= 3; // somewhat compensates for Geyser alone vs the several Minerals if (tile.AreaId() == Id()) tile.SetInternalData(tile.InternalData() + score); // note the additive effect (assume tile.InternalData() is 0 at the begining) } } // 3) Invalidate the 7 x 7 Tiles around each remaining Ressource (Starcraft rule) for (const Ressource * r : RemainingRessources) for (int dy = -3 ; dy < r->Size().y + 3 ; ++dy) for (int dx = -3 ; dx < r->Size().x + 3 ; ++dx) { TilePosition t = r->TopLeft() + TilePosition(dx, dy); if (pMap->Valid(t)) pMap->GetTile(t, check_t::no_check).SetInternalData(-1); } // 4) Search the best location inside the SearchBoundingBox: TilePosition bestLocation; int bestScore = 0; vector BlockingMinerals; for (int y = topLeftSearchBoundingBox.y ; y <= bottomRightSearchBoundingBox.y ; ++y) for (int x = topLeftSearchBoundingBox.x ; x <= bottomRightSearchBoundingBox.x ; ++x) { int score = ComputeBaseLocationScore(TilePosition(x, y)); if (score > bestScore) if (ValidateBaseLocation(TilePosition(x, y), BlockingMinerals)) { bestScore = score; bestLocation = TilePosition(x, y); } } // 5) Clear Tile::m_internalData (required due to our use of Potential Fields: see comments in 2)) for (const Ressource * r : RemainingRessources) for (int dy = -dimCC.y-max_tiles_between_CommandCenter_and_ressources ; dy < r->Size().y + dimCC.y+max_tiles_between_CommandCenter_and_ressources ; ++dy) for (int dx = -dimCC.x-max_tiles_between_CommandCenter_and_ressources ; dx < r->Size().x + dimCC.x+max_tiles_between_CommandCenter_and_ressources ; ++dx) { TilePosition t = r->TopLeft() + TilePosition(dx, dy); if (pMap->Valid(t)) pMap->GetTile(t, check_t::no_check).SetInternalData(0); } if (!bestScore) break; // 6) Create a new Base at bestLocation, assign to it the relevant ressources and remove them from RemainingRessources: vector AssignedRessources; for (Ressource * r : RemainingRessources) if (distToRectangle(r->Pos(), bestLocation, dimCC) + 2 <= max_tiles_between_CommandCenter_and_ressources*32) AssignedRessources.push_back(r); really_remove_if(RemainingRessources, [&AssignedRessources](const Ressource * r){ return contains(AssignedRessources, r); }); if (AssignedRessources.empty()) { //bwem_assert(false); break; } m_Bases.emplace_back(this, bestLocation, AssignedRessources, BlockingMinerals); } } } // namespace BWEM