@supalosa/chronodivide-bot 0.2.1 → 0.2.2-a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +3 -3
  2. package/dist/bot/bot.js +7 -3
  3. package/dist/bot/bot.js.map +1 -0
  4. package/dist/bot/logic/awareness.js +12 -2
  5. package/dist/bot/logic/awareness.js.map +1 -0
  6. package/dist/bot/logic/building/ArtilleryUnit.js +29 -8
  7. package/dist/bot/logic/building/antiGroundStaticDefence.js +3 -2
  8. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -0
  9. package/dist/bot/logic/building/artilleryUnit.js.map +1 -0
  10. package/dist/bot/logic/building/basicAirUnit.js +2 -1
  11. package/dist/bot/logic/building/basicAirUnit.js.map +1 -0
  12. package/dist/bot/logic/building/basicBuilding.js +2 -1
  13. package/dist/bot/logic/building/basicBuilding.js.map +1 -0
  14. package/dist/bot/logic/building/basicGroundUnit.js +2 -1
  15. package/dist/bot/logic/building/basicGroundUnit.js.map +1 -0
  16. package/dist/bot/logic/building/buildingRules.js +168 -0
  17. package/dist/bot/logic/building/buildingRules.js.map +1 -0
  18. package/dist/bot/logic/building/harvester.js +1 -0
  19. package/dist/bot/logic/building/harvester.js.map +1 -0
  20. package/dist/bot/logic/building/powerPlant.js +2 -1
  21. package/dist/bot/logic/building/powerPlant.js.map +1 -0
  22. package/dist/bot/logic/building/queueController.js +2 -1
  23. package/dist/bot/logic/building/queueController.js.map +1 -0
  24. package/dist/bot/logic/building/resourceCollectionBuilding.js +2 -1
  25. package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -0
  26. package/dist/bot/logic/common/scout.js +100 -0
  27. package/dist/bot/logic/common/scout.js.map +1 -0
  28. package/dist/bot/logic/common/utils.js +14 -0
  29. package/dist/bot/logic/common/utils.js.map +1 -0
  30. package/dist/bot/logic/map/map.js +9 -25
  31. package/dist/bot/logic/map/map.js.map +1 -0
  32. package/dist/bot/logic/map/sector.js +33 -1
  33. package/dist/bot/logic/map/sector.js.map +1 -0
  34. package/dist/bot/logic/mission/mission.js +3 -1
  35. package/dist/bot/logic/mission/mission.js.map +1 -0
  36. package/dist/bot/logic/mission/missionController.js +3 -2
  37. package/dist/bot/logic/mission/missionController.js.map +1 -0
  38. package/dist/bot/logic/mission/missionFactories.js +3 -0
  39. package/dist/bot/logic/mission/missionFactories.js.map +1 -0
  40. package/dist/bot/logic/mission/missions/attackMission.js +8 -7
  41. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -0
  42. package/dist/bot/logic/mission/missions/defenceMission.js +11 -6
  43. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -0
  44. package/dist/bot/logic/mission/missions/engineerMission.js +34 -0
  45. package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -0
  46. package/dist/bot/logic/mission/missions/expansionMission.js +5 -4
  47. package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -0
  48. package/dist/bot/logic/mission/missions/oneTimeMission.js +3 -2
  49. package/dist/bot/logic/mission/missions/oneTimeMission.js.map +1 -0
  50. package/dist/bot/logic/mission/missions/retreatMission.js +3 -2
  51. package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -0
  52. package/dist/bot/logic/mission/missions/scoutingMission.js +8 -9
  53. package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -0
  54. package/dist/bot/logic/squad/behaviours/combatSquad.js +19 -17
  55. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -0
  56. package/dist/bot/logic/squad/behaviours/common.js +20 -2
  57. package/dist/bot/logic/squad/behaviours/common.js.map +1 -0
  58. package/dist/bot/logic/squad/behaviours/engineerSquad.js +36 -0
  59. package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +1 -0
  60. package/dist/bot/logic/squad/behaviours/expansionSquad.js +1 -0
  61. package/dist/bot/logic/squad/behaviours/expansionSquad.js.map +1 -0
  62. package/dist/bot/logic/squad/behaviours/retreatSquad.js +1 -0
  63. package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -0
  64. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +66 -18
  65. package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -0
  66. package/dist/bot/logic/squad/squad.js +4 -5
  67. package/dist/bot/logic/squad/squad.js.map +1 -0
  68. package/dist/bot/logic/squad/squadBehaviour.js +1 -0
  69. package/dist/bot/logic/squad/squadBehaviour.js.map +1 -0
  70. package/dist/bot/logic/squad/squadBehaviours.js +1 -0
  71. package/dist/bot/logic/squad/squadBehaviours.js.map +1 -0
  72. package/dist/bot/logic/squad/squadController.js +58 -18
  73. package/dist/bot/logic/squad/squadController.js.map +1 -0
  74. package/dist/bot/logic/threat/threat.js +1 -0
  75. package/dist/bot/logic/threat/threat.js.map +1 -0
  76. package/dist/bot/logic/threat/threatCalculator.js +1 -0
  77. package/dist/bot/logic/threat/threatCalculator.js.map +1 -0
  78. package/dist/exampleBot.js +7 -6
  79. package/dist/exampleBot.js.map +1 -0
  80. package/package.json +15 -7
  81. package/src/bot/bot.ts +10 -7
  82. package/src/bot/logic/awareness.ts +21 -4
  83. package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -60
  84. package/src/bot/logic/building/artilleryUnit.ts +68 -0
  85. package/src/bot/logic/building/basicAirUnit.ts +68 -68
  86. package/src/bot/logic/building/basicBuilding.ts +47 -47
  87. package/src/bot/logic/building/basicGroundUnit.ts +1 -1
  88. package/src/bot/logic/building/buildingRules.ts +233 -0
  89. package/src/bot/logic/building/powerPlant.ts +32 -32
  90. package/src/bot/logic/building/queueController.ts +1 -1
  91. package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -56
  92. package/src/bot/logic/common/scout.ts +127 -1
  93. package/src/bot/logic/common/utils.ts +17 -0
  94. package/src/bot/logic/map/map.ts +70 -84
  95. package/src/bot/logic/map/sector.ts +46 -4
  96. package/src/bot/logic/mission/mission.ts +2 -2
  97. package/src/bot/logic/mission/missionController.ts +2 -3
  98. package/src/bot/logic/mission/missionFactories.ts +5 -0
  99. package/src/bot/logic/mission/missions/attackMission.ts +25 -20
  100. package/src/bot/logic/mission/missions/defenceMission.ts +34 -14
  101. package/src/bot/logic/mission/missions/engineerMission.ts +61 -0
  102. package/src/bot/logic/mission/missions/expansionMission.ts +6 -4
  103. package/src/bot/logic/mission/missions/oneTimeMission.ts +3 -2
  104. package/src/bot/logic/mission/missions/retreatMission.ts +3 -2
  105. package/src/bot/logic/mission/missions/scoutingMission.ts +9 -6
  106. package/src/bot/logic/squad/behaviours/combatSquad.ts +21 -17
  107. package/src/bot/logic/squad/behaviours/common.ts +33 -2
  108. package/src/bot/logic/squad/behaviours/engineerSquad.ts +53 -0
  109. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +79 -26
  110. package/src/bot/logic/squad/squad.ts +4 -4
  111. package/src/bot/logic/squad/squadBehaviour.ts +3 -1
  112. package/src/bot/logic/squad/squadController.ts +89 -44
  113. package/src/exampleBot.ts +6 -6
  114. package/tsconfig.json +73 -73
  115. package/src/bot/logic/building/ArtilleryUnit.ts +0 -43
  116. package/src/bot/logic/building/building.ts +0 -127
@@ -7,13 +7,15 @@ import { Mission } from "../mission.js";
7
7
  import { AttackMission } from "./attackMission.js";
8
8
  import { MissionController } from "../missionController.js";
9
9
  import { getUnseenStartingLocations } from "../../common/scout.js";
10
+ import { DebugLogger } from "../../common/utils.js";
10
11
 
11
12
  /**
12
13
  * A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)
13
14
  */
14
15
  export class ScoutingMission extends OneTimeMission {
15
- constructor(uniqueName: string, priority: number) {
16
- super(uniqueName, priority, () => new ScoutingSquad());
16
+ constructor(uniqueName: string, priority: number,
17
+ logger: DebugLogger) {
18
+ super(uniqueName, priority, () => new ScoutingSquad(), logger);
17
19
  }
18
20
  }
19
21
 
@@ -31,15 +33,15 @@ export class ScoutingMissionFactory implements MissionFactory {
31
33
  playerData: PlayerData,
32
34
  matchAwareness: MatchAwareness,
33
35
  missionController: MissionController,
36
+ logger: DebugLogger
34
37
  ): void {
35
38
  if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
36
39
  return;
37
40
  }
38
- const candidatePoints = getUnseenStartingLocations(gameApi, playerData);
39
- if (candidatePoints.length === 0) {
41
+ if (!matchAwareness.getScoutingManager().hasScoutTargets()) {
40
42
  return;
41
43
  }
42
- if (!missionController.addMission(new ScoutingMission("globalScout", 100))) {
44
+ if (!missionController.addMission(new ScoutingMission("globalScout", 100, logger))) {
43
45
  this.lastScoutAt = gameApi.getCurrentTick();
44
46
  }
45
47
  }
@@ -51,9 +53,10 @@ export class ScoutingMissionFactory implements MissionFactory {
51
53
  failedMission: Mission,
52
54
  failureReason: undefined,
53
55
  missionController: MissionController,
56
+ logger: DebugLogger
54
57
  ): void {
55
58
  if (failedMission instanceof AttackMission) {
56
- missionController.addMission(new ScoutingMission("globalScout", 100));
59
+ missionController.addMission(new ScoutingMission("globalScout", 100, logger));
57
60
  }
58
61
  }
59
62
  }
@@ -1,10 +1,11 @@
1
- import _ from "lodash";
2
- import { ActionsApi, GameApi, MovementZone, PlayerData, Point2D } from "@chronodivide/game-api";
1
+ import maxBy from "lodash.maxby";
2
+ import { ActionsApi, GameApi, MovementZone, PlayerData, Point2D, UnitData } from "@chronodivide/game-api";
3
3
  import { Squad } from "../squad.js";
4
4
  import { SquadAction, SquadBehaviour, grabCombatants, noop } from "../squadBehaviour.js";
5
5
  import { MatchAwareness } from "../../awareness.js";
6
6
  import { getDistanceBetweenPoints } from "../../map/map.js";
7
- import { manageAttackMicro, manageMoveMicro } from "./common.js";
7
+ import { getAttackWeight, manageAttackMicro, manageMoveMicro } from "./common.js";
8
+ import { DebugLogger } from "../../common/utils.js";
8
9
 
9
10
  const TARGET_UPDATE_INTERVAL_TICKS = 10;
10
11
  const GRAB_INTERVAL_TICKS = 10;
@@ -52,8 +53,12 @@ export class CombatSquad implements SquadBehaviour {
52
53
  playerData: PlayerData,
53
54
  squad: Squad,
54
55
  matchAwareness: MatchAwareness,
56
+ logger: DebugLogger,
55
57
  ): SquadAction {
56
- if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
58
+ if (
59
+ squad.getUnitIds().length > 0 &&
60
+ (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)
61
+ ) {
57
62
  this.lastCommand = gameApi.getCurrentTick();
58
63
  const centerOfMass = squad.getCenterOfMass();
59
64
  const maxDistance = squad.getMaxDistanceToCenterOfMass();
@@ -81,6 +86,7 @@ export class CombatSquad implements SquadBehaviour {
81
86
  manageMoveMicro(actionsApi, unit, centerOfMass);
82
87
  });
83
88
  } else {
89
+ logger(`CombatSquad ${squad.getName()} switching back to attack mode (${maxDistance})`);
84
90
  this.state = SquadState.Attacking;
85
91
  }
86
92
  } else {
@@ -93,23 +99,21 @@ export class CombatSquad implements SquadBehaviour {
93
99
  maxDistance > requiredGatherRadius
94
100
  ) {
95
101
  // Switch back to gather mode
102
+ logger(`CombatSquad ${squad.getName()} switching back to gather (${maxDistance})`);
96
103
  this.state = SquadState.Gathering;
97
104
  return noop();
98
105
  }
99
106
  for (const unit of units) {
100
- if (unit.isIdle) {
101
- const { rx: x, ry: y } = unit.tile;
102
- const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
103
- const nearbyHostiles = matchAwareness.getHostilesNearPoint(x, y, range * 2);
104
- const closest = _.minBy(nearbyHostiles, ({ x: hX, y: hY }) =>
105
- getDistanceBetweenPoints({ x, y }, { x: hX, y: hY }),
106
- );
107
- const closestUnit = closest ? gameApi.getUnitData(closest.unitId) ?? null : null;
108
- if (closestUnit) {
109
- manageAttackMicro(actionsApi, unit, closestUnit);
110
- } else {
111
- manageMoveMicro(actionsApi, unit, targetPoint);
112
- }
107
+ const { rx: x, ry: y } = unit.tile;
108
+ const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
109
+ const nearbyHostiles = matchAwareness
110
+ .getHostilesNearPoint(x, y, range * 2)
111
+ .map(({ unitId }) => gameApi.getUnitData(unitId)) as UnitData[];
112
+ const bestUnit = maxBy(nearbyHostiles, (target) => getAttackWeight(unit, target));
113
+ if (bestUnit) {
114
+ manageAttackMicro(actionsApi, unit, bestUnit);
115
+ } else {
116
+ manageMoveMicro(actionsApi, unit, targetPoint);
113
117
  }
114
118
  }
115
119
  }
@@ -1,5 +1,15 @@
1
- import { ActionsApi, AttackState, ObjectType, OrderType, Point2D, StanceType, UnitData } from "@chronodivide/game-api";
2
- import { getDistanceBetweenUnits } from "../../map/map.js";
1
+ import {
2
+ ActionsApi,
3
+ AttackState,
4
+ ObjectType,
5
+ OrderType,
6
+ Point2D,
7
+ StanceType,
8
+ UnitData,
9
+ ZoneType,
10
+ } from "@chronodivide/game-api";
11
+ import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
12
+ import { Zone } from "luxon";
3
13
 
4
14
  // Micro methods
5
15
  export function manageMoveMicro(actionsApi: ActionsApi, attacker: UnitData, attackPoint: Point2D) {
@@ -37,3 +47,24 @@ export function manageAttackMicro(actionsApi: ActionsApi, attacker: UnitData, ta
37
47
  }
38
48
  actionsApi.orderUnits([attacker.id], orderType, target.id);
39
49
  }
50
+
51
+ /**
52
+ *
53
+ * @param attacker
54
+ * @param target
55
+ * @returns A number describing the weight of the given target for the attacker, or null if it should not attack it.
56
+ */
57
+ export function getAttackWeight(attacker: UnitData, target: UnitData): number | null {
58
+ const { rx: x, ry: y } = attacker.tile;
59
+ const { rx: hX, ry: hY } = target.tile;
60
+
61
+ if (!attacker.primaryWeapon?.projectileRules.isAntiAir && target.zone === ZoneType.Air) {
62
+ return null;
63
+ }
64
+
65
+ if (!attacker.primaryWeapon?.projectileRules.isAntiGround && target.zone === ZoneType.Ground) {
66
+ return null;
67
+ }
68
+
69
+ return 1000000 - getDistanceBetweenPoints({ x, y }, { x: hX, y: hY });
70
+ }
@@ -0,0 +1,53 @@
1
+ import { ActionsApi, GameApi, OrderType, PlayerData, SideType } from "@chronodivide/game-api";
2
+ import { Squad } from "../squad.js";
3
+ import { SquadAction, SquadBehaviour, disband, noop, requestSpecificUnits, requestUnits } from "../squadBehaviour.js";
4
+ import { MatchAwareness } from "../../awareness.js";
5
+
6
+ const CAPTURE_COOLDOWN_TICKS = 30;
7
+
8
+ // Capture squad
9
+ export class EngineerSquad implements SquadBehaviour {
10
+ private hasAttemptedCaptureWith: {
11
+ unitId: number;
12
+ gameTick: number;
13
+ } | null = null;
14
+
15
+ /**
16
+ * @param captureTarget ID of the target to try and capture/send engineer into.
17
+ */
18
+ constructor(private captureTarget: number) {
19
+ };
20
+
21
+ public onAiUpdate(
22
+ gameApi: GameApi,
23
+ actionsApi: ActionsApi,
24
+ playerData: PlayerData,
25
+ squad: Squad,
26
+ matchAwareness: MatchAwareness
27
+ ): SquadAction {
28
+ const engineerTypes = ["ENGINEER", "SENGINEER"];
29
+ const engineers = squad.getUnitsOfTypes(gameApi, ...engineerTypes);
30
+ if (engineers.length === 0) {
31
+ // Perhaps we deployed already (or the unit was destroyed), end the mission.
32
+ if (this.hasAttemptedCaptureWith !== null) {
33
+ return disband();
34
+ }
35
+ return requestUnits(engineerTypes, 100);
36
+ } else if (
37
+ !this.hasAttemptedCaptureWith ||
38
+ gameApi.getCurrentTick() > this.hasAttemptedCaptureWith.gameTick + CAPTURE_COOLDOWN_TICKS
39
+ ) {
40
+ actionsApi.orderUnits(
41
+ engineers.map((engineer) => engineer.id),
42
+ OrderType.Capture,
43
+ this.captureTarget
44
+ );
45
+ // Add a cooldown to deploy attempts.
46
+ this.hasAttemptedCaptureWith = {
47
+ unitId: engineers[0].id,
48
+ gameTick: gameApi.getCurrentTick(),
49
+ };
50
+ }
51
+ return noop();
52
+ }
53
+ }
@@ -1,17 +1,29 @@
1
- import { ActionsApi, GameApi, OrderType, PlayerData, Point2D, SideType } from "@chronodivide/game-api";
2
- import { GlobalThreat } from "../../threat/threat.js";
1
+ import { ActionsApi, GameApi, OrderType, PlayerData, Point2D } from "@chronodivide/game-api";
3
2
  import { Squad } from "../squad.js";
4
3
  import { SquadAction, SquadBehaviour, disband, noop, requestUnits } from "../squadBehaviour.js";
5
4
  import { MatchAwareness } from "../../awareness.js";
6
- import { getUnseenStartingLocations } from "../../common/scout.js";
5
+ import { DebugLogger } from "../../common/utils.js";
6
+ import { getDistanceBetweenPoints } from "../../map/map.js";
7
7
 
8
8
  const SCOUT_MOVE_COOLDOWN_TICKS = 30;
9
9
 
10
+ // Max units to spend on a particular scout target.
11
+ const MAX_ATTEMPTS_PER_TARGET = 5;
12
+
13
+ // Maximum ticks to spend trying to scout a target *without making progress towards it*.
14
+ // Every time a unit gets closer to the target, the timer refreshes.
15
+ const MAX_TICKS_PER_TARGET = 600;
16
+
10
17
  export class ScoutingSquad implements SquadBehaviour {
11
- private scoutingWith: {
12
- unitId: number;
13
- gameTick: number;
14
- } | null = null;
18
+ private scoutTarget: Point2D | null = null;
19
+ private attemptsOnCurrentTarget: number = 0;
20
+ private scoutTargetRefreshedAt: number = 0;
21
+ private lastMoveCommandTick: number = 0;
22
+
23
+ // Minimum distance from a scout to the target.
24
+ private scoutMinDistance?: number;
25
+
26
+ private hadUnit: boolean = false;
15
27
 
16
28
  public onAiUpdate(
17
29
  gameApi: GameApi,
@@ -19,6 +31,7 @@ export class ScoutingSquad implements SquadBehaviour {
19
31
  playerData: PlayerData,
20
32
  squad: Squad,
21
33
  matchAwareness: MatchAwareness,
34
+ logger: DebugLogger,
22
35
  ): SquadAction {
23
36
  const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
24
37
  const scouts = squad.getUnitsOfTypes(gameApi, ...scoutNames);
@@ -28,29 +41,69 @@ export class ScoutingSquad implements SquadBehaviour {
28
41
  }
29
42
 
30
43
  if (scouts.length === 0) {
31
- this.scoutingWith = null;
44
+ // Count the number of times the scout dies trying to uncover the current scoutTarget.
45
+ if (this.scoutTarget && this.hadUnit) {
46
+ this.attemptsOnCurrentTarget++;
47
+ this.hadUnit = false;
48
+ }
32
49
  return requestUnits(scoutNames, 100);
33
- } else if (
34
- !this.scoutingWith ||
35
- gameApi.getCurrentTick() > this.scoutingWith.gameTick + SCOUT_MOVE_COOLDOWN_TICKS
36
- ) {
37
- const candidatePoints = getUnseenStartingLocations(gameApi, playerData);
38
- scouts.forEach((unit) => {
39
- if (candidatePoints.length > 0) {
40
- if (unit?.isIdle) {
41
- const scoutLocation =
42
- candidatePoints[Math.floor(gameApi.generateRandom() * candidatePoints.length)];
43
- actionsApi.orderUnits([unit.id], OrderType.AttackMove, scoutLocation.x, scoutLocation.y);
50
+ } else if (this.scoutTarget) {
51
+ this.hadUnit = true;
52
+ if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {
53
+ logger(
54
+ `Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,
55
+ );
56
+ this.setScoutTarget(null, 0);
57
+ return noop();
58
+ }
59
+ if (gameApi.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {
60
+ logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`);
61
+ this.setScoutTarget(null, 0);
62
+ return noop();
63
+ }
64
+ const targetTile = gameApi.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);
65
+ if (!targetTile) {
66
+ throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);
67
+ }
68
+ if (gameApi.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {
69
+ this.lastMoveCommandTick = gameApi.getCurrentTick();
70
+ scouts.forEach((unit) => {
71
+ if (this.scoutTarget) {
72
+ actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);
44
73
  }
74
+ });
75
+ // Check that a scout is actually moving closer to the target.
76
+ const distances = scouts.map((unit) =>
77
+ getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, this.scoutTarget!),
78
+ );
79
+ const newMinDistance = Math.min(...distances);
80
+ if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {
81
+ logger(
82
+ `Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,
83
+ );
84
+ this.scoutTargetRefreshedAt = gameApi.getCurrentTick();
85
+ this.scoutMinDistance = newMinDistance;
45
86
  }
46
- });
47
-
48
- // Add a cooldown to scout attempts.
49
- this.scoutingWith = {
50
- unitId: scouts[0].id,
51
- gameTick: gameApi.getCurrentTick(),
52
- };
87
+ }
88
+ if (gameApi.mapApi.isVisibleTile(targetTile, playerData.name)) {
89
+ logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`);
90
+ this.setScoutTarget(null, gameApi.getCurrentTick());
91
+ }
92
+ } else {
93
+ const candidatePoint = matchAwareness.getScoutingManager().getNewScoutTarget()?.asPoint2D();
94
+ if (!candidatePoint) {
95
+ logger(`No more scouting targets available, disbanding.`);
96
+ return disband();
97
+ }
98
+ this.setScoutTarget(candidatePoint, gameApi.getCurrentTick());
53
99
  }
54
100
  return noop();
55
101
  }
102
+
103
+ setScoutTarget(point: Point2D | null, currentTick: number) {
104
+ this.attemptsOnCurrentTarget = 0;
105
+ this.scoutTargetRefreshedAt = currentTick;
106
+ this.scoutTarget = point;
107
+ this.scoutMinDistance = undefined;
108
+ }
56
109
  }
@@ -4,7 +4,7 @@ import { GlobalThreat } from "../threat/threat.js";
4
4
  import { SquadAction, SquadBehaviour, disband } from "./squadBehaviour.js";
5
5
  import { MatchAwareness } from "../awareness.js";
6
6
  import { getDistanceBetweenPoints } from "../map/map.js";
7
- import _ from "lodash";
7
+ import { DebugLogger } from "../common/utils.js";
8
8
 
9
9
  export enum SquadLiveness {
10
10
  SquadDead,
@@ -41,7 +41,7 @@ const calculateCenterOfMass: (unitTiles: Tile[]) => {
41
41
 
42
42
  // max distance of units to the center of mass
43
43
  const distances = unitTiles.map((tile) => getDistanceBetweenPoints({ x: tile.rx, y: tile.ry }, centerOfMass));
44
- const maxDistance = _.max(distances)!;
44
+ const maxDistance = Math.max(...distances);
45
45
  return { centerOfMass, maxDistance };
46
46
  };
47
47
 
@@ -76,6 +76,7 @@ export class Squad {
76
76
  actionsApi: ActionsApi,
77
77
  playerData: PlayerData,
78
78
  matchAwareness: MatchAwareness,
79
+ logger: DebugLogger,
79
80
  ): SquadAction {
80
81
  this.updateLiveness(gameApi);
81
82
  const movableUnitTiles = this.unitIds
@@ -100,8 +101,7 @@ export class Squad {
100
101
  } else if (!this.mission) {
101
102
  return disband();
102
103
  }
103
- let outcome = this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness);
104
- return outcome;
104
+ return this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness, logger);
105
105
  }
106
106
  public getMission(): Mission | null {
107
107
  return this.mission;
@@ -2,6 +2,7 @@ import { ActionsApi, GameApi, PlayerData, Point2D } from "@chronodivide/game-api
2
2
  import { GlobalThreat } from "../threat/threat.js";
3
3
  import { Squad } from "./squad.js";
4
4
  import { MatchAwareness } from "../awareness.js";
5
+ import { DebugLogger } from "../common/utils.js";
5
6
 
6
7
  export type SquadActionNoop = {
7
8
  type: "noop";
@@ -56,6 +57,7 @@ export interface SquadBehaviour {
56
57
  actionsApi: ActionsApi,
57
58
  playerData: PlayerData,
58
59
  squad: Squad,
59
- matchAwareness: MatchAwareness
60
+ matchAwareness: MatchAwareness,
61
+ logger: DebugLogger
60
62
  ): SquadAction;
61
63
  }
@@ -1,7 +1,6 @@
1
1
  // Meta-controller for forming and controlling squads.
2
2
 
3
3
  import { ActionsApi, GameApi, PlayerData, UnitData } from "@chronodivide/game-api";
4
- import { GlobalThreat } from "../threat/threat.js";
5
4
  import { Squad, SquadLiveness } from "./squad.js";
6
5
  import {
7
6
  SquadAction,
@@ -13,7 +12,7 @@ import {
13
12
  } from "./squadBehaviour.js";
14
13
  import { MatchAwareness } from "../awareness.js";
15
14
  import { getDistanceBetween } from "../map/map.js";
16
- import _ from "lodash";
15
+ import countBy from "lodash.countby";
17
16
 
18
17
  type SquadWithAction<T> = {
19
18
  squad: Squad;
@@ -51,7 +50,7 @@ export class SquadController {
51
50
  const squadActions: SquadWithAction<SquadAction>[] = this.squads.map((squad) => {
52
51
  return {
53
52
  squad,
54
- action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
53
+ action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, this.logger),
55
54
  };
56
55
  });
57
56
  // Handle disbands and merges.
@@ -105,21 +104,43 @@ export class SquadController {
105
104
  },
106
105
  {} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
107
106
  );
108
- Object.entries(unitIdToHighestRequest).forEach(([id, request]) => {
109
- const unitId = Number.parseInt(id);
110
- const unit = gameApi.getUnitData(unitId);
111
- const { squad: requestingSquad } = request;
112
- const missionName = requestingSquad.getMission()?.getUniqueName();
113
- if (!unit) {
114
- this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
115
- return;
116
- }
117
- if (!this.unitIdToSquad.has(unitId)) {
118
- this.logger(
119
- `granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`,
120
- );
121
- this.addUnitToSquad(requestingSquad, unit);
122
- }
107
+
108
+ // Map of Squad ID to Unit Type to Count.
109
+ const newSquadAssignments = Object.entries(unitIdToHighestRequest)
110
+ .flatMap(([id, request]) => {
111
+ const unitId = Number.parseInt(id);
112
+ const unit = gameApi.getUnitData(unitId);
113
+ const { squad: requestingSquad } = request;
114
+ const missionName = requestingSquad.getMission()?.getUniqueName();
115
+ if (!unit) {
116
+ this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
117
+ return [];
118
+ }
119
+ if (!this.unitIdToSquad.has(unitId)) {
120
+ this.addUnitToSquad(requestingSquad, unit);
121
+ return [{ unitName: unit?.name, squad: requestingSquad.getName() }];
122
+ }
123
+ return [];
124
+ })
125
+ .reduce(
126
+ (acc, curr) => {
127
+ if (!acc[curr.squad]) {
128
+ acc[curr.squad] = {};
129
+ }
130
+ if (!acc[curr.squad][curr.unitName]) {
131
+ acc[curr.squad][curr.unitName] = 0;
132
+ }
133
+ acc[curr.squad][curr.unitName] = acc[curr.squad][curr.unitName] + 1;
134
+ return acc;
135
+ },
136
+ {} as Record<string, Record<string, number>>,
137
+ );
138
+ Object.entries(newSquadAssignments).forEach(([squad, assignments]) => {
139
+ this.logger(
140
+ `Squad ${squad} received: ${Object.entries(assignments)
141
+ .map(([unitType, count]) => unitType + " x " + count)
142
+ .join(", ")}`,
143
+ );
123
144
  });
124
145
 
125
146
  // Request units by type
@@ -157,33 +178,57 @@ export class SquadController {
157
178
  .filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
158
179
  .map((unit) => unit!);
159
180
 
160
- freeUnits.forEach((freeUnit) => {
161
- if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
162
- const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
163
- this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
164
- this.addUnitToSquad(requestingSquad, freeUnit);
165
- delete unitTypeToHighestRequest[freeUnit.name];
166
- } else if (grabRequests.length > 0) {
167
- grabRequests.some((request) => {
168
- const { squad: requestingSquad } = request;
169
- if (
170
- freeUnit.rules.isSelectableCombatant &&
171
- getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
172
- ) {
173
- this.logger(
174
- `granting unit ${freeUnit.id}#${
175
- freeUnit.name
176
- } to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${
177
- request.action.point.y
178
- }`,
181
+ type AssignmentWithType = { unitName: string; squad: string; method: "type" | "grab" };
182
+ // [squadName][unitName]['type' | 'grab']
183
+ const newAssignmentsByType = freeUnits
184
+ .flatMap((freeUnit) => {
185
+ if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
186
+ const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
187
+ this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
188
+ this.addUnitToSquad(requestingSquad, freeUnit);
189
+ delete unitTypeToHighestRequest[freeUnit.name];
190
+ return [
191
+ { unitName: freeUnit.name, squad: requestingSquad.getName(), method: "type" },
192
+ ] as AssignmentWithType[];
193
+ } else if (grabRequests.length > 0) {
194
+ const grantedSquad = grabRequests.find((request) => {
195
+ return (
196
+ freeUnit.rules.isSelectableCombatant &&
197
+ getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
179
198
  );
180
- this.addUnitToSquad(requestingSquad, freeUnit);
181
- return true;
182
- } else {
183
- return false;
199
+ });
200
+ if (grantedSquad) {
201
+ this.addUnitToSquad(grantedSquad.squad, freeUnit);
202
+ return [
203
+ { unitName: freeUnit.name, squad: grantedSquad.squad.getName(), method: "grab" },
204
+ ] as AssignmentWithType[];
184
205
  }
185
- });
186
- }
206
+ }
207
+ return [];
208
+ })
209
+ .reduce(
210
+ (acc, curr) => {
211
+ if (!acc[curr.squad]) {
212
+ acc[curr.squad] = {};
213
+ }
214
+ if (!acc[curr.squad][curr.unitName]) {
215
+ acc[curr.squad][curr.unitName] = { grab: 0, type: 0 };
216
+ }
217
+ acc[curr.squad][curr.unitName][curr.method] = acc[curr.squad][curr.unitName][curr.method] + 1;
218
+ return acc;
219
+ },
220
+ {} as Record<string, Record<string, Record<"type" | "grab", number>>>,
221
+ );
222
+ Object.entries(newAssignmentsByType).forEach(([squad, assignments]) => {
223
+ this.logger(
224
+ `Squad ${squad} received: ${Object.entries(assignments)
225
+ .flatMap(([unitType, methodToCount]) =>
226
+ Object.entries(methodToCount)
227
+ .filter(([, count]) => count > 0)
228
+ .map(([method, count]) => unitType + " x " + count + " (by " + method + ")"),
229
+ )
230
+ .join(", ")}`,
231
+ );
187
232
  });
188
233
  }
189
234
 
@@ -197,7 +242,7 @@ export class SquadController {
197
242
  }
198
243
 
199
244
  public debugSquads(gameApi: GameApi) {
200
- const unitsInSquad = (unitIds: number[]) => _.countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
245
+ const unitsInSquad = (unitIds: number[]) => countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
201
246
 
202
247
  this.squads.forEach((squad) => {
203
248
  this.logger(
package/src/exampleBot.ts CHANGED
@@ -6,17 +6,17 @@ async function main() {
6
6
  Ladder maps:
7
7
  CDR2 1v1 2_malibu_cliffs_le.map
8
8
  CDR2 1v1 4_country_swing_le_v2.map
9
- CDR2 1v1 mp01t4.map
10
- CDR2 1v1 tn04t2.map
11
- CDR2 1v1 mp10s4.map
9
+ CDR2 1v1 mp01t4.map, large map, oil derricks
10
+ CDR2 1v1 tn04t2.map, small map
11
+ CDR2 1v1 mp10s4.map <- depth charge, naval map (not supported)
12
12
  CDR2 1v1 heckcorners.map
13
13
  CDR2 1v1 4_montana_dmz_le.map
14
14
  CDR2 1v1 barrel.map
15
15
 
16
16
  Other maps:
17
- mp03t4
17
+ mp03t4 large map, no oil derricks
18
18
  */
19
- const mapName = "mp01t4.map";
19
+ const mapName = "tn04t2.map";
20
20
  // Bot names must be unique in online mode
21
21
  const botName = `Joe${String(Date.now()).substr(-6)}`;
22
22
  const otherBotName = `Bob${String(Date.now() + 1).substr(-6)}`;
@@ -48,7 +48,7 @@ async function main() {
48
48
  };
49
49
 
50
50
  const offlineSettings = {
51
- agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "French", true)],
51
+ agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "Russians", true)],
52
52
  };
53
53
 
54
54
  const game = await cdapi.createGame({