428 lines
16 KiB
C++
428 lines
16 KiB
C++
//////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 <map>
|
|
|
|
|
|
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<ChokePoint> & 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<ChokePoint> * pChokePoints)
|
|
{
|
|
bwem_assert(!m_ChokePointsByArea[pArea] && pChokePoints);
|
|
|
|
m_ChokePointsByArea[pArea] = pChokePoints;
|
|
|
|
for (const auto & cp : *pChokePoints)
|
|
m_ChokePoints.push_back(&cp);
|
|
}
|
|
|
|
|
|
|
|
vector<int> Area::ComputeDistances(const ChokePoint * pStartCP, const vector<const ChokePoint *> & 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<TilePosition> 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<int> Area::ComputeDistances(TilePosition start, const vector<TilePosition> & Targets) const
|
|
{
|
|
const Map * pMap = GetMap();
|
|
vector<int> Distances(Targets.size());
|
|
|
|
Tile::UnmarkAll();
|
|
|
|
multimap<int, TilePosition> 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<int, TilePosition> & e) { return e.second == next; });
|
|
bwem_assert(iNext != range.second);
|
|
|
|
ToVisit.erase(iNext);
|
|
nextTile.SetInternalData(newNextDist);
|
|
// nextTile.SetPtr(const_cast<Tile *>(¤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<Tile *>(¤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<Mineral *> & 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<Ressource *> 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<int>::max(), numeric_limits<int>::max()};
|
|
TilePosition bottomRightRessources = {numeric_limits<int>::min(), numeric_limits<int>::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<Mineral *> 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<Ressource *> 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
|
|
|
|
|
|
|