ProtoBot
Loading...
Searching...
No Matches
ScoutingZealot Class Reference

ScoutingZealot Controls scouting behavior for Zealot and Dragoon units. Combines positioning, threat detection, and movement control to provide safe and consistent map scouting. More...

#include <ScoutingZealot.h>

Public Member Functions

 ScoutingZealot (ProtoBotCommander *commander, ScoutingManager *manager)
void onStart ()
 Initializes the zealot scouting state.
void onFrame ()
 Main update loop executed every frame.
void onUnitDestroy (BWAPI::Unit unit)
 Handles cleanup when the scouting unit is destroyed.
void setEnemyMain (const BWAPI::TilePosition &tp)
 Sets the enemy main base location and updates scouting behavior.
void assign (BWAPI::Unit unit)
 Assigns a unit as a scouting zealot.
bool hasScout () const
void setProxyPatroller (bool v)
void drawDebug () const
 Draws debug information for the scouting unit.

Private Types

enum class  State {
  Idle , WaitEnemyMain , ProxyPatrol , MoveToNatural ,
  HoldEdge , Reposition , Done
}

Private Member Functions

BWAPI::Position homeRetreatPoint () const
void computeEnemyNatural ()
 Determines the enemy natural expansion location.
BWAPI::Position pickEdgeOfVisionSpot ()
 Selects a position near the edge of enemy vision at the natural.
BWAPI::Position pickNaturalChokeSpot ()
BWAPI::Position findReachableNearby (const BWAPI::Position &desired) const
 Finds a reachable nearby position if the desired location is invalid.
BWAPI::Position pushAwayFromResources (const BWAPI::Position &p, int clearPx) const
bool threatenedNow () const
 Checks whether the unit is currently threatened.
void issueMove (const BWAPI::Position &p, bool force=false, int reissueDist=32)
 Issues movement commands with anti-stuck handling.
void rebuildProxyPoints ()
 Builds patrol points used for proxy scouting.
bool isNear (const BWAPI::Position &a, const BWAPI::Position &b, int distPx) const
BWAPI::Unit findPrimaryThreat (int radiusPx) const
void retreatHomeMicro (BWAPI::Unit threat)
 Executes retreat and kiting behavior toward home base.
BWAPI::Unit findInWeaponRangeTarget () const
int attackCommitFrames () const
bool tryFireAndCommit (BWAPI::Unit target)
 Attempts to attack a target and commit to the attack briefly.
bool isGoodKiteTarget (BWAPI::Unit u, int radiusPx) const

Static Private Member Functions

static double groundPathLengthPx (const BWAPI::Position &from, const BWAPI::Position &to)
static BWAPI::Position clampToMapPx (const BWAPI::Position &p, int marginPx=32)

Private Attributes

State returnStateAfterReposition = State::HoldEdge
ProtoBotCommandercommanderRef = nullptr
ScoutingManagermanager = nullptr
BWAPI::Unit zealot = nullptr
std::optional< BWAPI::TilePosition > enemyMainTile
BWAPI::Position enemyMainPos = BWAPI::Positions::Invalid
BWAPI::TilePosition enemyNaturalTile = BWAPI::TilePositions::Invalid
BWAPI::Position enemyNaturalPos = BWAPI::Positions::Invalid
State state = State::Idle
int lastMoveIssueFrame = 0
int lastThreatFrame = -100000
int lastAttackCmdFrame = -100000
int lastTargetSelectFrame = -100000
int proxyRebuildReadyFrame = 0
std::vector< BWAPI::Position > proxyPoints
int proxyNextIdx = 0
int proxyNextRebuildFrame = 0
int proxyNextMoveFrame = 0
BWAPI::Position proxyCurTarget = BWAPI::Positions::Invalid
bool isProxyPatroller = false
std::vector< BWAPI::Position > cachedProxyPoints
bool proxyPointsBuiltOnce = false
BWAPI::Position cachedPerch = BWAPI::Positions::Invalid
int cachedPerchFrame = -100000
BWAPI::Position lastIssuedGoal = BWAPI::Positions::Invalid
BWAPI::Position lastPos = BWAPI::Positions::Invalid
int stuckFrames = 0
BWAPI::Unit lastAttackTarget = nullptr

Static Private Attributes

static constexpr int kMoveCooldownFrames = 8
static constexpr int kEdgeMarginPx = 24
static constexpr int kThreatRadiusPx = 256
static constexpr int kRepositionStepPx = 160
static constexpr int kCalmFramesToReturn = 72
static constexpr int kDangerClosePx = 96
static constexpr int kTargetStickFrames = 18
static constexpr int kProxyRebuildEveryFrames = 24 * 10
static constexpr int kProxyArriveDist = 96
static constexpr int kProxyMinBetweenMoves = 12
static constexpr double kMaxGroundDist = 180 * 32
static constexpr int kPerchRecalcFrames = 24
static constexpr int kGoalChangeResetDist = 64

Detailed Description

ScoutingZealot Controls scouting behavior for Zealot and Dragoon units. Combines positioning, threat detection, and movement control to provide safe and consistent map scouting.

/// Uses a state-driven system to:

  • Move toward and hold the enemy natural edge
  • Avoid entering the enemy main base
  • React to threats using retreat and kiting logic
  • Patrol proxy locations to detect early enemy structures

Definition at line 23 of file ScoutingZealot.h.

Member Enumeration Documentation

◆ State

enum class ScoutingZealot::State
strongprivate

Definition at line 47 of file ScoutingZealot.h.

48 {
49 Idle,
50 WaitEnemyMain,
51 ProxyPatrol,
52 MoveToNatural,
53 HoldEdge,
54 Reposition,
55 Done
56 };

Constructor & Destructor Documentation

◆ ScoutingZealot()

ScoutingZealot::ScoutingZealot ( ProtoBotCommander * commander,
ScoutingManager * manager )
inlineexplicit

Definition at line 26 of file ScoutingZealot.h.

27 : commanderRef(commander), manager(manager)
28 {
29 }

Member Function Documentation

◆ assign()

void ScoutingZealot::assign ( BWAPI::Unit unit)

Assigns a unit as a scouting zealot.

Initializes behavior based on role:

  • Proxy patrol zealots begin patrol setup
  • Standard scouts wait for or move toward enemy natural
Parameters
unitUnit to assign as scout

Definition at line 155 of file ScoutingZealot.cpp.

155 {
156
157 if (!unit || !unit->exists()) return;
158
159 zealot = unit;
160 //BWAPI::Broodwar->printf("[ZealotScout] assign id=%d type=%s",
161 //zealot->getID(), zealot->getType().c_str());
162
163 if (isProxyPatroller)
164 {
165 proxyPoints.clear();
166 proxyNextIdx = 0;
167 proxyCurTarget = BWAPI::Positions::Invalid;
168 proxyRebuildReadyFrame = BWAPI::Broodwar->getFrameCount() + 1;
169 state = State::ProxyPatrol;
170 return;
171 }
172 state = enemyMainPos.isValid() ? State::MoveToNatural : State::WaitEnemyMain;
173}

◆ attackCommitFrames()

int ScoutingZealot::attackCommitFrames ( ) const
private

Definition at line 1519 of file ScoutingZealot.cpp.

1520{
1521 if (!zealot || !zealot->exists())
1522 {
1523 return 0;
1524 }
1525
1526 const int range = zealot->getType().groundWeapon().maxRange();
1527
1528 // Dragoons need a bigger window so we don't cancel their windup.
1529 if (range > 32)
1530 {
1531 return 8;
1532 }
1533
1534 // Zealot (melee) is fine with a tiny commit.
1535 return 2;
1536}

◆ clampToMapPx()

BWAPI::Position ScoutingZealot::clampToMapPx ( const BWAPI::Position & p,
int marginPx = 32 )
staticprivate

Definition at line 1021 of file ScoutingZealot.cpp.

1021 {
1022 const int x = (std::max)(marginPx, (std::min)(p.x, mapWpx() - 1 - marginPx));
1023 const int y = (std::max)(marginPx, (std::min)(p.y, mapHpx() - 1 - marginPx));
1024 return BWAPI::Position(x, y);
1025}

◆ computeEnemyNatural()

void ScoutingZealot::computeEnemyNatural ( )
private

Determines the enemy natural expansion location.

Selects the closest reachable non-starting base outside of the enemy main area.

Definition at line 448 of file ScoutingZealot.cpp.

448 {
449 enemyNaturalTile = BWAPI::TilePositions::Invalid;
450 enemyNaturalPos = BWAPI::Positions::Invalid;
451 if (!enemyMainTile.has_value()) return;
452
453 const BWAPI::TilePosition enemyMain = *enemyMainTile;
454 const BWAPI::Position enemyMainCenter = BWAPI::Position(enemyMain) + BWAPI::Position(16, 16);
455
456 const BWEM::Area* mainArea = BWEM::Map::Instance().GetArea(enemyMain);
457 double bestDist = 1e30;
458
459 // Look at all non-starting bases NOT in the enemy main's area; pick shortest ground path
460 for (const BWEM::Area& area : BWEM::Map::Instance().Areas()) {
461 for (const BWEM::Base& base : area.Bases()) {
462 if (base.Starting()) continue;
463
464 const BWEM::Area* bArea = BWEM::Map::Instance().GetArea(base.Location());
465 if (bArea == mainArea) continue; // don't consider bases inside enemy main area
466
467 BWAPI::Position basePos = BWAPI::Position(base.Location());
468 const auto& path = BWEM::Map::Instance().GetPath(enemyMainCenter, basePos, nullptr);
469 if (path.empty()) continue; // unreachable
470
471 double gdist = ScoutingZealot::groundPathLengthPx(enemyMainCenter, basePos);
472 if (gdist < bestDist) {
473 bestDist = gdist;
474 enemyNaturalTile = base.Location();
475 enemyNaturalPos = basePos;
476 }
477 }
478 }
479
480 if (enemyNaturalPos.isValid()) {
481 //Broodwar->drawCircleMap(enemyNaturalPos, 10, Colors::Cyan, true);
482 //->drawTextMap(enemyNaturalPos, "nat");
483 }
484
485}

◆ drawDebug()

void ScoutingZealot::drawDebug ( ) const

Draws debug information for the scouting unit.

Displays:

  • Current state
  • Target positions
  • Threats and attack targets
  • Proxy patrol routes

Definition at line 1616 of file ScoutingZealot.cpp.

1617{
1618 if (!zealot || !zealot->exists())
1619 {
1620 return;
1621 }
1622
1623 BWAPI::Position p = zealot->getPosition();
1624
1625 const char* stateName = "Unknown";
1626
1627 switch (state)
1628 {
1629 case State::Idle:
1630 stateName = "Idle";
1631 break;
1632 case State::WaitEnemyMain:
1633 stateName = "WaitEnemyMain";
1634 break;
1635 case State::ProxyPatrol:
1636 stateName = "ProxyPatrol";
1637 break;
1638 case State::MoveToNatural:
1639 stateName = "MoveToNatural";
1640 break;
1641 case State::HoldEdge:
1642 stateName = "HoldEdge";
1643 break;
1644 case State::Reposition:
1645 stateName = "Reposition";
1646 break;
1647 case State::Done:
1648 stateName = "Done";
1649 break;
1650 }
1651
1652 BWAPI::Broodwar->drawCircleMap(p, 20, BWAPI::Colors::Green, false);
1653
1654 const char* label = zealot->getType() == BWAPI::UnitTypes::Protoss_Dragoon
1655 ? "Dragoon Scout"
1656 : "Zealot Scout";
1657
1658 BWAPI::Broodwar->drawTextMap(p.x - 40, p.y - 46, "\x07%s", label);
1659 BWAPI::Broodwar->drawTextMap(p.x - 40, p.y - 34, "\x11State: %s", stateName);
1660
1661 if (isProxyPatroller)
1662 {
1663 BWAPI::Broodwar->drawTextMap(p.x - 40, p.y - 22, "\x10Role: Proxy Patrol");
1664 }
1665 else
1666 {
1667 BWAPI::Broodwar->drawTextMap(p.x - 40, p.y - 22, "\x10Role: Natural Edge");
1668 }
1669
1670 if (enemyMainPos.isValid())
1671 {
1672 BWAPI::Broodwar->drawCircleMap(enemyMainPos, 20, BWAPI::Colors::Red, false);
1673 BWAPI::Broodwar->drawTextMap(enemyMainPos.x + 8, enemyMainPos.y - 8, "\x08Enemy Main");
1674 }
1675
1676 if (enemyNaturalPos.isValid())
1677 {
1678 BWAPI::Broodwar->drawCircleMap(enemyNaturalPos, 18, BWAPI::Colors::Cyan, false);
1679 BWAPI::Broodwar->drawTextMap(enemyNaturalPos.x + 8, enemyNaturalPos.y - 8, "\x0f" "Enemy Natural");
1680 }
1681
1682 if (state == State::MoveToNatural || state == State::HoldEdge || state == State::Reposition)
1683 {
1684 BWAPI::Position perch = cachedPerch.isValid() ? cachedPerch : enemyNaturalPos;
1685
1686 if (perch.isValid())
1687 {
1688 BWAPI::Broodwar->drawLineMap(p, perch, BWAPI::Colors::Green);
1689 BWAPI::Broodwar->drawCircleMap(perch, 10, BWAPI::Colors::Green, false);
1690 BWAPI::Broodwar->drawTextMap(perch.x + 6, perch.y - 6, "\x07Perch");
1691 }
1692 }
1693
1694 if (state == State::ProxyPatrol)
1695 {
1696 for (int i = 0; i < (int)proxyPoints.size(); ++i)
1697 {
1698 BWAPI::Color c = (i == proxyNextIdx - 1) ? BWAPI::Colors::Orange : BWAPI::Colors::Yellow;
1699
1700 BWAPI::Broodwar->drawCircleMap(proxyPoints[i], 7, c, false);
1701 BWAPI::Broodwar->drawTextMap(proxyPoints[i].x + 5, proxyPoints[i].y - 5, "#%d", i);
1702
1703 if (i + 1 < (int)proxyPoints.size())
1704 {
1705 BWAPI::Broodwar->drawLineMap(proxyPoints[i], proxyPoints[i + 1], BWAPI::Colors::Orange);
1706 }
1707 }
1708
1709 if (proxyCurTarget.isValid())
1710 {
1711 BWAPI::Broodwar->drawLineMap(p, proxyCurTarget, BWAPI::Colors::Orange);
1712 BWAPI::Broodwar->drawCircleMap(proxyCurTarget, 10, BWAPI::Colors::Orange, false);
1713 BWAPI::Broodwar->drawTextMap(proxyCurTarget.x + 6, proxyCurTarget.y - 6, "\x10Proxy Target");
1714 }
1715 }
1716
1717 BWAPI::Unit threat = findPrimaryThreat(kThreatRadiusPx);
1718 if (threat && threat->exists())
1719 {
1720 BWAPI::Broodwar->drawLineMap(p, threat->getPosition(), BWAPI::Colors::Red);
1721 BWAPI::Broodwar->drawCircleMap(threat->getPosition(), 12, BWAPI::Colors::Red, false);
1722 BWAPI::Broodwar->drawTextMap(
1723 threat->getPosition().x + 6,
1724 threat->getPosition().y - 6,
1725 "\x08Threat"
1726 );
1727 }
1728
1729 if (lastAttackTarget && lastAttackTarget->exists())
1730 {
1731 BWAPI::Broodwar->drawLineMap(p, lastAttackTarget->getPosition(), BWAPI::Colors::Purple);
1732 BWAPI::Broodwar->drawCircleMap(lastAttackTarget->getPosition(), 10, BWAPI::Colors::Purple, false);
1733 BWAPI::Broodwar->drawTextMap(
1734 p.x - 40,
1735 p.y - 10,
1736 "\x05Attack: %s",
1737 lastAttackTarget->getType().getName().c_str()
1738 );
1739 }
1740
1741 if (cachedPerch.isValid())
1742 {
1743 BWAPI::Broodwar->drawCircleMap(cachedPerch, 8, BWAPI::Colors::Blue, false);
1744 }
1745
1746 if (lastIssuedGoal.isValid())
1747 {
1748 BWAPI::Broodwar->drawLineMap(p, lastIssuedGoal, BWAPI::Colors::White);
1749 }
1750}

◆ findInWeaponRangeTarget()

BWAPI::Unit ScoutingZealot::findInWeaponRangeTarget ( ) const
private

Definition at line 1476 of file ScoutingZealot.cpp.

1477{
1478 if (!zealot || !zealot->exists())
1479 {
1480 return nullptr;
1481 }
1482
1483 BWAPI::Unit best = nullptr;
1484 int bestD = INT_MAX;
1485
1486 for (auto& e : BWAPI::Broodwar->enemy()->getUnits())
1487 {
1488 if (!e || !e->exists())
1489 {
1490 continue;
1491 }
1492
1493 if (e->getType().isWorker())
1494 {
1495 continue;
1496 }
1497
1498 if (!zealot->canAttackUnit(e))
1499 {
1500 continue;
1501 }
1502
1503 if (!zealot->isInWeaponRange(e))
1504 {
1505 continue;
1506 }
1507
1508 const int d = zealot->getDistance(e);
1509 if (d < bestD)
1510 {
1511 bestD = d;
1512 best = e;
1513 }
1514 }
1515
1516 return best;
1517}

◆ findPrimaryThreat()

BWAPI::Unit ScoutingZealot::findPrimaryThreat ( int radiusPx) const
private

Definition at line 1365 of file ScoutingZealot.cpp.

1366{
1367 if (!zealot || !zealot->exists())
1368 {
1369 return nullptr;
1370 }
1371
1372 BWAPI::Unit best = nullptr;
1373 int bestD = INT_MAX;
1374
1375 for (auto& e : BWAPI::Broodwar->enemy()->getUnits())
1376 {
1377 if (!e || !e->exists())
1378 {
1379 continue;
1380 }
1381
1382 if (!canBeKitedByScoutUnit(zealot, e))
1383 {
1384 continue;
1385 }
1386
1387 const int d = zealot->getDistance(e);
1388 if (d > radiusPx)
1389 {
1390 continue;
1391 }
1392
1393 if (d < bestD)
1394 {
1395 bestD = d;
1396 best = e;
1397 }
1398 }
1399
1400 return best;
1401}

◆ findReachableNearby()

BWAPI::Position ScoutingZealot::findReachableNearby ( const BWAPI::Position & desired) const
private

Finds a reachable nearby position if the desired location is invalid.

Searches surrounding positions for a walkable and pathable alternative.

Parameters
desiredDesired target position
Returns
Reachable position near the desired location

Definition at line 742 of file ScoutingZealot.cpp.

743{
744 if (!zealot || !zealot->exists())
745 {
746 return desired;
747 }
748
749 const BWAPI::Position from = zealot->getPosition();
750 if (!from.isValid() || !desired.isValid())
751 {
752 return desired;
753 }
754
755 auto isOk = [&](const BWAPI::Position& p)
756 {
757 if (!p.isValid())
758 {
759 return false;
760 }
761
762 if (!isWalkablePx(p))
763 {
764 return false;
765 }
766
767 if (!hasGroundPath(from, p))
768 {
769 return false;
770 }
771
772 return true;
773 };
774
775 if (isOk(desired))
776 {
777 return desired;
778 }
779
780 // Search ring: try a few radii around the desired point
781 static const int radii[] = { 32, 64, 96, 128, 160, 192 };
782 static const int dirs[][2] =
783 {
784 { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 },
785 { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 }
786 };
787
788 for (int r : radii)
789 {
790 for (const auto& d : dirs)
791 {
792 BWAPI::Position p(desired.x + d[0] * r, desired.y + d[1] * r);
793 p = clampToMapPx(p, 8);
794
795 if (isOk(p))
796 {
797 return p;
798 }
799 }
800 }
801
802 // Last resort: head toward natural center if reachable
803 if (enemyNaturalPos.isValid() && isOk(enemyNaturalPos))
804 {
805 return enemyNaturalPos;
806 }
807
808 // Give up, return desired (caller may still handle stuck)
809 return desired;
810}

◆ groundPathLengthPx()

double ScoutingZealot::groundPathLengthPx ( const BWAPI::Position & from,
const BWAPI::Position & to )
staticprivate

Definition at line 861 of file ScoutingZealot.cpp.

862{
863 auto& m = BWEM::Map::Instance();
864 const auto& path = m.GetPath(from, to, nullptr);
865 if (path.empty()) {
866 return from.getDistance(to);
867 }
868 double sum = 0.0;
869 BWAPI::Position prev = from;
870 for (const BWEM::ChokePoint* cp : path) {
871 if (!cp) continue;
872 BWAPI::Position c(cp->Center());
873 sum += prev.getDistance(c);
874 prev = c;
875 }
876 sum += prev.getDistance(to);
877 return sum;
878}

◆ hasScout()

bool ScoutingZealot::hasScout ( ) const
inline

Definition at line 37 of file ScoutingZealot.h.

37{ return zealot && zealot->exists(); }

◆ homeRetreatPoint()

BWAPI::Position ScoutingZealot::homeRetreatPoint ( ) const
private

Definition at line 433 of file ScoutingZealot.cpp.

434{
435 BWAPI::Position home = g_myMainPos.isValid()
436 ? g_myMainPos
437 : BWAPI::Position(BWAPI::Broodwar->self()->getStartLocation());
438 return ScoutingZealot::clampToMapPx(home, 32);
439}

◆ isGoodKiteTarget()

bool ScoutingZealot::isGoodKiteTarget ( BWAPI::Unit u,
int radiusPx ) const
private

Definition at line 1586 of file ScoutingZealot.cpp.

1587{
1588 if (!u || !u->exists())
1589 {
1590 return false;
1591 }
1592
1593 if (u->getPlayer() != BWAPI::Broodwar->enemy())
1594 {
1595 return false;
1596 }
1597
1598 if (!canBeKitedByScoutUnit(zealot, u))
1599 {
1600 return false;
1601 }
1602
1603 return zealot->getDistance(u) <= radiusPx;
1604}

◆ isNear()

bool ScoutingZealot::isNear ( const BWAPI::Position & a,
const BWAPI::Position & b,
int distPx ) const
private

Definition at line 1028 of file ScoutingZealot.cpp.

1029{
1030 return a.getApproxDistance(b) <= distPx;
1031}

◆ issueMove()

void ScoutingZealot::issueMove ( const BWAPI::Position & p,
bool force = false,
int reissueDist = 32 )
private

Issues movement commands with anti-stuck handling.

Includes:

  • Movement throttling
  • Stuck detection and recovery
  • Goal tracking
Parameters
pTarget position
forceForce movement regardless of cooldown
reissueDistMinimum distance to reissue command

Definition at line 913 of file ScoutingZealot.cpp.

914{
915 if (!zealot || !zealot->exists() || !p.isValid())
916 {
917 return;
918 }
919
920 const int now = BWAPI::Broodwar->getFrameCount();
921
922 // If the goal changed meaningfully, reset stuck tracking.
923 if (lastIssuedGoal.isValid())
924 {
925 const int goalDelta = p.getApproxDistance(lastIssuedGoal);
926 if (goalDelta > kGoalChangeResetDist)
927 {
928 stuckFrames = 0;
929 }
930 }
931 lastIssuedGoal = p;
932
933 // Stuck detection: only when we intend to be moving toward the same goal.
934 if (lastPos.isValid())
935 {
936 const int moved = zealot->getPosition().getApproxDistance(lastPos);
937
938 if (moved < 4 && zealot->isMoving())
939 {
940 stuckFrames += 1;
941 }
942 else
943 {
944 stuckFrames = 0;
945 }
946 }
947 lastPos = zealot->getPosition();
948
949 if (stuckFrames > 24)
950 {
951 // Try a few nudges around current position, biased toward goal.
952 BWAPI::Position here = zealot->getPosition();
953 BWAPI::Position best = BWAPI::Positions::Invalid;
954
955 const int dx = p.x - here.x;
956 const int dy = p.y - here.y;
957
958 auto tryNudge = [&](int ox, int oy)
959 {
960 if (best.isValid())
961 {
962 return;
963 }
964
965 BWAPI::Position np(here.x + ox, here.y + oy);
966 np = clampToMapPx(np, 8);
967
968 if (!isWalkablePx(np))
969 {
970 return;
971 }
972
973 if (!hasGroundPath(here, np))
974 {
975 return;
976 }
977
978 best = np;
979 };
980
981 // Forward-ish, then side steps, then back-ish
982 const int step = 64;
983 const int sx = (dx >= 0) ? 1 : -1;
984 const int sy = (dy >= 0) ? 1 : -1;
985
986 tryNudge(step * sx, 0);
987 tryNudge(0, step * sy);
988 tryNudge(step * sx, step * sy);
989
990 tryNudge(0, step * -sy);
991 tryNudge(step * -sx, 0);
992 tryNudge(step * -sx, step * sy);
993 tryNudge(step * sx, step * -sy);
994
995 if (best.isValid())
996 {
997 zealot->move(best);
998 lastMoveIssueFrame = now;
999 stuckFrames = 0;
1000 return;
1001 }
1002
1003 // If no good nudge found, just reset and let normal move happen.
1004 stuckFrames = 0;
1005 }
1006
1007 if (!force && now - lastMoveIssueFrame < kMoveCooldownFrames)
1008 {
1009 return;
1010 }
1011
1012 if (force || zealot->getDistance(p) > reissueDist || !zealot->isMoving())
1013 {
1014 zealot->move(p);
1015 lastMoveIssueFrame = now;
1016 }
1017
1018 //Broodwar->drawLineMap(zealot->getPosition(), p, Colors::Orange);
1019}

◆ onFrame()

void ScoutingZealot::onFrame ( )

Main update loop executed every frame.

Controls state transitions and behavior including:

  • Moving to enemy natural
  • Holding edge positions
  • Avoiding enemy main base
  • Repositioning under threat
  • Proxy patrol routing

Definition at line 229 of file ScoutingZealot.cpp.

229 {
230 if (!zealot || !zealot->exists() || state == State::Done) return;
231 /*
232 BWAPI::Broodwar->drawTextMap(zealot->getPosition(),
233 "\x11Zealot[%d] %s", zealot->getID(),
234 (state == State::Idle ? "Idle" :
235 state == State::ProxyPatrol ? "ProxyPatrol" :
236 state == State::WaitEnemyMain ? "WaitEnemyMain" :
237 state == State::MoveToNatural ? "MoveToNatural" :
238 state == State::HoldEdge ? "HoldEdge" :
239 state == State::Reposition ? "Reposition" : "Done"));
240 */
241
242 // --- hard avoid enemy main every frame ---
243 if (enemyMainTile.has_value() && zealot && zealot->exists()) {
244 const auto* mainArea = BWEM::Map::Instance().GetArea(*enemyMainTile);
245 const auto* hereArea = BWEM::Map::Instance().GetArea(zealot->getTilePosition());
246 if (mainArea && hereArea == mainArea) {
247 //BWAPI::Broodwar->drawTextMap(zealot->getPosition(), "\x08[AVOID MAIN]");
248 issueMove(pickEdgeOfVisionSpot(), /*force*/true);
249 return;
250 }
251 }
252
253 switch (state) {
254 case State::Idle:
255 // wait until we know the main
256 if (isProxyPatroller)
257 {
258 proxyRebuildReadyFrame = BWAPI::Broodwar->getFrameCount() + 1;
259 state = State::ProxyPatrol;
260 }
261 else
262 {
263 state = enemyMainPos.isValid() ? State::MoveToNatural : State::WaitEnemyMain;
264 }
265 break;
266
267 case State::WaitEnemyMain:
268 // commander will call setEnemyMain() once your Probe finds it
269 if (commanderRef)
270 {
271 const auto& e = commanderRef->enemy();
272 if (e.main.has_value())
273 {
274 setEnemyMain(*e.main);
275 break;
276 }
277 }
278 break;
279
280 case State::MoveToNatural:
281 {
282 if (!enemyMainPos.isValid())
283 {
284 state = State::WaitEnemyMain;
285 break;
286 }
287
288 if (!enemyNaturalPos.isValid())
289 {
291 }
292
293 BWAPI::Unit threat = findPrimaryThreat(kThreatRadiusPx);
294
295 if (threat || zealot->isUnderAttack())
296 {
297 lastThreatFrame = BWAPI::Broodwar->getFrameCount();
298 returnStateAfterReposition = State::MoveToNatural;
299 state = State::Reposition;
300
301 retreatHomeMicro(threat);
302 break;
303 }
304
305 if (enemyNaturalPos.isValid())
306 {
307 const BWAPI::Position perch = pickNaturalChokeSpot();
308 issueMove(perch);
309
310 if (zealot->getDistance(perch) < 96)
311 {
312 state = State::HoldEdge;
313 }
314 }
315
316 break;
317 }
318
319 case State::HoldEdge:
320 {
321 BWAPI::Unit threat = findPrimaryThreat(kThreatRadiusPx);
322
323 if (threat || zealot->isUnderAttack())
324 {
325 lastThreatFrame = BWAPI::Broodwar->getFrameCount();
326 returnStateAfterReposition = State::HoldEdge;
327 state = State::Reposition;
328
329 retreatHomeMicro(threat);
330 break;
331 }
332
333
334
335 break;
336 }
337
338 case State::Reposition:
339 {
340 const int now = BWAPI::Broodwar->getFrameCount();
341 BWAPI::Unit threat = findPrimaryThreat(kThreatRadiusPx);
342
343 if (threat || zealot->isUnderAttack())
344 {
345 lastThreatFrame = now;
346 retreatHomeMicro(threat);
347 break;
348 }
349
350 if (now - lastThreatFrame >= kCalmFramesToReturn)
351 {
352 if (returnStateAfterReposition == State::MoveToNatural)
353 {
354 issueMove(pickNaturalChokeSpot(), true);
355 state = State::MoveToNatural;
356 }
357 else
358 {
359 issueMove(pickNaturalChokeSpot(), true);
360 state = State::HoldEdge;
361 }
362
363 break;
364 }
365
366 break;
367 }
368 case State::ProxyPatrol:
369 {
370 const int now = Broodwar->getFrameCount();
371
372 // If zealot is missing points, rebuild here.
373 if (proxyPoints.empty() && now >= proxyRebuildReadyFrame)
374 {
376 }
377
378 if (proxyPoints.empty())
379 {
380 // fallback: just stand
381 if (!zealot->isMoving() && !zealot->isAttacking())
382 {
383 zealot->holdPosition();
384 }
385 break;
386 }
387
388 // advance target when reached
389 if (!proxyCurTarget.isValid() || zealot->getDistance(proxyCurTarget) <= 96)
390 {
391 if (proxyPoints.empty())
392 {
393 proxyCurTarget = BWAPI::Positions::Invalid;
394 break;
395 }
396
397 if (proxyNextIdx < 0 || proxyNextIdx >= (int)proxyPoints.size())
398 {
399 proxyNextIdx = 0;
400 }
401
402 proxyCurTarget = proxyPoints[proxyNextIdx];
403 ++proxyNextIdx;
404 }
405
406 BWAPI::Position tgt = proxyCurTarget;
407
408 if (!isWalkablePx(tgt) || !hasGroundPath(zealot->getPosition(), tgt))
409 {
410 tgt = findReachableNearby(tgt);
411 proxyCurTarget = tgt;
412 }
413
414 issueMove(tgt, /*force*/false, /*reissueDist*/64);
415
416 break;
417 }
418
419 case State::Done:
420 default:
421 break;
422 }
423
424 // tiny debug
425 /*if (enemyNaturalPos.isValid())
426 Broodwar->drawCircleMap(enemyNaturalPos, 12, Colors::Cyan, true);
427 */
428}
void rebuildProxyPoints()
Builds patrol points used for proxy scouting.
void setEnemyMain(const BWAPI::TilePosition &tp)
Sets the enemy main base location and updates scouting behavior.
void computeEnemyNatural()
Determines the enemy natural expansion location.
void retreatHomeMicro(BWAPI::Unit threat)
Executes retreat and kiting behavior toward home base.
BWAPI::Position pickEdgeOfVisionSpot()
Selects a position near the edge of enemy vision at the natural.
BWAPI::Position findReachableNearby(const BWAPI::Position &desired) const
Finds a reachable nearby position if the desired location is invalid.
void issueMove(const BWAPI::Position &p, bool force=false, int reissueDist=32)
Issues movement commands with anti-stuck handling.

◆ onStart()

void ScoutingZealot::onStart ( )

Initializes the zealot scouting state.

Sets initial state and tracking values, including home position and movement timers.

Definition at line 136 of file ScoutingZealot.cpp.

136 {
137 // We only act when assigned + enemy main becomes known
138 state = State::Idle;
139 lastMoveIssueFrame = 0;
140 lastThreatFrame = -10000;
141 g_myMainPos = BWAPI::Position(BWAPI::Broodwar->self()->getStartLocation());
142 //BWAPI::Broodwar->printf("[ZealotScout] onStart()");
143}

◆ onUnitDestroy()

void ScoutingZealot::onUnitDestroy ( BWAPI::Unit unit)

Handles cleanup when the scouting unit is destroyed.

Clears references and marks behavior as complete.

Parameters
unitDestroyed unit

Definition at line 211 of file ScoutingZealot.cpp.

211 {
212 if (zealot && unit == zealot) {
213 zealot = nullptr;
214 state = State::Done;
215 }
216}

◆ pickEdgeOfVisionSpot()

BWAPI::Position ScoutingZealot::pickEdgeOfVisionSpot ( )
private

Selects a position near the edge of enemy vision at the natural.

Ensures the unit remains outside the enemy main while maintaining vision of key areas.

Definition at line 494 of file ScoutingZealot.cpp.

495{
496 if (!zealot || !zealot->exists())
497 {
498 return BWAPI::Positions::Invalid;
499 }
500
501 if (!enemyNaturalPos.isValid())
502 {
503 return enemyNaturalPos;
504 }
505
506 const int now = BWAPI::Broodwar->getFrameCount();
507
508 // Reuse cached perch for a bit to prevent thrash.
509 if (cachedPerch.isValid())
510 {
511 const bool recent = (now - cachedPerchFrame) < kPerchRecalcFrames;
512 const bool closeEnough = zealot->getDistance(cachedPerch) <= 96;
513
514 if (recent || closeEnough)
515 {
516 return cachedPerch;
517 }
518 }
519
520 const auto* natArea = BWEM::Map::Instance().GetArea(enemyNaturalTile);
521 const auto* mainArea = enemyMainTile.has_value()
522 ? BWEM::Map::Instance().GetArea(*enemyMainTile)
523 : nullptr;
524
525 BWAPI::Position natCenter = enemyNaturalPos;
526 if (natArea)
527 {
528 for (const auto& b : natArea->Bases())
529 {
530 if (b.Location() == enemyNaturalTile)
531 {
532 long long sx = 0;
533 long long sy = 0;
534 int n = 0;
535
536 for (auto* m : b.Minerals())
537 {
538 if (!m)
539 {
540 continue;
541 }
542
543 BWAPI::Position p(m->Pos());
544 if (!p.isValid())
545 {
546 continue;
547 }
548
549 sx += p.x;
550 sy += p.y;
551 ++n;
552 }
553
554 if (n > 0)
555 {
556 natCenter = BWAPI::Position(int(sx / n), int(sy / n));
557 }
558
559 break;
560 }
561 }
562 }
563
564 BWAPI::Position myCenter = g_myMainPos.isValid()
565 ? g_myMainPos
566 : BWAPI::Position(BWAPI::Broodwar->self()->getStartLocation());
567
568 int dirx = myCenter.x - natCenter.x;
569 int diry = myCenter.y - natCenter.y;
570 if (dirx == 0 && diry == 0)
571 {
572 dirx = 1;
573 }
574
575 const int sight = BWAPI::UnitTypes::Protoss_Zealot.sightRange();
576 const int want = std::max(0, sight - kEdgeMarginPx);
577
578 const double len = std::max(1.0, std::sqrt(double(dirx * dirx + diry * diry)));
579
580 BWAPI::Position candidate(
581 natCenter.x + int((dirx / len) * want),
582 natCenter.y + int((diry / len) * want)
583 );
584
585 BWAPI::Position clamped = clampToMapPx(candidate, 8);
586
587 if (mainArea)
588 {
589 const auto* tgtArea = BWEM::Map::Instance().GetArea(BWAPI::TilePosition(clamped));
590 if (tgtArea == mainArea)
591 {
592 for (int i = 0; i < 3; ++i)
593 {
594 const auto* a = BWEM::Map::Instance().GetArea(BWAPI::TilePosition(clamped));
595 if (a != mainArea)
596 {
597 break;
598 }
599
600 clamped = BWAPI::Position(
601 clamped.x + int((myCenter.x - clamped.x) * 0.3),
602 clamped.y + int((myCenter.y - clamped.y) * 0.3)
603 );
604
605 clamped = clampToMapPx(clamped, 8);
606 }
607 }
608 }
609
610 BWAPI::Position reachable = findReachableNearby(clamped);
611
612 // Cache result to prevent constant goal changes.
613 cachedPerch = reachable;
614 cachedPerchFrame = now;
615
616 return cachedPerch;
617}

◆ pickNaturalChokeSpot()

BWAPI::Position ScoutingZealot::pickNaturalChokeSpot ( )
private

Definition at line 619 of file ScoutingZealot.cpp.

620{
621 if (!zealot || !zealot->exists())
622 {
623 return BWAPI::Positions::Invalid;
624 }
625
626 if (!enemyNaturalPos.isValid())
627 {
628 return enemyNaturalPos;
629 }
630
631 const int now = BWAPI::Broodwar->getFrameCount();
632
633 if (cachedPerch.isValid())
634 {
635 const bool recent = (now - cachedPerchFrame) < kPerchRecalcFrames;
636 const bool closeEnough = zealot->getDistance(cachedPerch) <= 96;
637
638 if (recent || closeEnough)
639 {
640 return cachedPerch;
641 }
642 }
643
644 const BWAPI::Position myCenter = g_myMainPos.isValid()
645 ? g_myMainPos
646 : BWAPI::Position(BWAPI::Broodwar->self()->getStartLocation());
647
648 const BWEM::Area* natArea = BWEM::Map::Instance().GetArea(enemyNaturalTile);
649 if (!natArea)
650 {
651 BWAPI::Position fallback = findReachableNearby(clampToMapPx(enemyNaturalPos, 8));
652 cachedPerch = fallback;
653 cachedPerchFrame = now;
654 return cachedPerch;
655 }
656
657 BWAPI::Position best = BWAPI::Positions::Invalid;
658 double bestScore = 1e30;
659
660 for (const BWEM::ChokePoint* cp : natArea->ChokePoints())
661 {
662 if (!cp)
663 {
664 continue;
665 }
666
667 BWAPI::Position c(cp->Center());
668 c = clampToMapPx(c, 8);
669
670 if (!c.isValid())
671 {
672 continue;
673 }
674
675 if (!hasGroundPath(myCenter, c))
676 {
677 continue;
678 }
679
680 // Score:
681 // - close to natural
682 // - also "toward our base" by preferring shorter ground path to our base
683 const double dn = groundPathLengthPx(enemyNaturalPos, c);
684 const double dh = groundPathLengthPx(myCenter, c);
685
686 const double score = dn + (0.45 * dh);
687
688 if (score < bestScore)
689 {
690 bestScore = score;
691 best = c;
692 }
693 }
694
695 if (!best.isValid())
696 {
697 BWAPI::Position fallback = findReachableNearby(clampToMapPx(enemyNaturalPos, 8));
698 cachedPerch = fallback;
699 cachedPerchFrame = now;
700 return cachedPerch;
701 }
702
703 // Nudge the choke point a little toward our base so we stay on "our side"
704 {
705 int vx = myCenter.x - best.x;
706 int vy = myCenter.y - best.y;
707
708 if (vx == 0 && vy == 0)
709 {
710 vx = 1;
711 }
712
713 const double len = std::max(1.0, std::sqrt(double(vx * vx + vy * vy)));
714 const int stepPx = 3 * 32; // 3 tiles toward home
715
716 best = BWAPI::Position(
717 best.x + int((vx / len) * stepPx),
718 best.y + int((vy / len) * stepPx)
719 );
720
721 best = clampToMapPx(best, 8);
722 }
723
724 // Avoid silly spots around geysers/minerals, then snap to reachable
725 best = pushAwayFromResources(best, 3 * 32);
726 best = findReachableNearby(best);
727
728 cachedPerch = best;
729 cachedPerchFrame = now;
730
731 return cachedPerch;
732}

◆ pushAwayFromResources()

BWAPI::Position ScoutingZealot::pushAwayFromResources ( const BWAPI::Position & p,
int clearPx ) const
private

Definition at line 812 of file ScoutingZealot.cpp.

813{
814 BWAPI::Position p = start;
815
816 if (!p.isValid())
817 {
818 return p;
819 }
820
821 for (int i = 0; i < 6; ++i)
822 {
823 BWAPI::Unit r = closestNeutralResourceTo(p);
824 if (!r)
825 {
826 break;
827 }
828
829 const BWAPI::Position rp = r->getPosition();
830 const int d = rp.getApproxDistance(p);
831
832 if (d >= clearPx)
833 {
834 break;
835 }
836
837 int vx = p.x - rp.x;
838 int vy = p.y - rp.y;
839
840 if (vx == 0 && vy == 0)
841 {
842 vx = 1;
843 }
844
845 const double len = std::max(1.0, std::sqrt(double(vx * vx + vy * vy)));
846 const int need = clearPx - d;
847
848 p = BWAPI::Position(
849 p.x + int((vx / len) * need),
850 p.y + int((vy / len) * need)
851 );
852
853 p = clampToMapPx(p, 16);
854 }
855
856 return p;
857}

◆ rebuildProxyPoints()

void ScoutingZealot::rebuildProxyPoints ( )
private

Builds patrol points used for proxy scouting.

Generates positions around:

  • Main base chokes
  • Neighboring areas
  • Natural expansion
  • Map perimeter regions

Points are filtered for reachability and spacing.

Definition at line 1045 of file ScoutingZealot.cpp.

1046{
1047 if (proxyPointsBuiltOnce && !cachedProxyPoints.empty())
1048 {
1049 proxyPoints = cachedProxyPoints;
1050 proxyNextIdx = 0;
1051 proxyCurTarget = BWAPI::Positions::Invalid;
1052 return;
1053 }
1054 proxyPoints.clear();
1055 proxyNextIdx = 0;
1056 proxyCurTarget = BWAPI::Positions::Invalid;
1057
1058 if (!zealot || !zealot->exists())
1059 {
1060 return;
1061 }
1062
1063
1064
1065
1066 const BWAPI::TilePosition myMainTp = BWAPI::Broodwar->self()->getStartLocation();
1067 const BWEM::Area* myMainArea = BWEM::Map::Instance().GetArea(myMainTp);
1068 if (!myMainArea)
1069 {
1070 return;
1071 }
1072
1073 const BWAPI::Position myMainCenter = BWAPI::Position(myMainTp) + BWAPI::Position(16, 16);
1074
1075 auto isInArea = [&](const BWEM::Area* area, const BWAPI::TilePosition& t) -> bool
1076 {
1077 if (!t.isValid() || !area)
1078 {
1079 return false;
1080 }
1081
1082 const BWEM::Area* a = BWEM::Map::Instance().GetArea(t);
1083 return a == area;
1084 };
1085
1086 auto addPoint = [&](BWAPI::Position p)
1087 {
1088 if (!p.isValid())
1089 {
1090 return;
1091 }
1092
1093 p = clampToMapPx(p, 16);
1094
1095 if (!hasGroundPath(myMainCenter, p))
1096 {
1097 return;
1098 }
1099
1100 if (!isWalkablePx(p))
1101 {
1102 // Snap to something nearby that we can actually stand on
1103 p = findReachableNearby(p);
1104
1105 if (!p.isValid() || !hasGroundPath(myMainCenter, p))
1106 {
1107 return;
1108 }
1109 }
1110
1111 p = pushAwayFromResources(p, 3 * 32);
1112 p = findReachableNearby(p);
1113
1114 if (!p.isValid() || !hasGroundPath(myMainCenter, p))
1115 {
1116 return;
1117 }
1118
1119 for (const auto& existing : proxyPoints)
1120 {
1121 if (isNear(existing, p, 3 * 32)) // was 5*32
1122 {
1123 return;
1124 }
1125 }
1126
1127 proxyPoints.push_back(p);
1128
1129 };
1130
1131
1132
1133 // 1) Main-area chokes
1134 for (const BWEM::ChokePoint* cp : myMainArea->ChokePoints())
1135 {
1136 if (!cp)
1137 {
1138 continue;
1139 }
1140 addPoint(BWAPI::Position(cp->Center()));
1141 }
1142
1143 // 2) Neighbor-area chokes (typical proxy routes)
1144 for (const BWEM::Area* n : myMainArea->AccessibleNeighbours())
1145 {
1146 if (!n)
1147 {
1148 continue;
1149 }
1150
1151 for (const BWEM::ChokePoint* cp : n->ChokePoints())
1152 {
1153 if (!cp)
1154 {
1155 continue;
1156 }
1157 addPoint(BWAPI::Position(cp->Center()));
1158 }
1159 }
1160
1161 // 3) Add our natural base center (closest non-starting base not in main area)
1162
1163
1164 double bestDist = 1e30;
1165 BWAPI::Position bestNat = BWAPI::Positions::Invalid;
1166
1167 for (const BWEM::Area& area : BWEM::Map::Instance().Areas())
1168 {
1169 for (const BWEM::Base& base : area.Bases())
1170 {
1171 if (base.Starting())
1172 {
1173 continue;
1174 }
1175
1176 const BWEM::Area* bArea = BWEM::Map::Instance().GetArea(base.Location());
1177 if (bArea == myMainArea)
1178 {
1179 continue;
1180 }
1181
1182 BWAPI::Position basePos = base.Center();
1183 if (!basePos.isValid())
1184 {
1185 basePos = BWAPI::Position(base.Location()) + BWAPI::Position(16, 16);
1186 }
1187
1188 const auto& path = BWEM::Map::Instance().GetPath(myMainCenter, basePos, nullptr);
1189 if (path.empty())
1190 {
1191 continue;
1192 }
1193
1194 const double d = groundPathLengthPx(myMainCenter, basePos);
1195 if (d < bestDist)
1196 {
1197 bestDist = d;
1198 bestNat = basePos;
1199 }
1200 }
1201 }
1202
1203 if (bestNat.isValid())
1204 {
1205 addPoint(bestNat);
1206 }
1207
1208 // If we somehow got nothing, at least patrol home
1209 if (proxyPoints.empty())
1210 {
1211 addPoint(g_myMainPos);
1212 }
1213
1214
1215
1216 // 4) Perimeter ring: boundary tiles of our main area (proxy pylons tucked around edges)
1217 {
1218 const int stepTiles = 4; // lower = more points;
1219 const BWAPI::TilePosition topLeft = myMainArea->TopLeft();
1220 const BWAPI::TilePosition bottomRight = myMainArea->BottomRight();
1221
1222 for (int y = topLeft.y; y <= bottomRight.y; y += stepTiles)
1223 {
1224 for (int x = topLeft.x; x <= bottomRight.x; x += stepTiles)
1225 {
1226 BWAPI::TilePosition t(x, y);
1227 if (!isInArea(myMainArea, t))
1228 {
1229 continue;
1230 }
1231
1232 // If any 4-neighbor is NOT in our main area, this is a boundary-ish tile
1233 const BWAPI::TilePosition n1(x + 1, y);
1234 const BWAPI::TilePosition n2(x - 1, y);
1235 const BWAPI::TilePosition n3(x, y + 1);
1236 const BWAPI::TilePosition n4(x, y - 1);
1237
1238 if (isInArea(myMainArea, n1) && isInArea(myMainArea, n2) && isInArea(myMainArea, n3) && isInArea(myMainArea, n4))
1239 {
1240 continue;
1241 }
1242
1243 addPoint(BWAPI::Position(t) + BWAPI::Position(16, 16));
1244 }
1245 }
1246 }
1247 /*
1248 // 5) Air-close but ground-far points (cliff/maze-y proxy spots near us)
1249 {
1250 const double kMinRatio = 1.4;
1251 const int kMaxAirDist = 40 * 32;
1252 const int sampleStepTiles = 3;
1253 const double kMaxGroundDist = 140.0 * 32.0;
1254
1255 const int margin = 12;
1256 const BWAPI::TilePosition topLeft = myMainArea->TopLeft();
1257 const BWAPI::TilePosition bottomRight = myMainArea->BottomRight();
1258
1259 for (int y = topLeft.y - margin; y <= bottomRight.y + margin; y += sampleStepTiles)
1260 {
1261 for (int x = topLeft.x - margin; x <= bottomRight.x + margin; x += sampleStepTiles)
1262 {
1263 BWAPI::TilePosition t(x, y);
1264 if (!t.isValid())
1265 {
1266 continue;
1267 }
1268
1269 BWAPI::Position p = BWAPI::Position(t) + BWAPI::Position(16, 16);
1270 p = clampToMapPx(p, 16);
1271
1272 const int air = myMainCenter.getApproxDistance(p);
1273 if (air > kMaxAirDist)
1274 {
1275 continue;
1276 }
1277
1278 const auto& path = BWEM::Map::Instance().GetPath(myMainCenter, p, nullptr);
1279 if (path.empty())
1280 {
1281 continue;
1282 }
1283
1284 const double ground = groundPathLengthPx(myMainCenter, p);
1285 if (ground <= 1.0)
1286 {
1287 continue;
1288 }
1289
1290 if (ground > kMaxGroundDist)
1291 {
1292 continue;
1293 }
1294
1295 if (ground >= double(air) * kMinRatio)
1296 {
1297 addPoint(p);
1298 }
1299 }
1300 }
1301 }*/
1302 auto addRing = [&](BWAPI::Position center, int radius, int n)
1303 {
1304 for (int i = 0; i < n; ++i)
1305 {
1306 const double a = (6.28318530718 * i) / double(n);
1307 BWAPI::Position p(
1308 center.x + int(std::cos(a) * radius),
1309 center.y + int(std::sin(a) * radius)
1310 );
1311
1312 addPoint(p);
1313 }
1314 };
1315
1316 if ((int)proxyPoints.size() < 6)
1317 {
1318 BWAPI::Position c = myMainCenter;
1319 addRing(c, 8 * 32, 12);
1320 addRing(c, 12 * 32, 12);
1321
1322 if (bestNat.isValid())
1323 {
1324 addRing(bestNat, 6 * 32, 10);
1325 }
1326 }
1327
1328
1329 // Order: nearest-neighbor greedy starting from zealot
1330 std::vector<BWAPI::Position> ordered;
1331 ordered.reserve(proxyPoints.size());
1332
1333 BWAPI::Position cur = zealot->getPosition();
1334
1335 while (!proxyPoints.empty())
1336 {
1337 int bestIdx = 0;
1338 int bestD = INT_MAX;
1339
1340 for (int i = 0; i < (int)proxyPoints.size(); ++i)
1341 {
1342 const int d = cur.getApproxDistance(proxyPoints[i]);
1343 if (d < bestD)
1344 {
1345 bestD = d;
1346 bestIdx = i;
1347 }
1348 }
1349
1350 ordered.push_back(proxyPoints[bestIdx]);
1351 cur = proxyPoints[bestIdx];
1352 proxyPoints.erase(proxyPoints.begin() + bestIdx);
1353 }
1354
1355
1356
1357 proxyPoints = std::move(ordered);
1358
1359
1360 cachedProxyPoints = proxyPoints;
1361 proxyPointsBuiltOnce = !cachedProxyPoints.empty();
1362}

◆ retreatHomeMicro()

void ScoutingZealot::retreatHomeMicro ( BWAPI::Unit threat)
private

Executes retreat and kiting behavior toward home base.

Attempts to attack if possible, otherwise moves away from threats while maintaining safe distance.

Parameters
threatPrimary threat unit

Definition at line 1411 of file ScoutingZealot.cpp.

1412{
1413 if (!zealot || !zealot->exists())
1414 {
1415 return;
1416 }
1417
1418 // If we're mid-windup / firing, don't change anything.
1419 if (zealot->isStartingAttack() || zealot->isAttackFrame())
1420 {
1421 return;
1422 }
1423
1424 const int now = BWAPI::Broodwar->getFrameCount();
1425
1426 BWAPI::Unit target = nullptr;
1427
1428 // Stick to last target for a short window to avoid retarget stutter.
1429 if (lastAttackTarget && lastAttackTarget->exists())
1430 {
1431 if (now - lastTargetSelectFrame < kTargetStickFrames)
1432 {
1433 if (isGoodKiteTarget(lastAttackTarget, kThreatRadiusPx))
1434 {
1435 target = lastAttackTarget;
1436 }
1437 }
1438 }
1439
1440 // If we don't have a sticky target, pick a new one.
1441 if (!target)
1442 {
1443 BWAPI::Unit candidate = nullptr;
1444
1445 if (isGoodKiteTarget(threat, kThreatRadiusPx))
1446 {
1447 candidate = threat;
1448 }
1449 else
1450 {
1451 candidate = findPrimaryThreat(kThreatRadiusPx);
1452 }
1453
1454 target = candidate;
1455
1456 if (target)
1457 {
1458 lastAttackTarget = target;
1459 lastTargetSelectFrame = now;
1460 }
1461 }
1462
1463 // If in range, try to shoot (tryFireAndCommit also "locks" the target).
1464 if (target && target->exists())
1465 {
1466 if (tryFireAndCommit(target))
1467 {
1468 return;
1469 }
1470 }
1471
1472 // No shot right now: kite step.
1473 issueMove(homeRetreatPoint(), true, 64);
1474}
bool tryFireAndCommit(BWAPI::Unit target)
Attempts to attack a target and commit to the attack briefly.

◆ setEnemyMain()

void ScoutingZealot::setEnemyMain ( const BWAPI::TilePosition & tp)

Sets the enemy main base location and updates scouting behavior.

Computes the enemy natural and transitions the unit to natural-edge scouting unless acting as a proxy patroller.

Parameters
tpEnemy main tile position

Definition at line 183 of file ScoutingZealot.cpp.

183 {
184 enemyMainTile = tp;
185 enemyMainPos = Position(tp);
186
187 // If this zealot is the proxy patrol zealot,
188 // do NOT switch state into enemy natural logic.
189 if (state == State::ProxyPatrol)
190 {
191 return;
192 }
193
195 state = enemyNaturalPos.isValid() ? State::MoveToNatural : State::WaitEnemyMain;
196
197 // Immediate course set to the natural edge perch (don’t step into main first)
198 if (enemyNaturalPos.isValid()) {
199 issueMove(pickEdgeOfVisionSpot(), /*force*/true);
200 //BWAPI::Broodwar->printf("[ZealotScout] setEnemyMain (%d,%d)", tp.x, tp.y);
201 }
202}

◆ setProxyPatroller()

void ScoutingZealot::setProxyPatroller ( bool v)
inline

Definition at line 39 of file ScoutingZealot.h.

40 {
41 isProxyPatroller = v;
42 }

◆ threatenedNow()

bool ScoutingZealot::threatenedNow ( ) const
private

Checks whether the unit is currently threatened.

Considers nearby enemy combat units and attack status.

Returns
True if under threat

Definition at line 887 of file ScoutingZealot.cpp.

887 {
888 if (!zealot || !zealot->exists()) return false;
889 if (zealot->isUnderAttack()) return true;
890
891 for (auto& e : Broodwar->enemy()->getUnits()) {
892 if (!e || !e->exists()) continue;
893 if (e->getType().isWorker()) continue; // <-- ignore workers
894 if (!e->getType().canAttack()) continue; // skip non-combat
895 if (e->getDistance(zealot) < kThreatRadiusPx) // inside danger bubble
896 return true;
897 }
898 return false;
899}

◆ tryFireAndCommit()

bool ScoutingZealot::tryFireAndCommit ( BWAPI::Unit target)
private

Attempts to attack a target and commit to the attack briefly.

Prevents attack cancellation during weapon windup.

Parameters
targetTarget unit
Returns
True if attack was issued or committed

Definition at line 1546 of file ScoutingZealot.cpp.

1547{
1548 if (!zealot || !zealot->exists() || !target || !target->exists())
1549 {
1550 return false;
1551 }
1552
1553 if (!zealot->canAttackUnit(target))
1554 {
1555 return false;
1556 }
1557
1558 if (!zealot->isInWeaponRange(target))
1559 {
1560 return false;
1561 }
1562
1563 // Lock target choice even if weapon is cooling down.
1564 lastAttackTarget = target;
1565
1566 const int now = BWAPI::Broodwar->getFrameCount();
1567 const int commit = attackCommitFrames();
1568
1569 if (now - lastAttackCmdFrame < commit && lastAttackTarget == target)
1570 {
1571 lastTargetSelectFrame = now;
1572 return true;
1573 }
1574
1575 if (zealot->getGroundWeaponCooldown() == 0)
1576 {
1577 zealot->attack(target);
1578 lastAttackCmdFrame = now;
1579 lastTargetSelectFrame = now;
1580 return true;
1581 }
1582
1583 return false;
1584}

Member Data Documentation

◆ cachedPerch

BWAPI::Position ScoutingZealot::cachedPerch = BWAPI::Positions::Invalid
private

Definition at line 98 of file ScoutingZealot.h.

◆ cachedPerchFrame

int ScoutingZealot::cachedPerchFrame = -100000
private

Definition at line 99 of file ScoutingZealot.h.

◆ cachedProxyPoints

std::vector<BWAPI::Position> ScoutingZealot::cachedProxyPoints
private

Definition at line 93 of file ScoutingZealot.h.

◆ commanderRef

ProtoBotCommander* ScoutingZealot::commanderRef = nullptr
private

Definition at line 58 of file ScoutingZealot.h.

◆ enemyMainPos

BWAPI::Position ScoutingZealot::enemyMainPos = BWAPI::Positions::Invalid
private

Definition at line 63 of file ScoutingZealot.h.

◆ enemyMainTile

std::optional<BWAPI::TilePosition> ScoutingZealot::enemyMainTile
private

Definition at line 62 of file ScoutingZealot.h.

◆ enemyNaturalPos

BWAPI::Position ScoutingZealot::enemyNaturalPos = BWAPI::Positions::Invalid
private

Definition at line 65 of file ScoutingZealot.h.

◆ enemyNaturalTile

BWAPI::TilePosition ScoutingZealot::enemyNaturalTile = BWAPI::TilePositions::Invalid
private

Definition at line 64 of file ScoutingZealot.h.

◆ isProxyPatroller

bool ScoutingZealot::isProxyPatroller = false
private

Definition at line 92 of file ScoutingZealot.h.

◆ kCalmFramesToReturn

int ScoutingZealot::kCalmFramesToReturn = 72
staticconstexprprivate

Definition at line 73 of file ScoutingZealot.h.

◆ kDangerClosePx

int ScoutingZealot::kDangerClosePx = 96
staticconstexprprivate

Definition at line 75 of file ScoutingZealot.h.

◆ kEdgeMarginPx

int ScoutingZealot::kEdgeMarginPx = 24
staticconstexprprivate

Definition at line 70 of file ScoutingZealot.h.

◆ kGoalChangeResetDist

int ScoutingZealot::kGoalChangeResetDist = 64
staticconstexprprivate

Definition at line 107 of file ScoutingZealot.h.

◆ kMaxGroundDist

double ScoutingZealot::kMaxGroundDist = 180 * 32
staticconstexprprivate

Definition at line 84 of file ScoutingZealot.h.

◆ kMoveCooldownFrames

int ScoutingZealot::kMoveCooldownFrames = 8
staticconstexprprivate

Definition at line 69 of file ScoutingZealot.h.

◆ kPerchRecalcFrames

int ScoutingZealot::kPerchRecalcFrames = 24
staticconstexprprivate

Definition at line 106 of file ScoutingZealot.h.

◆ kProxyArriveDist

int ScoutingZealot::kProxyArriveDist = 96
staticconstexprprivate

Definition at line 82 of file ScoutingZealot.h.

◆ kProxyMinBetweenMoves

int ScoutingZealot::kProxyMinBetweenMoves = 12
staticconstexprprivate

Definition at line 83 of file ScoutingZealot.h.

◆ kProxyRebuildEveryFrames

int ScoutingZealot::kProxyRebuildEveryFrames = 24 * 10
staticconstexprprivate

Definition at line 81 of file ScoutingZealot.h.

◆ kRepositionStepPx

int ScoutingZealot::kRepositionStepPx = 160
staticconstexprprivate

Definition at line 72 of file ScoutingZealot.h.

◆ kTargetStickFrames

int ScoutingZealot::kTargetStickFrames = 18
staticconstexprprivate

Definition at line 78 of file ScoutingZealot.h.

◆ kThreatRadiusPx

int ScoutingZealot::kThreatRadiusPx = 256
staticconstexprprivate

Definition at line 71 of file ScoutingZealot.h.

◆ lastAttackCmdFrame

int ScoutingZealot::lastAttackCmdFrame = -100000
private

Definition at line 76 of file ScoutingZealot.h.

◆ lastAttackTarget

BWAPI::Unit ScoutingZealot::lastAttackTarget = nullptr
private

Definition at line 130 of file ScoutingZealot.h.

◆ lastIssuedGoal

BWAPI::Position ScoutingZealot::lastIssuedGoal = BWAPI::Positions::Invalid
private

Definition at line 101 of file ScoutingZealot.h.

◆ lastMoveIssueFrame

int ScoutingZealot::lastMoveIssueFrame = 0
private

Definition at line 68 of file ScoutingZealot.h.

◆ lastPos

BWAPI::Position ScoutingZealot::lastPos = BWAPI::Positions::Invalid
private

Definition at line 103 of file ScoutingZealot.h.

◆ lastTargetSelectFrame

int ScoutingZealot::lastTargetSelectFrame = -100000
private

Definition at line 77 of file ScoutingZealot.h.

◆ lastThreatFrame

int ScoutingZealot::lastThreatFrame = -100000
private

Definition at line 74 of file ScoutingZealot.h.

◆ manager

ScoutingManager* ScoutingZealot::manager = nullptr
private

Definition at line 59 of file ScoutingZealot.h.

◆ proxyCurTarget

BWAPI::Position ScoutingZealot::proxyCurTarget = BWAPI::Positions::Invalid
private

Definition at line 91 of file ScoutingZealot.h.

◆ proxyNextIdx

int ScoutingZealot::proxyNextIdx = 0
private

Definition at line 88 of file ScoutingZealot.h.

◆ proxyNextMoveFrame

int ScoutingZealot::proxyNextMoveFrame = 0
private

Definition at line 90 of file ScoutingZealot.h.

◆ proxyNextRebuildFrame

int ScoutingZealot::proxyNextRebuildFrame = 0
private

Definition at line 89 of file ScoutingZealot.h.

◆ proxyPoints

std::vector<BWAPI::Position> ScoutingZealot::proxyPoints
private

Definition at line 87 of file ScoutingZealot.h.

◆ proxyPointsBuiltOnce

bool ScoutingZealot::proxyPointsBuiltOnce = false
private

Definition at line 94 of file ScoutingZealot.h.

◆ proxyRebuildReadyFrame

int ScoutingZealot::proxyRebuildReadyFrame = 0
private

Definition at line 85 of file ScoutingZealot.h.

◆ returnStateAfterReposition

State ScoutingZealot::returnStateAfterReposition = State::HoldEdge
private

Definition at line 57 of file ScoutingZealot.h.

◆ state

State ScoutingZealot::state = State::Idle
private

Definition at line 67 of file ScoutingZealot.h.

◆ stuckFrames

int ScoutingZealot::stuckFrames = 0
private

Definition at line 104 of file ScoutingZealot.h.

◆ zealot

BWAPI::Unit ScoutingZealot::zealot = nullptr
private

Definition at line 60 of file ScoutingZealot.h.


The documentation for this class was generated from the following files: