@supalosa/chronodivide-bot 0.2.0 → 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 (118) hide show
  1. package/README.md +11 -3
  2. package/dist/bot/bot.js +17 -9
  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/building.js +2 -0
  17. package/dist/bot/logic/building/buildingRules.js +168 -0
  18. package/dist/bot/logic/building/buildingRules.js.map +1 -0
  19. package/dist/bot/logic/building/harvester.js +1 -0
  20. package/dist/bot/logic/building/harvester.js.map +1 -0
  21. package/dist/bot/logic/building/powerPlant.js +2 -1
  22. package/dist/bot/logic/building/powerPlant.js.map +1 -0
  23. package/dist/bot/logic/building/queueController.js +2 -1
  24. package/dist/bot/logic/building/queueController.js.map +1 -0
  25. package/dist/bot/logic/building/resourceCollectionBuilding.js +2 -1
  26. package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -0
  27. package/dist/bot/logic/common/scout.js +100 -0
  28. package/dist/bot/logic/common/scout.js.map +1 -0
  29. package/dist/bot/logic/common/utils.js +14 -0
  30. package/dist/bot/logic/common/utils.js.map +1 -0
  31. package/dist/bot/logic/map/map.js +9 -25
  32. package/dist/bot/logic/map/map.js.map +1 -0
  33. package/dist/bot/logic/map/sector.js +33 -1
  34. package/dist/bot/logic/map/sector.js.map +1 -0
  35. package/dist/bot/logic/mission/mission.js +3 -1
  36. package/dist/bot/logic/mission/mission.js.map +1 -0
  37. package/dist/bot/logic/mission/missionController.js +5 -4
  38. package/dist/bot/logic/mission/missionController.js.map +1 -0
  39. package/dist/bot/logic/mission/missionFactories.js +3 -0
  40. package/dist/bot/logic/mission/missionFactories.js.map +1 -0
  41. package/dist/bot/logic/mission/missions/attackMission.js +9 -10
  42. package/dist/bot/logic/mission/missions/attackMission.js.map +1 -0
  43. package/dist/bot/logic/mission/missions/defenceMission.js +12 -7
  44. package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -0
  45. package/dist/bot/logic/mission/missions/engineerMission.js +34 -0
  46. package/dist/bot/logic/mission/missions/engineerMission.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/combatSquad.js +20 -18
  56. package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -0
  57. package/dist/bot/logic/squad/behaviours/common.js +25 -5
  58. package/dist/bot/logic/squad/behaviours/common.js.map +1 -0
  59. package/dist/bot/logic/squad/behaviours/engineerSquad.js +36 -0
  60. package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +1 -0
  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 +4 -5
  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 +29 -12
  80. package/dist/exampleBot.js.map +1 -0
  81. package/package.json +15 -7
  82. package/src/bot/bot.ts +21 -16
  83. package/src/bot/logic/awareness.ts +22 -5
  84. package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -60
  85. package/src/bot/logic/building/artilleryUnit.ts +68 -0
  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/basicGroundUnit.ts +1 -1
  89. package/src/bot/logic/building/buildingRules.ts +233 -0
  90. package/src/bot/logic/building/powerPlant.ts +32 -32
  91. package/src/bot/logic/building/queueController.ts +1 -1
  92. package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -56
  93. package/src/bot/logic/common/scout.ts +127 -1
  94. package/src/bot/logic/common/utils.ts +17 -0
  95. package/src/bot/logic/map/map.ts +70 -84
  96. package/src/bot/logic/map/sector.ts +46 -4
  97. package/src/bot/logic/mission/mission.ts +2 -2
  98. package/src/bot/logic/mission/missionController.ts +7 -6
  99. package/src/bot/logic/mission/missionFactories.ts +5 -0
  100. package/src/bot/logic/mission/missions/attackMission.ts +19 -12
  101. package/src/bot/logic/mission/missions/defenceMission.ts +35 -15
  102. package/src/bot/logic/mission/missions/engineerMission.ts +61 -0
  103. package/src/bot/logic/mission/missions/expansionMission.ts +6 -4
  104. package/src/bot/logic/mission/missions/oneTimeMission.ts +3 -2
  105. package/src/bot/logic/mission/missions/retreatMission.ts +3 -2
  106. package/src/bot/logic/mission/missions/scoutingMission.ts +9 -6
  107. package/src/bot/logic/squad/behaviours/combatSquad.ts +22 -18
  108. package/src/bot/logic/squad/behaviours/common.ts +38 -5
  109. package/src/bot/logic/squad/behaviours/engineerSquad.ts +53 -0
  110. package/src/bot/logic/squad/behaviours/retreatSquad.ts +10 -12
  111. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +79 -26
  112. package/src/bot/logic/squad/squad.ts +4 -4
  113. package/src/bot/logic/squad/squadBehaviour.ts +3 -1
  114. package/src/bot/logic/squad/squadController.ts +135 -70
  115. package/src/exampleBot.ts +31 -12
  116. package/tsconfig.json +73 -73
  117. package/src/bot/logic/building/ArtilleryUnit.ts +0 -43
  118. package/src/bot/logic/building/building.ts +0 -125
@@ -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,6 +12,7 @@ import {
13
12
  } from "./squadBehaviour.js";
14
13
  import { MatchAwareness } from "../awareness.js";
15
14
  import { getDistanceBetween } from "../map/map.js";
15
+ import countBy from "lodash.countby";
16
16
 
17
17
  type SquadWithAction<T> = {
18
18
  squad: Squad;
@@ -23,14 +23,13 @@ export class SquadController {
23
23
  private squads: Squad[] = [];
24
24
  private unitIdToSquad: Map<number, Squad> = new Map();
25
25
 
26
- constructor() {}
26
+ constructor(private logger: (message: string, sayInGame?: boolean) => void) {}
27
27
 
28
28
  public onAiUpdate(
29
29
  gameApi: GameApi,
30
30
  actionsApi: ActionsApi,
31
31
  playerData: PlayerData,
32
32
  matchAwareness: MatchAwareness,
33
- logger: (message: string) => void
34
33
  ) {
35
34
  // Remove dead squads or those where the mission is dead.
36
35
  this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
@@ -41,7 +40,7 @@ export class SquadController {
41
40
  this.squads.forEach((squad) => {
42
41
  squad.getUnitIds().forEach((unitId) => {
43
42
  if (this.unitIdToSquad.has(unitId)) {
44
- logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
43
+ this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
45
44
  } else {
46
45
  this.unitIdToSquad.set(unitId, squad);
47
46
  }
@@ -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.
@@ -61,7 +60,7 @@ export class SquadController {
61
60
  squadActions
62
61
  .filter((a) => isDisband(a.action))
63
62
  .forEach((a) => {
64
- logger(`Squad ${a.squad.getName()} disbanding as requested.`);
63
+ this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
65
64
  a.squad.getMission()?.removeSquad();
66
65
  a.squad.getUnitIds().forEach((unitId) => {
67
66
  this.unitIdToSquad.delete(unitId);
@@ -73,8 +72,8 @@ export class SquadController {
73
72
  .forEach((a) => {
74
73
  let mergeInto = a.action as SquadActionMergeInto;
75
74
  if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
76
- logger(
77
- `Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`
75
+ this.logger(
76
+ `Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`,
78
77
  );
79
78
  return;
80
79
  }
@@ -88,58 +87,88 @@ export class SquadController {
88
87
  const isRequestSpecific = (a: SquadAction) => a.type === "requestSpecific";
89
88
  const unitIdToHighestRequest = squadActions
90
89
  .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) {
90
+ .reduce(
91
+ (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) {
97
+ prev[unitId] = squadWithAction;
98
+ }
99
+ } else {
97
100
  prev[unitId] = squadWithAction;
98
101
  }
99
- } else {
100
- prev[unitId] = squadWithAction;
102
+ });
103
+ return prev;
104
+ },
105
+ {} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
106
+ );
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] = {};
101
129
  }
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
- }
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
+ );
118
144
  });
119
145
 
120
146
  // Request units by type
121
147
  const isRequest = (a: SquadAction) => a.type === "request";
122
148
  const unitTypeToHighestRequest = squadActions
123
149
  .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) {
150
+ .reduce(
151
+ (prev, a) => {
152
+ const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
153
+ const { unitNames } = squadWithAction.action;
154
+ unitNames.forEach((unitName) => {
155
+ if (prev.hasOwnProperty(unitName)) {
156
+ if (prev[unitName].action.priority > prev[unitName].action.priority) {
157
+ prev[unitName] = squadWithAction;
158
+ }
159
+ } else {
130
160
  prev[unitName] = squadWithAction;
131
161
  }
132
- } else {
133
- prev[unitName] = squadWithAction;
134
- }
135
- });
136
- return prev;
137
- }, {} as Record<string, SquadWithAction<SquadActionRequestUnits>>);
162
+ });
163
+ return prev;
164
+ },
165
+ {} as Record<string, SquadWithAction<SquadActionRequestUnits>>,
166
+ );
138
167
 
139
168
  // Request combat-capable units in an area
140
169
  const isGrab = (a: SquadAction) => a.type === "requestCombatants";
141
170
  const grabRequests = squadActions.filter((a) =>
142
- isGrab(a.action)
171
+ isGrab(a.action),
143
172
  ) as SquadWithAction<SquadActionGrabFreeCombatants>[];
144
173
 
145
174
  // Find loose units
@@ -149,33 +178,57 @@ export class SquadController {
149
178
  .filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
150
179
  .map((unit) => unit!);
151
180
 
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
- }`
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
171
198
  );
172
- this.addUnitToSquad(requestingSquad, freeUnit);
173
- return true;
174
- } else {
175
- 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[];
176
205
  }
177
- });
178
- }
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
+ );
179
232
  });
180
233
  }
181
234
 
@@ -187,4 +240,16 @@ export class SquadController {
187
240
  public registerSquad(squad: Squad) {
188
241
  this.squads.push(squad);
189
242
  }
243
+
244
+ public debugSquads(gameApi: GameApi) {
245
+ const unitsInSquad = (unitIds: number[]) => countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
246
+
247
+ this.squads.forEach((squad) => {
248
+ this.logger(
249
+ `Squad ${squad.getName()}: ${Object.entries(unitsInSquad(squad.getUnitIds()))
250
+ .map(([unitName, count]) => `${unitName} x ${count}`)
251
+ .join(", ")}`,
252
+ );
253
+ });
254
+ }
190
255
  }
package/src/exampleBot.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { cdapi } from "@chronodivide/game-api";
1
+ import { Agent, Bot, cdapi } from "@chronodivide/game-api";
2
2
  import { SupalosaBot } from "./bot/bot.js";
3
3
 
4
4
  async function main() {
@@ -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)}`;
@@ -26,14 +26,33 @@ async function main() {
26
26
  console.log("Server URL: " + process.env.SERVER_URL!);
27
27
  console.log("Client URL: " + process.env.CLIENT_URL!);
28
28
 
29
- const game = await cdapi.createGame({
30
- // Uncomment the following lines to play in real time versus the bot
31
- /*online: true,
29
+ /*
30
+ Countries:
31
+ 0=Americans
32
+ 1=Alliance -> Korea
33
+ 2=French
34
+ 3=Germans
35
+ 4=British
36
+
37
+ 5=Africans -> Libya
38
+ 6=Arabs -> Iraq
39
+ 7=Confederation -> Cuba
40
+ 8=Russians
41
+ */
42
+
43
+ const onlineSettings = {
44
+ online: true as true,
32
45
  serverUrl: process.env.SERVER_URL!,
33
46
  clientUrl: process.env.CLIENT_URL!,
34
- agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }],*/
35
- agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "French", true)],
36
- //agents: [new SupalosaBot(botName, "Americans", false), new SupalosaBot(otherBotName, "Russians", false)],
47
+ agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }] as [Bot, ...Agent[]],
48
+ };
49
+
50
+ const offlineSettings = {
51
+ agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "Russians", true)],
52
+ };
53
+
54
+ const game = await cdapi.createGame({
55
+ ...offlineSettings,
37
56
  buildOffAlly: false,
38
57
  cratesAppear: false,
39
58
  credits: 10000,