@supalosa/chronodivide-bot 0.2.0 → 0.2.2

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 (121) hide show
  1. package/.prettierrc +5 -5
  2. package/README.md +11 -3
  3. package/dist/bot/bot.js +15 -7
  4. package/dist/bot/bot.js.map +1 -0
  5. package/dist/bot/logic/awareness.js +12 -2
  6. package/dist/bot/logic/awareness.js.map +1 -0
  7. package/dist/bot/logic/awarenessImpl.js +132 -0
  8. package/dist/bot/logic/awarenessImpl.js.map +1 -0
  9. package/dist/bot/logic/building/ArtilleryUnit.js +1 -0
  10. package/dist/bot/logic/building/ArtilleryUnit.js.map +1 -0
  11. package/dist/bot/logic/building/antiGroundStaticDefence.js +1 -0
  12. package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -0
  13. package/dist/bot/logic/building/basicAirUnit.js +1 -0
  14. package/dist/bot/logic/building/basicAirUnit.js.map +1 -0
  15. package/dist/bot/logic/building/basicBuilding.js +1 -0
  16. package/dist/bot/logic/building/basicBuilding.js.map +1 -0
  17. package/dist/bot/logic/building/basicGroundUnit.js +1 -0
  18. package/dist/bot/logic/building/basicGroundUnit.js.map +1 -0
  19. package/dist/bot/logic/building/building.js +57 -11
  20. package/dist/bot/logic/building/building.js.map +1 -0
  21. package/dist/bot/logic/building/harvester.js +1 -0
  22. package/dist/bot/logic/building/harvester.js.map +1 -0
  23. package/dist/bot/logic/building/powerPlant.js +1 -0
  24. package/dist/bot/logic/building/powerPlant.js.map +1 -0
  25. package/dist/bot/logic/building/queueController.js +1 -0
  26. package/dist/bot/logic/building/queueController.js.map +1 -0
  27. package/dist/bot/logic/building/resourceCollectionBuilding.js +1 -0
  28. package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -0
  29. package/dist/bot/logic/common/scout.js +100 -0
  30. package/dist/bot/logic/common/scout.js.map +1 -0
  31. package/dist/bot/logic/common/utils.js +2 -0
  32. package/dist/bot/logic/common/utils.js.map +1 -0
  33. package/dist/bot/logic/map/map.js +9 -25
  34. package/dist/bot/logic/map/map.js.map +1 -0
  35. package/dist/bot/logic/map/sector.js +33 -1
  36. package/dist/bot/logic/map/sector.js.map +1 -0
  37. package/dist/bot/logic/mission/mission.js +3 -1
  38. package/dist/bot/logic/mission/mission.js.map +1 -0
  39. package/dist/bot/logic/mission/missionController.js +5 -4
  40. package/dist/bot/logic/mission/missionController.js.map +1 -0
  41. package/dist/bot/logic/mission/missionFactories.js +1 -0
  42. package/dist/bot/logic/mission/missionFactories.js.map +1 -0
  43. package/dist/bot/logic/mission/missions/attackMission.js +7 -8
  44. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -0
  45. package/dist/bot/logic/mission/missions/defenceMission.js +12 -7
  46. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -0
  47. package/dist/bot/logic/mission/missions/expansionMission.js +5 -4
  48. package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -0
  49. package/dist/bot/logic/mission/missions/oneTimeMission.js +3 -2
  50. package/dist/bot/logic/mission/missions/oneTimeMission.js.map +1 -0
  51. package/dist/bot/logic/mission/missions/retreatMission.js +3 -2
  52. package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -0
  53. package/dist/bot/logic/mission/missions/scoutingMission.js +8 -9
  54. package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -0
  55. package/dist/bot/logic/squad/behaviours/attackSquad.js +63 -56
  56. package/dist/bot/logic/squad/behaviours/combatSquad.js +6 -3
  57. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -0
  58. package/dist/bot/logic/squad/behaviours/common.js +7 -4
  59. package/dist/bot/logic/squad/behaviours/common.js.map +1 -0
  60. package/dist/bot/logic/squad/behaviours/defenceSquad.js +15 -2
  61. package/dist/bot/logic/squad/behaviours/expansionSquad.js +1 -0
  62. package/dist/bot/logic/squad/behaviours/expansionSquad.js.map +1 -0
  63. package/dist/bot/logic/squad/behaviours/retreatSquad.js +6 -10
  64. package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -0
  65. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +66 -18
  66. package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -0
  67. package/dist/bot/logic/squad/squad.js +3 -3
  68. package/dist/bot/logic/squad/squad.js.map +1 -0
  69. package/dist/bot/logic/squad/squadBehaviour.js +1 -0
  70. package/dist/bot/logic/squad/squadBehaviour.js.map +1 -0
  71. package/dist/bot/logic/squad/squadBehaviours.js +1 -0
  72. package/dist/bot/logic/squad/squadBehaviours.js.map +1 -0
  73. package/dist/bot/logic/squad/squadController.js +73 -23
  74. package/dist/bot/logic/squad/squadController.js.map +1 -0
  75. package/dist/bot/logic/threat/threat.js +1 -0
  76. package/dist/bot/logic/threat/threat.js.map +1 -0
  77. package/dist/bot/logic/threat/threatCalculator.js +1 -0
  78. package/dist/bot/logic/threat/threatCalculator.js.map +1 -0
  79. package/dist/exampleBot.js +24 -7
  80. package/dist/exampleBot.js.map +1 -0
  81. package/package.json +9 -6
  82. package/src/bot/bot.ts +17 -11
  83. package/src/bot/logic/awareness.ts +22 -5
  84. package/src/bot/logic/building/ArtilleryUnit.ts +43 -43
  85. package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -60
  86. package/src/bot/logic/building/basicAirUnit.ts +68 -68
  87. package/src/bot/logic/building/basicBuilding.ts +47 -47
  88. package/src/bot/logic/building/building.ts +72 -12
  89. package/src/bot/logic/building/powerPlant.ts +32 -32
  90. package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -56
  91. package/src/bot/logic/common/scout.ts +127 -1
  92. package/src/bot/logic/common/utils.ts +1 -0
  93. package/src/bot/logic/map/map.ts +70 -84
  94. package/src/bot/logic/map/sector.ts +46 -4
  95. package/src/bot/logic/mission/mission.ts +2 -2
  96. package/src/bot/logic/mission/missionController.ts +7 -6
  97. package/src/bot/logic/mission/missionFactories.ts +3 -0
  98. package/src/bot/logic/mission/missions/attackMission.ts +19 -13
  99. package/src/bot/logic/mission/missions/defenceMission.ts +35 -15
  100. package/src/bot/logic/mission/missions/expansionMission.ts +6 -4
  101. package/src/bot/logic/mission/missions/oneTimeMission.ts +3 -2
  102. package/src/bot/logic/mission/missions/retreatMission.ts +3 -2
  103. package/src/bot/logic/mission/missions/scoutingMission.ts +9 -6
  104. package/src/bot/logic/squad/behaviours/combatSquad.ts +6 -2
  105. package/src/bot/logic/squad/behaviours/common.ts +6 -4
  106. package/src/bot/logic/squad/behaviours/retreatSquad.ts +10 -12
  107. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +81 -23
  108. package/src/bot/logic/squad/squad.ts +3 -2
  109. package/src/bot/logic/squad/squadBehaviour.ts +3 -1
  110. package/src/bot/logic/squad/squadController.ts +136 -69
  111. package/src/bot/logic/threat/threat.ts +15 -15
  112. package/src/bot/logic/threat/threatCalculator.ts +99 -99
  113. package/src/exampleBot.ts +25 -6
  114. package/tsconfig.json +73 -73
  115. package/dist/bot/logic/building/massedAntiGroundUnit.js +0 -20
  116. package/dist/bot/logic/building/queues.js +0 -19
  117. package/dist/bot/logic/knowledge.js +0 -1
  118. package/dist/bot/logic/mission/basicMission.js +0 -26
  119. package/dist/bot/logic/mission/expansionMission.js +0 -32
  120. package/dist/bot/logic/squad/behaviours/squadExpansion.js +0 -31
  121. package/dist/bot/logic/squad/behaviours/squadScouters.js +0 -8
@@ -6,6 +6,7 @@ import { Squad } from "../../squad/squad.js";
6
6
  import { MissionFactory } from "../missionFactories.js";
7
7
  import { SquadBehaviour } from "../../squad/squadBehaviour.js";
8
8
  import { MatchAwareness } from "../../awareness.js";
9
+ import { DebugLogger } from "../../common/utils.js";
9
10
 
10
11
  /**
11
12
  * A mission that gets dispatched once, and once the squad decides to disband, the mission is disbanded.
@@ -13,8 +14,8 @@ import { MatchAwareness } from "../../awareness.js";
13
14
  export abstract class OneTimeMission<T = undefined> extends Mission<T> {
14
15
  private hadSquad = false;
15
16
 
16
- constructor(uniqueName: string, priority: number, private behaviourFactory: () => SquadBehaviour) {
17
- super(uniqueName, priority);
17
+ constructor(uniqueName: string, priority: number, private behaviourFactory: () => SquadBehaviour, logger: DebugLogger) {
18
+ super(uniqueName, priority, logger);
18
19
  }
19
20
 
20
21
  onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
@@ -1,9 +1,10 @@
1
1
  import { Point2D } from "@chronodivide/game-api";
2
2
  import { OneTimeMission } from "./oneTimeMission.js";
3
3
  import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
4
+ import { DebugLogger } from "../../common/utils.js";
4
5
 
5
6
  export class RetreatMission extends OneTimeMission {
6
- constructor(uniqueName: string, priority: number, retreatToPoint: Point2D, unitIds: number[]) {
7
- super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint));
7
+ constructor(uniqueName: string, priority: number, retreatToPoint: Point2D, unitIds: number[], logger: DebugLogger) {
8
+ super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint), logger);
8
9
  }
9
10
  }
@@ -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
  }
@@ -5,6 +5,7 @@ import { SquadAction, SquadBehaviour, grabCombatants, noop } from "../squadBehav
5
5
  import { MatchAwareness } from "../../awareness.js";
6
6
  import { getDistanceBetweenPoints } from "../../map/map.js";
7
7
  import { 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,9 @@ 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 (squad.getUnitIds().length > 0 && (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)) {
57
59
  this.lastCommand = gameApi.getCurrentTick();
58
60
  const centerOfMass = squad.getCenterOfMass();
59
61
  const maxDistance = squad.getMaxDistanceToCenterOfMass();
@@ -81,6 +83,7 @@ export class CombatSquad implements SquadBehaviour {
81
83
  manageMoveMicro(actionsApi, unit, centerOfMass);
82
84
  });
83
85
  } else {
86
+ logger(`CombatSquad ${squad.getName()} switching back to attack mode (${maxDistance})`)
84
87
  this.state = SquadState.Attacking;
85
88
  }
86
89
  } else {
@@ -92,7 +95,8 @@ export class CombatSquad implements SquadBehaviour {
92
95
  gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
93
96
  maxDistance > requiredGatherRadius
94
97
  ) {
95
- // Switch back to gather mode.
98
+ // Switch back to gather mode
99
+ logger(`CombatSquad ${squad.getName()} switching back to gather (${maxDistance})`)
96
100
  this.state = SquadState.Gathering;
97
101
  return noop();
98
102
  }
@@ -1,10 +1,11 @@
1
- import { ActionsApi, ObjectType, OrderType, Point2D, UnitData } from "@chronodivide/game-api";
1
+ import { ActionsApi, AttackState, ObjectType, OrderType, Point2D, StanceType, UnitData } from "@chronodivide/game-api";
2
2
  import { getDistanceBetweenUnits } from "../../map/map.js";
3
3
 
4
4
  // Micro methods
5
5
  export function manageMoveMicro(actionsApi: ActionsApi, attacker: UnitData, attackPoint: Point2D) {
6
6
  if (attacker.name === "E1") {
7
- if (!attacker.canMove) {
7
+ const isDeployed = attacker.stance === StanceType.Deployed;
8
+ if (isDeployed) {
8
9
  actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
9
10
  }
10
11
  }
@@ -16,10 +17,11 @@ export function manageAttackMicro(actionsApi: ActionsApi, attacker: UnitData, ta
16
17
  if (attacker.name === "E1") {
17
18
  // Para (deployed weapon) range is 5.
18
19
  const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
19
- if (attacker.canMove && distance <= deployedWeaponRange * 0.8) {
20
+ const isDeployed = attacker.stance === StanceType.Deployed;
21
+ if (!isDeployed && (distance <= deployedWeaponRange || attacker.attackState === AttackState.JustFired)) {
20
22
  actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
21
23
  return;
22
- } else if (!attacker.canMove && distance > deployedWeaponRange) {
24
+ } else if (isDeployed && distance > deployedWeaponRange) {
23
25
  actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
24
26
  return;
25
27
  }
@@ -7,8 +7,6 @@ import { MatchAwareness } from "../../awareness.js";
7
7
  const SCOUT_MOVE_COOLDOWN_TICKS = 30;
8
8
 
9
9
  export class RetreatSquad implements SquadBehaviour {
10
- private hasRequestedUnits: boolean = false;
11
- private moveOrderSentAt: number | null = null;
12
10
  private createdAt: number | null = null;
13
11
 
14
12
  constructor(
@@ -28,19 +26,19 @@ export class RetreatSquad implements SquadBehaviour {
28
26
  }
29
27
  if (squad.getUnitIds().length > 0) {
30
28
  // Only send the order once we have managed to claim some units.
31
- console.log(`Retreat squad ordered ${squad.getUnitIds()} to retreat`);
32
- actionsApi.orderUnits(squad.getUnitIds(), OrderType.Move, this.retreatToPoint.x, this.retreatToPoint.y);
33
- if (!this.moveOrderSentAt) {
34
- this.moveOrderSentAt = gameApi.getCurrentTick();
35
- }
29
+ actionsApi.orderUnits(
30
+ squad.getUnitIds(),
31
+ OrderType.AttackMove,
32
+ this.retreatToPoint.x,
33
+ this.retreatToPoint.y,
34
+ );
35
+ return disband();
36
36
  }
37
- if (
38
- (this.moveOrderSentAt && gameApi.getCurrentTick() > this.moveOrderSentAt + 60) ||
39
- (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240)
40
- ) {
37
+ if (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240) {
38
+ // Disband automatically after 240 ticks in case we couldn't actually claim any units.
41
39
  return disband();
42
40
  } else {
43
- return requestSpecificUnits(this.unitIds, 100);
41
+ return requestSpecificUnits(this.unitIds, 1000);
44
42
  }
45
43
  }
46
44
  }
@@ -4,14 +4,30 @@ import { Squad } from "../squad.js";
4
4
  import { SquadAction, SquadBehaviour, disband, noop, requestUnits } from "../squadBehaviour.js";
5
5
  import { MatchAwareness } from "../../awareness.js";
6
6
  import { getUnseenStartingLocations } from "../../common/scout.js";
7
+ import { match } from "assert";
8
+ import { DebugLogger } from "../../common/utils.js";
9
+ import _ from "lodash";
10
+ import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
7
11
 
8
12
  const SCOUT_MOVE_COOLDOWN_TICKS = 30;
9
13
 
14
+ // Max units to spend on a particular scout target.
15
+ const MAX_ATTEMPTS_PER_TARGET = 5;
16
+
17
+ // Maximum ticks to spend trying to scout a target *without making progress towards it*.
18
+ // Every time a unit gets closer to the target, the timer refreshes.
19
+ const MAX_TICKS_PER_TARGET = 600;
20
+
10
21
  export class ScoutingSquad implements SquadBehaviour {
11
- private scoutingWith: {
12
- unitId: number;
13
- gameTick: number;
14
- } | null = null;
22
+ private scoutTarget: Point2D | null = null;
23
+ private attemptsOnCurrentTarget: number = 0;
24
+ private scoutTargetRefreshedAt: number = 0;
25
+ private lastMoveCommandTick: number = 0;
26
+
27
+ // Minimum distance from a scout to the target.
28
+ private scoutMinDistance?: number;
29
+
30
+ private hadUnit: boolean = false;
15
31
 
16
32
  public onAiUpdate(
17
33
  gameApi: GameApi,
@@ -19,6 +35,7 @@ export class ScoutingSquad implements SquadBehaviour {
19
35
  playerData: PlayerData,
20
36
  squad: Squad,
21
37
  matchAwareness: MatchAwareness,
38
+ logger: DebugLogger,
22
39
  ): SquadAction {
23
40
  const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
24
41
  const scouts = squad.getUnitsOfTypes(gameApi, ...scoutNames);
@@ -28,29 +45,70 @@ export class ScoutingSquad implements SquadBehaviour {
28
45
  }
29
46
 
30
47
  if (scouts.length === 0) {
31
- this.scoutingWith = null;
48
+ // Count the number of times the scout dies trying to uncover the current scoutTarget.
49
+ if (this.scoutTarget && this.hadUnit) {
50
+ this.attemptsOnCurrentTarget++;
51
+ this.hadUnit = false;
52
+ }
32
53
  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);
54
+ } else if (this.scoutTarget) {
55
+ this.hadUnit = true;
56
+ if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {
57
+ logger(
58
+ `Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,
59
+ );
60
+ this.setScoutTarget(null, 0);
61
+ return noop();
62
+ }
63
+ if (gameApi.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {
64
+ logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`);
65
+ this.setScoutTarget(null, 0);
66
+ return noop();
67
+ }
68
+ const targetTile = gameApi.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);
69
+ if (!targetTile) {
70
+ throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);
71
+ }
72
+ if (gameApi.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {
73
+ this.lastMoveCommandTick = gameApi.getCurrentTick();
74
+ scouts.forEach((unit) => {
75
+ if (this.scoutTarget) {
76
+ actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);
44
77
  }
78
+ });
79
+ // Check that a scout is actually moving closer to the target.
80
+ const newMinDistance = _.min(
81
+ scouts.map((unit) =>
82
+ getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, this.scoutTarget!),
83
+ ),
84
+ )!;
85
+ if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {
86
+ logger(
87
+ `Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,
88
+ );
89
+ this.scoutTargetRefreshedAt = gameApi.getCurrentTick();
90
+ this.scoutMinDistance = newMinDistance;
45
91
  }
46
- });
47
-
48
- // Add a cooldown to scout attempts.
49
- this.scoutingWith = {
50
- unitId: scouts[0].id,
51
- gameTick: gameApi.getCurrentTick(),
52
- };
92
+ }
93
+ if (gameApi.mapApi.isVisibleTile(targetTile, playerData.name)) {
94
+ logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`);
95
+ this.setScoutTarget(null, gameApi.getCurrentTick());
96
+ }
97
+ } else {
98
+ const candidatePoint = matchAwareness.getScoutingManager().getNewScoutTarget()?.asPoint2D();
99
+ if (!candidatePoint) {
100
+ logger(`No more scouting targets available, disbanding.`);
101
+ return disband();
102
+ }
103
+ this.setScoutTarget(candidatePoint, gameApi.getCurrentTick());
53
104
  }
54
105
  return noop();
55
106
  }
107
+
108
+ setScoutTarget(point: Point2D | null, currentTick: number) {
109
+ this.attemptsOnCurrentTarget = 0;
110
+ this.scoutTargetRefreshedAt = currentTick;
111
+ this.scoutTarget = point;
112
+ this.scoutMinDistance = undefined;
113
+ }
56
114
  }
@@ -5,6 +5,7 @@ import { SquadAction, SquadBehaviour, disband } from "./squadBehaviour.js";
5
5
  import { MatchAwareness } from "../awareness.js";
6
6
  import { getDistanceBetweenPoints } from "../map/map.js";
7
7
  import _ from "lodash";
8
+ import { DebugLogger } from "../common/utils.js";
8
9
 
9
10
  export enum SquadLiveness {
10
11
  SquadDead,
@@ -76,6 +77,7 @@ export class Squad {
76
77
  actionsApi: ActionsApi,
77
78
  playerData: PlayerData,
78
79
  matchAwareness: MatchAwareness,
80
+ logger: DebugLogger
79
81
  ): SquadAction {
80
82
  this.updateLiveness(gameApi);
81
83
  const movableUnitTiles = this.unitIds
@@ -100,8 +102,7 @@ export class Squad {
100
102
  } else if (!this.mission) {
101
103
  return disband();
102
104
  }
103
- let outcome = this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness);
104
- return outcome;
105
+ return this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness, logger);
105
106
  }
106
107
  public getMission(): Mission | null {
107
108
  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
  }
@@ -13,6 +13,8 @@ import {
13
13
  } from "./squadBehaviour.js";
14
14
  import { MatchAwareness } from "../awareness.js";
15
15
  import { getDistanceBetween } from "../map/map.js";
16
+ import _ from "lodash";
17
+ import { DebugLogger } from "../common/utils.js";
16
18
 
17
19
  type SquadWithAction<T> = {
18
20
  squad: Squad;
@@ -23,14 +25,13 @@ export class SquadController {
23
25
  private squads: Squad[] = [];
24
26
  private unitIdToSquad: Map<number, Squad> = new Map();
25
27
 
26
- constructor() {}
28
+ constructor(private logger: (message: string, sayInGame?: boolean) => void) {}
27
29
 
28
30
  public onAiUpdate(
29
31
  gameApi: GameApi,
30
32
  actionsApi: ActionsApi,
31
33
  playerData: PlayerData,
32
34
  matchAwareness: MatchAwareness,
33
- logger: (message: string) => void
34
35
  ) {
35
36
  // Remove dead squads or those where the mission is dead.
36
37
  this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
@@ -41,7 +42,7 @@ export class SquadController {
41
42
  this.squads.forEach((squad) => {
42
43
  squad.getUnitIds().forEach((unitId) => {
43
44
  if (this.unitIdToSquad.has(unitId)) {
44
- logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
45
+ this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
45
46
  } else {
46
47
  this.unitIdToSquad.set(unitId, squad);
47
48
  }
@@ -51,7 +52,7 @@ export class SquadController {
51
52
  const squadActions: SquadWithAction<SquadAction>[] = this.squads.map((squad) => {
52
53
  return {
53
54
  squad,
54
- action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
55
+ action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, this.logger),
55
56
  };
56
57
  });
57
58
  // Handle disbands and merges.
@@ -61,7 +62,7 @@ export class SquadController {
61
62
  squadActions
62
63
  .filter((a) => isDisband(a.action))
63
64
  .forEach((a) => {
64
- logger(`Squad ${a.squad.getName()} disbanding as requested.`);
65
+ this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
65
66
  a.squad.getMission()?.removeSquad();
66
67
  a.squad.getUnitIds().forEach((unitId) => {
67
68
  this.unitIdToSquad.delete(unitId);
@@ -73,8 +74,8 @@ export class SquadController {
73
74
  .forEach((a) => {
74
75
  let mergeInto = a.action as SquadActionMergeInto;
75
76
  if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
76
- logger(
77
- `Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`
77
+ this.logger(
78
+ `Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`,
78
79
  );
79
80
  return;
80
81
  }
@@ -88,58 +89,88 @@ export class SquadController {
88
89
  const isRequestSpecific = (a: SquadAction) => a.type === "requestSpecific";
89
90
  const unitIdToHighestRequest = squadActions
90
91
  .filter((a) => isRequestSpecific(a.action))
91
- .reduce((prev, a) => {
92
- const squadWithAction = a as SquadWithAction<SquadActionRequestSpecificUnits>;
93
- const { unitIds } = squadWithAction.action;
94
- unitIds.forEach((unitId) => {
95
- if (prev.hasOwnProperty(unitId)) {
96
- if (prev[unitId].action.priority > prev[unitId].action.priority) {
92
+ .reduce(
93
+ (prev, a) => {
94
+ const squadWithAction = a as SquadWithAction<SquadActionRequestSpecificUnits>;
95
+ const { unitIds } = squadWithAction.action;
96
+ unitIds.forEach((unitId) => {
97
+ if (prev.hasOwnProperty(unitId)) {
98
+ if (prev[unitId].action.priority > prev[unitId].action.priority) {
99
+ prev[unitId] = squadWithAction;
100
+ }
101
+ } else {
97
102
  prev[unitId] = squadWithAction;
98
103
  }
99
- } else {
100
- prev[unitId] = squadWithAction;
104
+ });
105
+ return prev;
106
+ },
107
+ {} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
108
+ );
109
+
110
+ // Map of Squad ID to Unit Type to Count.
111
+ const newSquadAssignments = Object.entries(unitIdToHighestRequest)
112
+ .flatMap(([id, request]) => {
113
+ const unitId = Number.parseInt(id);
114
+ const unit = gameApi.getUnitData(unitId);
115
+ const { squad: requestingSquad } = request;
116
+ const missionName = requestingSquad.getMission()?.getUniqueName();
117
+ if (!unit) {
118
+ this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
119
+ return [];
120
+ }
121
+ if (!this.unitIdToSquad.has(unitId)) {
122
+ this.addUnitToSquad(requestingSquad, unit);
123
+ return [{ unitName: unit?.name, squad: requestingSquad.getName() }];
124
+ }
125
+ return [];
126
+ })
127
+ .reduce(
128
+ (acc, curr) => {
129
+ if (!acc[curr.squad]) {
130
+ acc[curr.squad] = {};
101
131
  }
102
- });
103
- return prev;
104
- }, {} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>);
105
- Object.entries(unitIdToHighestRequest).forEach(([id, request]) => {
106
- const unitId = Number.parseInt(id);
107
- const unit = gameApi.getUnitData(unitId);
108
- const { squad: requestingSquad } = request;
109
- const missionName = requestingSquad.getMission()?.getUniqueName();
110
- if (!unit) {
111
- logger(`mission ${missionName} requested non-existent unit ${unitId}`);
112
- return;
113
- }
114
- if (!this.unitIdToSquad.has(unitId)) {
115
- logger(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
116
- this.addUnitToSquad(requestingSquad, unit);
117
- }
132
+ if (!acc[curr.squad][curr.unitName]) {
133
+ acc[curr.squad][curr.unitName] = 0;
134
+ }
135
+ acc[curr.squad][curr.unitName] = acc[curr.squad][curr.unitName] + 1;
136
+ return acc;
137
+ },
138
+ {} as Record<string, Record<string, number>>,
139
+ );
140
+ Object.entries(newSquadAssignments).forEach(([squad, assignments]) => {
141
+ this.logger(
142
+ `Squad ${squad} received: ${Object.entries(assignments)
143
+ .map(([unitType, count]) => unitType + " x " + count)
144
+ .join(", ")}`,
145
+ );
118
146
  });
119
147
 
120
148
  // Request units by type
121
149
  const isRequest = (a: SquadAction) => a.type === "request";
122
150
  const unitTypeToHighestRequest = squadActions
123
151
  .filter((a) => isRequest(a.action))
124
- .reduce((prev, a) => {
125
- const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
126
- const { unitNames } = squadWithAction.action;
127
- unitNames.forEach((unitName) => {
128
- if (prev.hasOwnProperty(unitName)) {
129
- if (prev[unitName].action.priority > prev[unitName].action.priority) {
152
+ .reduce(
153
+ (prev, a) => {
154
+ const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
155
+ const { unitNames } = squadWithAction.action;
156
+ unitNames.forEach((unitName) => {
157
+ if (prev.hasOwnProperty(unitName)) {
158
+ if (prev[unitName].action.priority > prev[unitName].action.priority) {
159
+ prev[unitName] = squadWithAction;
160
+ }
161
+ } else {
130
162
  prev[unitName] = squadWithAction;
131
163
  }
132
- } else {
133
- prev[unitName] = squadWithAction;
134
- }
135
- });
136
- return prev;
137
- }, {} as Record<string, SquadWithAction<SquadActionRequestUnits>>);
164
+ });
165
+ return prev;
166
+ },
167
+ {} as Record<string, SquadWithAction<SquadActionRequestUnits>>,
168
+ );
138
169
 
139
170
  // Request combat-capable units in an area
140
171
  const isGrab = (a: SquadAction) => a.type === "requestCombatants";
141
172
  const grabRequests = squadActions.filter((a) =>
142
- isGrab(a.action)
173
+ isGrab(a.action),
143
174
  ) as SquadWithAction<SquadActionGrabFreeCombatants>[];
144
175
 
145
176
  // Find loose units
@@ -149,33 +180,57 @@ export class SquadController {
149
180
  .filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
150
181
  .map((unit) => unit!);
151
182
 
152
- freeUnits.forEach((freeUnit) => {
153
- if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
154
- const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
155
- logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
156
- this.addUnitToSquad(requestingSquad, freeUnit);
157
- delete unitTypeToHighestRequest[freeUnit.name];
158
- } else if (grabRequests.length > 0) {
159
- grabRequests.some((request) => {
160
- const { squad: requestingSquad } = request;
161
- if (
162
- freeUnit.rules.isSelectableCombatant &&
163
- getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
164
- ) {
165
- logger(
166
- `granting unit ${freeUnit.id}#${
167
- freeUnit.name
168
- } to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${
169
- request.action.point.y
170
- }`
183
+ type AssignmentWithType = { unitName: string; squad: string; method: "type" | "grab" };
184
+ // [squadName][unitName]['type' | 'grab']
185
+ const newAssignmentsByType = freeUnits
186
+ .flatMap((freeUnit) => {
187
+ if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
188
+ const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
189
+ this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
190
+ this.addUnitToSquad(requestingSquad, freeUnit);
191
+ delete unitTypeToHighestRequest[freeUnit.name];
192
+ return [
193
+ { unitName: freeUnit.name, squad: requestingSquad.getName(), method: "type" },
194
+ ] as AssignmentWithType[];
195
+ } else if (grabRequests.length > 0) {
196
+ const grantedSquad = grabRequests.find((request) => {
197
+ return (
198
+ freeUnit.rules.isSelectableCombatant &&
199
+ getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
171
200
  );
172
- this.addUnitToSquad(requestingSquad, freeUnit);
173
- return true;
174
- } else {
175
- return false;
201
+ });
202
+ if (grantedSquad) {
203
+ this.addUnitToSquad(grantedSquad.squad, freeUnit);
204
+ return [
205
+ { unitName: freeUnit.name, squad: grantedSquad.squad.getName(), method: "grab" },
206
+ ] as AssignmentWithType[];
176
207
  }
177
- });
178
- }
208
+ }
209
+ return [];
210
+ })
211
+ .reduce(
212
+ (acc, curr) => {
213
+ if (!acc[curr.squad]) {
214
+ acc[curr.squad] = {};
215
+ }
216
+ if (!acc[curr.squad][curr.unitName]) {
217
+ acc[curr.squad][curr.unitName] = { grab: 0, type: 0 };
218
+ }
219
+ acc[curr.squad][curr.unitName][curr.method] = acc[curr.squad][curr.unitName][curr.method] + 1;
220
+ return acc;
221
+ },
222
+ {} as Record<string, Record<string, Record<"type" | "grab", number>>>,
223
+ );
224
+ Object.entries(newAssignmentsByType).forEach(([squad, assignments]) => {
225
+ this.logger(
226
+ `Squad ${squad} received: ${Object.entries(assignments)
227
+ .flatMap(([unitType, methodToCount]) =>
228
+ Object.entries(methodToCount)
229
+ .filter(([, count]) => count > 0)
230
+ .map(([method, count]) => unitType + " x " + count + " (by " + method + ")"),
231
+ )
232
+ .join(", ")}`,
233
+ );
179
234
  });
180
235
  }
181
236
 
@@ -187,4 +242,16 @@ export class SquadController {
187
242
  public registerSquad(squad: Squad) {
188
243
  this.squads.push(squad);
189
244
  }
245
+
246
+ public debugSquads(gameApi: GameApi) {
247
+ const unitsInSquad = (unitIds: number[]) => _.countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
248
+
249
+ this.squads.forEach((squad) => {
250
+ this.logger(
251
+ `Squad ${squad.getName()}: ${Object.entries(unitsInSquad(squad.getUnitIds()))
252
+ .map(([unitName, count]) => `${unitName} x ${count}`)
253
+ .join(", ")}`,
254
+ );
255
+ });
256
+ }
190
257
  }