@supalosa/chronodivide-bot 0.1.1 → 0.2.0

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 (74) hide show
  1. package/README.md +71 -46
  2. package/dist/bot/bot.js +27 -183
  3. package/dist/bot/logic/awareness.js +122 -0
  4. package/dist/bot/logic/building/basicGroundUnit.js +8 -6
  5. package/dist/bot/logic/building/building.js +6 -3
  6. package/dist/bot/logic/building/harvester.js +1 -1
  7. package/dist/bot/logic/building/queueController.js +4 -21
  8. package/dist/bot/logic/common/scout.js +10 -0
  9. package/dist/bot/logic/knowledge.js +1 -0
  10. package/dist/bot/logic/map/map.js +6 -0
  11. package/dist/bot/logic/map/sector.js +6 -1
  12. package/dist/bot/logic/mission/basicMission.js +1 -5
  13. package/dist/bot/logic/mission/expansionMission.js +22 -4
  14. package/dist/bot/logic/mission/mission.js +49 -2
  15. package/dist/bot/logic/mission/missionController.js +67 -34
  16. package/dist/bot/logic/mission/missionFactories.js +10 -0
  17. package/dist/bot/logic/mission/missions/attackMission.js +109 -0
  18. package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
  19. package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
  20. package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
  21. package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
  22. package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
  23. package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
  24. package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
  25. package/dist/bot/logic/squad/behaviours/common.js +37 -0
  26. package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
  27. package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
  28. package/dist/bot/logic/squad/behaviours/retreatSquad.js +32 -0
  29. package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
  30. package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
  31. package/dist/bot/logic/squad/squad.js +68 -15
  32. package/dist/bot/logic/squad/squadBehaviour.js +5 -5
  33. package/dist/bot/logic/squad/squadBehaviours.js +6 -0
  34. package/dist/bot/logic/squad/squadController.js +106 -15
  35. package/dist/exampleBot.js +22 -7
  36. package/package.json +29 -24
  37. package/src/bot/bot.ts +178 -378
  38. package/src/bot/logic/awareness.ts +220 -0
  39. package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
  40. package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
  41. package/src/bot/logic/building/basicAirUnit.ts +2 -2
  42. package/src/bot/logic/building/basicBuilding.ts +2 -2
  43. package/src/bot/logic/building/basicGroundUnit.ts +83 -78
  44. package/src/bot/logic/building/building.ts +125 -120
  45. package/src/bot/logic/building/harvester.ts +27 -27
  46. package/src/bot/logic/building/powerPlant.ts +1 -1
  47. package/src/bot/logic/building/queueController.ts +17 -38
  48. package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
  49. package/src/bot/logic/common/scout.ts +12 -0
  50. package/src/bot/logic/map/map.ts +11 -3
  51. package/src/bot/logic/map/sector.ts +136 -130
  52. package/src/bot/logic/mission/mission.ts +83 -47
  53. package/src/bot/logic/mission/missionController.ts +103 -51
  54. package/src/bot/logic/mission/missionFactories.ts +46 -0
  55. package/src/bot/logic/mission/missions/attackMission.ts +152 -0
  56. package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
  57. package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
  58. package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
  59. package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
  60. package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
  61. package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
  62. package/src/bot/logic/squad/behaviours/common.ts +37 -0
  63. package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
  64. package/src/bot/logic/squad/behaviours/retreatSquad.ts +46 -0
  65. package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
  66. package/src/bot/logic/squad/squad.ts +163 -97
  67. package/src/bot/logic/squad/squadBehaviour.ts +61 -43
  68. package/src/bot/logic/squad/squadBehaviours.ts +8 -0
  69. package/src/bot/logic/squad/squadController.ts +190 -66
  70. package/src/exampleBot.ts +19 -4
  71. package/tsconfig.json +1 -1
  72. package/src/bot/logic/mission/basicMission.ts +0 -42
  73. package/src/bot/logic/mission/expansionMission.ts +0 -25
  74. package/src/bot/logic/squad/behaviours/squadExpansion.ts +0 -33
@@ -1,66 +1,190 @@
1
- // Meta-controller for forming and controlling squads.
2
-
3
- import { GameApi, PlayerData } from "@chronodivide/game-api";
4
- import { GlobalThreat } from "../threat/threat.js";
5
- import { Squad, SquadLiveness } from "./squad.js";
6
- import { SquadAction, SquadActionDisband, SquadActionMergeInto } from "./squadBehaviour.js";
7
-
8
- export class SquadController {
9
- constructor(
10
- private squads: Squad[] = [],
11
- private unitIdToSquad: Map<number, Squad> = new Map(),
12
- ) {}
13
-
14
- public onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat | undefined) {
15
- // Remove dead squads.
16
- this.squads = this.squads.filter((squad) => squad.getLiveness() == SquadLiveness.SquadDead);
17
- this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
18
-
19
- // Check for units in multiple squads, this shouldn't happen.
20
- this.unitIdToSquad = new Map();
21
- this.squads.forEach((squad) => {
22
- squad.getUnitIds().forEach((unitId) => {
23
- if (this.unitIdToSquad.has(unitId)) {
24
- console.log(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
25
- } else {
26
- this.unitIdToSquad.set(unitId, squad);
27
- }
28
- });
29
- });
30
-
31
- let squadActions = this.squads.map((squad) => {
32
- return {
33
- squad,
34
- action: squad.onAiUpdate(gameApi, playerData, threatData),
35
- };
36
- });
37
- // Handle disbands and merges.
38
- const isDisband = (a: SquadAction): a is SquadActionDisband => a.type == "disband";
39
- const isMerge = (a: SquadAction): a is SquadActionMergeInto => a.type == "mergeInto";
40
- let disbandedSquads: Set<string> = new Set();
41
- squadActions
42
- .filter((a) => isDisband(a.action))
43
- .forEach((a) => {
44
- a.squad.getUnitIds().forEach((unitId) => {
45
- this.unitIdToSquad.delete(unitId);
46
- });
47
- a.squad.clearUnits();
48
- disbandedSquads.add(a.squad.getName());
49
- });
50
- squadActions
51
- .filter((a) => isMerge(a.action))
52
- .forEach((a) => {
53
- let mergeInto = a.action as SquadActionMergeInto;
54
- if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
55
- console.log("Merging into a disbanded squad, cancelling.");
56
- return;
57
- }
58
- a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
59
- disbandedSquads.add(a.squad.getName());
60
- });
61
- // remove disbanded and merged squads.
62
- this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
63
-
64
- // Form squads.
65
- }
66
- }
1
+ // Meta-controller for forming and controlling squads.
2
+
3
+ import { ActionsApi, GameApi, PlayerData, UnitData } from "@chronodivide/game-api";
4
+ import { GlobalThreat } from "../threat/threat.js";
5
+ import { Squad, SquadLiveness } from "./squad.js";
6
+ import {
7
+ SquadAction,
8
+ SquadActionDisband,
9
+ SquadActionGrabFreeCombatants,
10
+ SquadActionMergeInto,
11
+ SquadActionRequestSpecificUnits,
12
+ SquadActionRequestUnits,
13
+ } from "./squadBehaviour.js";
14
+ import { MatchAwareness } from "../awareness.js";
15
+ import { getDistanceBetween } from "../map/map.js";
16
+
17
+ type SquadWithAction<T> = {
18
+ squad: Squad;
19
+ action: T;
20
+ };
21
+
22
+ export class SquadController {
23
+ private squads: Squad[] = [];
24
+ private unitIdToSquad: Map<number, Squad> = new Map();
25
+
26
+ constructor() {}
27
+
28
+ public onAiUpdate(
29
+ gameApi: GameApi,
30
+ actionsApi: ActionsApi,
31
+ playerData: PlayerData,
32
+ matchAwareness: MatchAwareness,
33
+ logger: (message: string) => void
34
+ ) {
35
+ // Remove dead squads or those where the mission is dead.
36
+ this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
37
+ this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
38
+
39
+ // Check for units in multiple squads, this shouldn't happen.
40
+ this.unitIdToSquad = new Map();
41
+ this.squads.forEach((squad) => {
42
+ squad.getUnitIds().forEach((unitId) => {
43
+ if (this.unitIdToSquad.has(unitId)) {
44
+ logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
45
+ } else {
46
+ this.unitIdToSquad.set(unitId, squad);
47
+ }
48
+ });
49
+ });
50
+
51
+ const squadActions: SquadWithAction<SquadAction>[] = this.squads.map((squad) => {
52
+ return {
53
+ squad,
54
+ action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
55
+ };
56
+ });
57
+ // Handle disbands and merges.
58
+ const isDisband = (a: SquadAction): a is SquadActionDisband => a.type === "disband";
59
+ const isMerge = (a: SquadAction): a is SquadActionMergeInto => a.type === "mergeInto";
60
+ let disbandedSquads: Set<string> = new Set();
61
+ squadActions
62
+ .filter((a) => isDisband(a.action))
63
+ .forEach((a) => {
64
+ logger(`Squad ${a.squad.getName()} disbanding as requested.`);
65
+ a.squad.getMission()?.removeSquad();
66
+ a.squad.getUnitIds().forEach((unitId) => {
67
+ this.unitIdToSquad.delete(unitId);
68
+ });
69
+ disbandedSquads.add(a.squad.getName());
70
+ });
71
+ squadActions
72
+ .filter((a) => isMerge(a.action))
73
+ .forEach((a) => {
74
+ let mergeInto = a.action as SquadActionMergeInto;
75
+ if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
76
+ logger(
77
+ `Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`
78
+ );
79
+ return;
80
+ }
81
+ a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
82
+ disbandedSquads.add(a.squad.getName());
83
+ });
84
+ // remove disbanded and merged squads.
85
+ this.squads = this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
86
+
87
+ // Request specific units by ID
88
+ const isRequestSpecific = (a: SquadAction) => a.type === "requestSpecific";
89
+ const unitIdToHighestRequest = squadActions
90
+ .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) {
97
+ prev[unitId] = squadWithAction;
98
+ }
99
+ } else {
100
+ prev[unitId] = squadWithAction;
101
+ }
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
+ }
118
+ });
119
+
120
+ // Request units by type
121
+ const isRequest = (a: SquadAction) => a.type === "request";
122
+ const unitTypeToHighestRequest = squadActions
123
+ .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) {
130
+ prev[unitName] = squadWithAction;
131
+ }
132
+ } else {
133
+ prev[unitName] = squadWithAction;
134
+ }
135
+ });
136
+ return prev;
137
+ }, {} as Record<string, SquadWithAction<SquadActionRequestUnits>>);
138
+
139
+ // Request combat-capable units in an area
140
+ const isGrab = (a: SquadAction) => a.type === "requestCombatants";
141
+ const grabRequests = squadActions.filter((a) =>
142
+ isGrab(a.action)
143
+ ) as SquadWithAction<SquadActionGrabFreeCombatants>[];
144
+
145
+ // Find loose units
146
+ const unitIds = gameApi.getVisibleUnits(playerData.name, "self");
147
+ const freeUnits = unitIds
148
+ .map((unitId) => gameApi.getUnitData(unitId))
149
+ .filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
150
+ .map((unit) => unit!);
151
+
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
+ }`
171
+ );
172
+ this.addUnitToSquad(requestingSquad, freeUnit);
173
+ return true;
174
+ } else {
175
+ return false;
176
+ }
177
+ });
178
+ }
179
+ });
180
+ }
181
+
182
+ private addUnitToSquad(squad: Squad, unit: UnitData) {
183
+ squad.addUnit(unit.id);
184
+ this.unitIdToSquad.set(unit.id, squad);
185
+ }
186
+
187
+ public registerSquad(squad: Squad) {
188
+ this.squads.push(squad);
189
+ }
190
+ }
package/src/exampleBot.ts CHANGED
@@ -2,7 +2,21 @@ import { cdapi } from "@chronodivide/game-api";
2
2
  import { SupalosaBot } from "./bot/bot.js";
3
3
 
4
4
  async function main() {
5
- const mapName = "mp03t4.map";
5
+ /*
6
+ Ladder maps:
7
+ CDR2 1v1 2_malibu_cliffs_le.map
8
+ CDR2 1v1 4_country_swing_le_v2.map
9
+ CDR2 1v1 mp01t4.map
10
+ CDR2 1v1 tn04t2.map
11
+ CDR2 1v1 mp10s4.map
12
+ CDR2 1v1 heckcorners.map
13
+ CDR2 1v1 4_montana_dmz_le.map
14
+ CDR2 1v1 barrel.map
15
+
16
+ Other maps:
17
+ mp03t4
18
+ */
19
+ const mapName = "mp01t4.map";
6
20
  // Bot names must be unique in online mode
7
21
  const botName = `Joe${String(Date.now()).substr(-6)}`;
8
22
  const otherBotName = `Bob${String(Date.now() + 1).substr(-6)}`;
@@ -14,11 +28,12 @@ async function main() {
14
28
 
15
29
  const game = await cdapi.createGame({
16
30
  // Uncomment the following lines to play in real time versus the bot
17
- online: true,
31
+ /*online: true,
18
32
  serverUrl: process.env.SERVER_URL!,
19
33
  clientUrl: process.env.CLIENT_URL!,
20
- agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }],
21
- //agents: [new ExampleBot(botName, "Russians", false), new ExampleBot(otherBotName, "Americans", true)],
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)],
22
37
  buildOffAlly: false,
23
38
  cratesAppear: false,
24
39
  credits: 10000,
package/tsconfig.json CHANGED
@@ -45,7 +45,7 @@
45
45
 
46
46
  /* Module Resolution Options */
47
47
  "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
48
- // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
48
+ //"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
49
49
  // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
50
50
  // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
51
51
  // "typeRoots": [], /* List of folders to include type definitions from. */
@@ -1,42 +0,0 @@
1
- import { GameApi, PlayerData } from "@chronodivide/game-api";
2
- import { Squad } from "../squad/squad.js";
3
- import { GlobalThreat } from "../threat/threat.js";
4
- import { Mission, MissionAction, MissionActionNoop, MissionFactory } from "./mission.js";
5
-
6
- // A basic mission requests specific units and does nothing with them. It is not recommended
7
- // to actually create this in a game as they'll just sit around idle.
8
- export class BasicMission implements Mission {
9
- constructor(
10
- private uniqueName: string,
11
- private priority: number = 1,
12
- private squads: Squad[] = [],
13
- ) {}
14
-
15
- getUniqueName(): string {
16
- return this.uniqueName;
17
- }
18
-
19
- isActive(): boolean {
20
- return true;
21
- }
22
-
23
- removeSquad(squad: Squad): void {
24
- this.squads = this.squads.filter((s) => s != squad);
25
- }
26
-
27
- addSquad(squad: Squad): void {
28
- if (!this.squads.find((s) => s == squad)) {
29
- this.squads.push(squad);
30
- }
31
- }
32
-
33
- getSquads(): Squad[] {
34
- return this.squads;
35
- }
36
-
37
- onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): MissionAction {
38
- return {} as MissionActionNoop;
39
- }
40
-
41
- onSquadAdded(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): void {}
42
- }
@@ -1,25 +0,0 @@
1
- import { GameApi, PlayerData } from "@chronodivide/game-api";
2
- import { GlobalThreat } from "../threat/threat.js";
3
- import { BasicMission } from "./basicMission.js";
4
- import { Mission, MissionAction, MissionActionNoop, MissionFactory } from "./mission";
5
-
6
- export class ExpansionMission extends BasicMission {
7
- constructor(uniqueName: string, priority: number) {
8
- super(uniqueName, priority);
9
- }
10
-
11
- onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): MissionAction {
12
- return {} as MissionActionNoop;
13
- }
14
- }
15
-
16
- export class ExpansionMissionFactory implements MissionFactory {
17
- maybeCreateMission(
18
- gameApi: GameApi,
19
- playerData: PlayerData,
20
- threatData: GlobalThreat | undefined,
21
- existingMissions: Mission[],
22
- ): Mission | undefined {
23
- return new ExpansionMission("expansion", 10);
24
- }
25
- }
@@ -1,33 +0,0 @@
1
- import { GameApi, PlayerData, SideType, TechnoRules } from "@chronodivide/game-api";
2
- import { GlobalThreat } from "../../threat/threat.js";
3
- import { Squad } from "../squad.js";
4
- import { SquadAction, SquadActionNoop, SquadBehaviour } from "../squadBehaviour.js";
5
-
6
- // Expansion or initial base.
7
- export class SquadExpansion implements SquadBehaviour {
8
- public getDesiredComposition(
9
- gameApi: GameApi,
10
- playerData: PlayerData,
11
- squad: Squad,
12
- threatData: GlobalThreat | undefined,
13
- ): { unitName: string; priority: number; amount: number }[] {
14
- // This squad desires an MCV.
15
- let myMcvName = playerData.country?.side == SideType.GDI ? "AMCV" : "SMCV";
16
- return [
17
- {
18
- unitName: myMcvName,
19
- priority: 10,
20
- amount: 1,
21
- },
22
- ];
23
- }
24
-
25
- public onAiUpdate(
26
- gameApi: GameApi,
27
- playerData: PlayerData,
28
- squad: Squad,
29
- threatData: GlobalThreat | undefined,
30
- ): SquadAction {
31
- return {} as SquadActionNoop;
32
- }
33
- }