@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
package/README.md CHANGED
@@ -1,46 +1,71 @@
1
- # Supalosa's Chrono Divine Bot
2
-
3
- [Chrono Divide](https://chronodivide.com/) is a ground-up rebuild of Red Alert 2 in the browser. It is feature-complete and allows for online skirmish play against other players.
4
- It also provides [an API to build bots](https://discord.com/channels/771701199812558848/842700851520339988), as there is no built-in AI yet.
5
-
6
- This repository is one such implementation of a bot.
7
-
8
- ## Development State
9
-
10
- Development on this is paused as I'm currently focusing on a Starcraft 2 bot instead.
11
- The bot only plays Allied, and is not particularly good at the game. Feel free to use its code as a training dummy against your own bot, or extend it if you'd like. Caveat: I'm not a professional AI dev, this was my first foray into this, nor am I particularly experienced with TypeScript or JS.
12
-
13
- ## Future plans (on hold)
14
-
15
- I was working on three things at once before I put this on hold:
16
-
17
- - Task System - Something to not only follow actual build orders, but manage attacks, harass/attack the enemy, perform scouting, expand to other bases etc.
18
- - Squad System - Ability to independently control more than one mass of units (i.e. squads), for example a Harass Squad directed by a Harass Task.
19
- - Map Control System - Ability to analyse the state of the map and decide whether to fight for control over areas. Currently we already divide the map into square regions with individual threat calculations, but don't really do much with that information.
20
-
21
- A lot of these concepts are being built into my Starcraft 2 bot, [Supabot](https://github.com/Supalosa/supabot) - maybe I'll come back to this when I'm done there.
22
-
23
- ## Install instructions
24
-
25
- ```sh
26
- npm install
27
- npm run build
28
- npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm start
29
- ```
30
-
31
- This will create a replay (`.rpl`) file that can be [imported into the live game](https://game.chronodivide.com/).
32
-
33
- ## Playing against the bot
34
-
35
- Contact the developer of Chrono Divide for details if you are seriously interested in playing against a bot (this one or your own).
36
-
37
- ## Debugging
38
-
39
- ```sh
40
- npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" NODE_OPTIONS="--inspect" npm start
41
- ```
42
-
43
- # G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II
44
-
45
- npx cross-env MIX_DIR="G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II" npm start
46
- npx cross-env MIX_DIR="G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II" NODE_OPTIONS="--inspect" npm start
1
+ # Supalosa's Chrono Divide Bot
2
+
3
+ [Chrono Divide](https://chronodivide.com/) is a ground-up rebuild of Red Alert 2 in the browser. It is feature-complete and allows for online skirmish play against other players.
4
+ It also provides [an API to build bots](https://discord.com/channels/771701199812558848/842700851520339988), as there is no built-in AI yet.
5
+
6
+ This repository is one such implementation of a bot.
7
+
8
+ ## Development State
9
+
10
+ Development on this is paused as I'm currently focusing on a Starcraft 2 bot instead.
11
+ The bot only plays Allied, and is not particularly good at the game. Feel free to use its code as a training dummy against your own bot, or extend it if you'd like. Caveat: I'm not a professional AI dev, this was my first foray into this, nor am I particularly experienced with TypeScript or JS.
12
+
13
+ ## Future plans (on hold)
14
+
15
+ I was working on three things at once before I put this on hold:
16
+
17
+ - Task System - Something to not only follow actual build orders, but manage attacks, harass/attack the enemy, perform scouting, expand to other bases etc.
18
+ - Squad System - Ability to independently control more than one mass of units (i.e. squads), for example a Harass Squad directed by a Harass Task.
19
+ - Map Control System - Ability to analyse the state of the map and decide whether to fight for control over areas. Currently we already divide the map into square regions with individual threat calculations, but don't really do much with that information.
20
+
21
+ A lot of these concepts are being built into my Starcraft 2 bot, [Supabot](https://github.com/Supalosa/supabot) - maybe I'll come back to this when I'm done there.
22
+
23
+ ## Install instructions
24
+
25
+ ```sh
26
+ npm install
27
+ npm run build
28
+ npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm start
29
+ ```
30
+
31
+ This will create a replay (`.rpl`) file that can be [imported into the live game](https://game.chronodivide.com/).
32
+
33
+ ## Playing against the bot
34
+
35
+ Contact the developer of Chrono Divide for details if you are seriously interested in playing against a bot (this one or your own).
36
+
37
+ ## Debugging
38
+
39
+ ```sh
40
+ npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" NODE_OPTIONS="--inspect" npm start
41
+ ```
42
+
43
+ # ignore me
44
+
45
+ export GAMEPATH="G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II"
46
+
47
+ export GAMEPATH="D:\EA Games\Command and Conquer Red Alert II"
48
+
49
+ ---
50
+
51
+ npx cross-env MIX_DIR="${GAMEPATH}" npm start
52
+ npx cross-env MIX_DIR="${GAMEPATH}" NODE_OPTIONS="--inspect" npm start
53
+
54
+ ---
55
+
56
+ ladder maps: https://github.com/chronodivide/pvpgn-server/blob/26bbbe39613751cff696a73f087ce5b4cd938fc8/conf/bnmaps.conf.in#L321-L328
57
+
58
+ CDR2 1v1 2_malibu_cliffs_le.map
59
+ CDR2 1v1 4_country_swing_le_v2.map
60
+ CDR2 1v1 mp01t4.map
61
+ CDR2 1v1 tn04t2.map
62
+ CDR2 1v1 mp10s4.map
63
+ CDR2 1v1 heckcorners.map
64
+ CDR2 1v1 4_montana_dmz_le.map
65
+ CDR2 1v1 barrel.map
66
+
67
+ ---
68
+
69
+ to play vs bot
70
+ export SERVER_URL="wss://<region_server>"
71
+ export CLIENT_URL="https://game.chronodivide.com/"
package/dist/bot/bot.js CHANGED
@@ -1,30 +1,21 @@
1
- import { OrderType, ApiEventType, Bot, QueueStatus, ObjectType, FactoryType, } from "@chronodivide/game-api";
1
+ import { ApiEventType, Bot, QueueStatus, ObjectType, FactoryType, } from "@chronodivide/game-api";
2
2
  import { Duration } from "luxon";
3
- import { determineMapBounds, getDistanceBetweenPoints, getPointTowardsOtherPoint } from "./logic/map/map.js";
3
+ import { determineMapBounds } from "./logic/map/map.js";
4
4
  import { SectorCache } from "./logic/map/sector.js";
5
5
  import { MissionController } from "./logic/mission/missionController.js";
6
6
  import { SquadController } from "./logic/squad/squadController.js";
7
- import { calculateGlobalThreat } from "./logic/threat/threatCalculator.js";
8
7
  import { QUEUES, QueueController, queueTypeToName } from "./logic/building/queueController.js";
9
- var BotState;
10
- (function (BotState) {
11
- BotState["Initial"] = "init";
12
- BotState["Deployed"] = "deployed";
13
- BotState["Attacking"] = "attack";
14
- BotState["Defending"] = "defend";
15
- BotState["Scouting"] = "scout";
16
- BotState["Defeated"] = "defeat";
17
- })(BotState || (BotState = {}));
8
+ import { MatchAwarenessImpl } from "./logic/awareness.js";
18
9
  const DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS = 60;
19
10
  const NATURAL_TICK_RATE = 15;
20
- const BOT_AUTO_SURRENDER_TIME_SECONDS = 7200; // 2 hours (approx 30 mins in real game)
21
- export class ExampleBot extends Bot {
11
+ const BOT_AUTO_SURRENDER_TIME_SECONDS = 7200; // 7200; // 2 hours (approx 30 mins in real game)
12
+ export class SupalosaBot extends Bot {
22
13
  constructor(name, country, enableLogging = true) {
23
14
  super(name, country);
24
- this.botState = BotState.Initial;
25
15
  this.tickOfLastAttackOrder = 0;
16
+ this.matchAwareness = null;
17
+ this.missionController = new MissionController((message) => this.logBotStatus(message));
26
18
  this.squadController = new SquadController();
27
- this.missionController = new MissionController();
28
19
  this.queueController = new QueueController();
29
20
  this.enableLogging = enableLogging;
30
21
  }
@@ -33,42 +24,23 @@ export class ExampleBot extends Bot {
33
24
  const botApm = 300;
34
25
  const botRate = botApm / 60;
35
26
  this.tickRatio = Math.ceil(gameRate / botRate);
36
- this.enemyPlayers = game.getPlayers().filter((p) => p !== this.name && !game.areAlliedPlayers(this.name, p));
37
27
  this.knownMapBounds = determineMapBounds(game.mapApi);
38
- this.sectorCache = new SectorCache(game.mapApi, this.knownMapBounds);
39
- this.threatCache = undefined;
28
+ this.matchAwareness = new MatchAwarenessImpl(null, new SectorCache(game.mapApi, this.knownMapBounds), game.getPlayerData(this.name).startLocation, (msg) => this.logBotStatus(msg));
40
29
  this.logBotStatus(`Map bounds: ${this.knownMapBounds.x}, ${this.knownMapBounds.y}`);
41
30
  }
42
31
  onGameTick(game) {
32
+ if (!this.matchAwareness) {
33
+ return;
34
+ }
35
+ const threatCache = this.matchAwareness.getThreatCache();
43
36
  if ((game.getCurrentTick() / NATURAL_TICK_RATE) % DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS === 0) {
44
37
  this.logDebugState(game);
45
38
  }
46
39
  if (game.getCurrentTick() % this.tickRatio === 0) {
47
40
  const myPlayer = game.getPlayerData(this.name);
48
- const sectorsToUpdatePerCycle = 8; // TODO tune this
49
- this.sectorCache?.updateSectors(game.getCurrentTick(), sectorsToUpdatePerCycle, game.mapApi, myPlayer);
50
- let updateRatio = this.sectorCache?.getSectorUpdateRatio(game.getCurrentTick() - game.getTickRate() * 60);
51
- if (updateRatio && updateRatio < 1.0) {
52
- this.logBotStatus(`${updateRatio * 100.0}% of sectors updated in last 60 seconds.`);
53
- }
54
- // Threat decays over time if we haven't killed anything
55
- let boredomFactor = 1.0 -
56
- Math.min(1.0, Math.max(0.0, (this.gameApi.getCurrentTick() - this.tickOfLastAttackOrder) / 1600.0));
57
- let shouldAttack = this.threatCache ? this.isWorthAttacking(this.threatCache, boredomFactor) : false;
58
- if (game.getCurrentTick() % (this.tickRatio * 150) == 0) {
59
- let visibility = this.sectorCache?.getOverallVisibility();
60
- if (visibility) {
61
- this.logBotStatus(`${Math.round(visibility * 1000.0) / 10}% of tiles visible. Calculating threat.`);
62
- this.threatCache = calculateGlobalThreat(game, myPlayer, visibility);
63
- this.logBotStatus(`Threat LAND: Them ${Math.round(this.threatCache.totalOffensiveLandThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiGroundFirepower)}.`);
64
- this.logBotStatus(`Threat DEFENSIVE: Them ${Math.round(this.threatCache.totalDefensiveThreat)}, us: ${Math.round(this.threatCache.totalDefensivePower)}.`);
65
- this.logBotStatus(`Threat AIR: Them ${Math.round(this.threatCache.totalOffensiveAirThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiAirFirepower)}.`);
66
- this.logBotStatus(`Boredom: ${boredomFactor}`);
67
- }
68
- }
41
+ this.matchAwareness.onAiUpdate(game, myPlayer);
69
42
  if (game.getCurrentTick() / NATURAL_TICK_RATE > BOT_AUTO_SURRENDER_TIME_SECONDS) {
70
43
  this.logBotStatus(`Auto-surrendering after ${BOT_AUTO_SURRENDER_TIME_SECONDS} seconds.`);
71
- this.botState = BotState.Defeated;
72
44
  this.actionsApi.quitGame();
73
45
  }
74
46
  // hacky resign condition
@@ -77,136 +49,17 @@ export class ExampleBot extends Bot {
77
49
  const productionBuildings = game.getVisibleUnits(this.name, "self", (r) => r.type == ObjectType.Building && r.factory != FactoryType.None);
78
50
  if (armyUnits.length == 0 && productionBuildings.length == 0 && mcvUnits.length == 0) {
79
51
  this.logBotStatus(`No army or production left, quitting.`);
80
- this.botState = BotState.Defeated;
81
52
  this.actionsApi.quitGame();
82
53
  }
83
54
  // Build logic.
84
- this.queueController.onAiUpdate(game, this.productionApi, this.actionsApi, myPlayer, this.threatCache, (message) => this.logBotStatus(message));
85
- // Mission logic.
86
- this.missionController.onAiUpdate(game, myPlayer, this.threatCache);
87
- // Squad logic.
88
- this.squadController.onAiUpdate(game, myPlayer, this.threatCache);
89
- switch (this.botState) {
90
- case BotState.Initial: {
91
- const baseUnits = game.getGeneralRules().baseUnit;
92
- let conYards = game.getVisibleUnits(this.name, "self", (r) => r.constructionYard);
93
- if (conYards.length) {
94
- this.botState = BotState.Deployed;
95
- break;
96
- }
97
- const units = game.getVisibleUnits(this.name, "self", (r) => baseUnits.includes(r.name));
98
- if (units.length) {
99
- this.actionsApi.orderUnits([units[0]], OrderType.DeploySelected);
100
- }
101
- break;
102
- }
103
- case BotState.Deployed: {
104
- this.botState = BotState.Attacking;
105
- break;
106
- }
107
- case BotState.Attacking: {
108
- const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
109
- if (!shouldAttack) {
110
- this.logBotStatus(`Not worth attacking, reverting to defence.`);
111
- this.botState = BotState.Defending;
112
- }
113
- const enemyBuildings = game.getVisibleUnits(this.name, "hostile");
114
- let foundTarget = false;
115
- if (enemyBuildings.length) {
116
- const weightedTargets = enemyBuildings
117
- .filter((unit) => this.isHostileUnit(game, unit))
118
- .map((unitId) => {
119
- let unit = game.getUnitData(unitId);
120
- return {
121
- unit,
122
- unitId: unitId,
123
- weight: getDistanceBetweenPoints(myPlayer.startLocation, {
124
- x: unit.tile.rx,
125
- y: unit.tile.rx,
126
- }),
127
- };
128
- })
129
- .filter((unit) => unit.unit != null);
130
- weightedTargets.sort((targetA, targetB) => {
131
- return targetA.weight - targetB.weight;
132
- });
133
- const target = weightedTargets.find((_) => true);
134
- if (target !== undefined) {
135
- let targetData = target.unit;
136
- for (const unitId of armyUnits) {
137
- const unit = game.getUnitData(unitId);
138
- foundTarget = true;
139
- if (shouldAttack && unit?.isIdle) {
140
- let orderType = OrderType.AttackMove;
141
- if (targetData?.type == ObjectType.Building) {
142
- orderType = OrderType.Attack;
143
- }
144
- else if (targetData?.rules.canDisguise) {
145
- // Special case for mirage tank/spy as otherwise they just sit next to it.
146
- orderType = OrderType.Attack;
147
- }
148
- this.actionsApi.orderUnits([unitId], orderType, target.unitId);
149
- }
150
- }
151
- }
152
- }
153
- if (!foundTarget) {
154
- this.logBotStatus(`Can't see any targets, scouting.`);
155
- this.botState = BotState.Scouting;
156
- }
157
- break;
158
- }
159
- case BotState.Defending: {
160
- const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
161
- const enemy = game.getPlayerData(this.enemyPlayers[0]);
162
- const fallbackPoint = getPointTowardsOtherPoint(game, myPlayer.startLocation, enemy.startLocation, 10, 10, 0);
163
- armyUnits.forEach((armyUnitId) => {
164
- let unit = game.getUnitData(armyUnitId);
165
- if (unit && !unit.guardMode) {
166
- let distanceToFallback = getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, fallbackPoint);
167
- if (distanceToFallback > 10) {
168
- this.actionsApi.orderUnits([armyUnitId], OrderType.GuardArea, fallbackPoint.x, fallbackPoint.y);
169
- }
170
- }
171
- });
172
- if (shouldAttack) {
173
- this.logBotStatus(`Finished defending, ready to attack.`);
174
- this.botState = BotState.Attacking;
175
- }
176
- break;
177
- }
178
- case BotState.Scouting: {
179
- const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
180
- let candidatePoints = [];
181
- // Move to an unseen starting location.
182
- const unseenStartingLocations = game.mapApi.getStartingLocations().filter((startingLocation) => {
183
- if (startingLocation == game.getPlayerData(this.name).startLocation) {
184
- return false;
185
- }
186
- let tile = game.mapApi.getTile(startingLocation.x, startingLocation.y);
187
- return tile ? !game.mapApi.isVisibleTile(tile, this.name) : false;
188
- });
189
- candidatePoints.push(...unseenStartingLocations);
190
- armyUnits.forEach((unitId) => {
191
- if (candidatePoints.length > 0) {
192
- const unit = game.getUnitData(unitId);
193
- if (unit?.isIdle) {
194
- const scoutLocation = candidatePoints[Math.floor(game.generateRandom() * candidatePoints.length)];
195
- this.actionsApi.orderUnits([unitId], OrderType.AttackMove, scoutLocation.x, scoutLocation.y);
196
- }
197
- }
198
- });
199
- const enemyBuildings = game
200
- .getVisibleUnits(this.name, "hostile")
201
- .filter((unit) => this.isHostileUnit(game, unit));
202
- if (enemyBuildings.length > 0) {
203
- this.logBotStatus(`Scouted a target, reverting to attack mode.`);
204
- this.botState = BotState.Attacking;
205
- }
206
- break;
207
- }
208
- default:
209
- break;
55
+ this.queueController.onAiUpdate(game, this.productionApi, this.actionsApi, myPlayer, threatCache, (message) => this.logBotStatus(message));
56
+ // Mission logic every 6 ticks
57
+ if (this.gameApi.getCurrentTick() % 6 === 0) {
58
+ this.missionController.onAiUpdate(game, myPlayer, this.matchAwareness, this.squadController);
59
+ }
60
+ // Squad logic every 3 ticks
61
+ if (this.gameApi.getCurrentTick() % 3 === 0) {
62
+ this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness, (message) => this.logBotStatus(message));
210
63
  }
211
64
  }
212
65
  }
@@ -217,9 +70,12 @@ export class ExampleBot extends Bot {
217
70
  if (!this.enableLogging) {
218
71
  return;
219
72
  }
220
- console.log(`[${this.getHumanTimestamp(this.gameApi)} ${this.name} ${this.botState}] ${message}`);
73
+ console.log(`[${this.getHumanTimestamp(this.gameApi)} ${this.name}] ${message}`);
221
74
  }
222
75
  logDebugState(game) {
76
+ if (!this.enableLogging) {
77
+ return;
78
+ }
223
79
  const myPlayer = game.getPlayerData(this.name);
224
80
  const queueState = QUEUES.reduce((prev, queueType) => {
225
81
  if (this.productionApi.getQueueData(queueType).size === 0) {
@@ -238,20 +94,8 @@ export class ExampleBot extends Bot {
238
94
  const harvesters = game.getVisibleUnits(this.name, "self", (r) => r.harvester).length;
239
95
  this.logBotStatus(`Harvesters: ${harvesters}`);
240
96
  this.logBotStatus(`----- End -----`);
241
- }
242
- isWorthAttacking(threatCache, threatFactor) {
243
- let scaledGroundPower = Math.pow(threatCache.totalAvailableAntiGroundFirepower, 1.125);
244
- let scaledGroundThreat = (threatFactor * threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat) * 1.1;
245
- let scaledAirPower = Math.pow(threatCache.totalAvailableAirPower, 1.125);
246
- let scaledAirThreat = (threatFactor * threatCache.totalOffensiveAntiAirThreat + threatCache.totalDefensiveThreat) * 1.1;
247
- return scaledGroundPower > scaledGroundThreat || scaledAirPower > scaledAirThreat;
248
- }
249
- isHostileUnit(game, unitId) {
250
- const unitData = game.getUnitData(unitId);
251
- if (!unitData) {
252
- return false;
253
- }
254
- return unitData.owner != this.name && game.getPlayerData(unitData.owner)?.isCombatant;
97
+ this.missionController.logDebugOutput();
98
+ this.actionsApi.sayAll(`Cash: ${myPlayer.credits}`);
255
99
  }
256
100
  onGameEvent(ev) {
257
101
  switch (ev.type) {
@@ -0,0 +1,122 @@
1
+ import { ObjectType } from "@chronodivide/game-api";
2
+ import { calculateGlobalThreat } from "../logic/threat/threatCalculator.js";
3
+ import { getDistanceBetweenPoints, getPointTowardsOtherPoint } from "../logic/map/map.js";
4
+ import { Circle, Quadtree } from "@timohausmann/quadtree-ts";
5
+ const SECTORS_TO_UPDATE_PER_CYCLE = 8;
6
+ const RALLY_POINT_UPDATE_INTERVAL_TICKS = 60;
7
+ const THREAT_UPDATE_INTERVAL_TICKS = 30;
8
+ const rebuildQuadtree = (quadtree, units) => {
9
+ quadtree.clear();
10
+ units.forEach((unit) => {
11
+ quadtree.insert(new Circle({ x: unit.tile.rx, y: unit.tile.ry, r: 1, data: unit.id }));
12
+ });
13
+ };
14
+ export class MatchAwarenessImpl {
15
+ constructor(threatCache, sectorCache, mainRallyPoint, logger) {
16
+ this.threatCache = threatCache;
17
+ this.sectorCache = sectorCache;
18
+ this.mainRallyPoint = mainRallyPoint;
19
+ this.logger = logger;
20
+ this._shouldAttack = false;
21
+ const { x: width, y: height } = sectorCache.getMapBounds();
22
+ this.hostileQuadTree = new Quadtree({ width, height });
23
+ }
24
+ getHostilesNearPoint2d(point, radius) {
25
+ return this.getHostilesNearPoint(point.x, point.y, radius);
26
+ }
27
+ getHostilesNearPoint(searchX, searchY, radius) {
28
+ const intersections = this.hostileQuadTree.retrieve(new Circle({ x: searchX, y: searchY, r: radius }));
29
+ return intersections
30
+ .map(({ x, y, data: unitId }) => ({ x, y, unitId: unitId }))
31
+ .filter(({ x, y }) => getDistanceBetweenPoints({ x, y }, { x: searchX, y: searchY }) <= radius)
32
+ .filter(({ unitId }) => !!unitId);
33
+ }
34
+ getThreatCache() {
35
+ return this.threatCache;
36
+ }
37
+ getSectorCache() {
38
+ return this.sectorCache;
39
+ }
40
+ getMainRallyPoint() {
41
+ return this.mainRallyPoint;
42
+ }
43
+ shouldAttack() {
44
+ return this._shouldAttack;
45
+ }
46
+ checkShouldAttack(threatCache, threatFactor) {
47
+ let scaledGroundPower = Math.pow(threatCache.totalAvailableAntiGroundFirepower, 1.025);
48
+ let scaledGroundThreat = (threatFactor * threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat) * 1.1;
49
+ let scaledAirPower = Math.pow(threatCache.totalAvailableAirPower, 1.025);
50
+ let scaledAirThreat = (threatFactor * threatCache.totalOffensiveAntiAirThreat + threatCache.totalDefensiveThreat) * 1.1;
51
+ return scaledGroundPower > scaledGroundThreat || scaledAirPower > scaledAirThreat;
52
+ }
53
+ isHostileUnit(unit, hostilePlayerNames) {
54
+ if (!unit) {
55
+ return false;
56
+ }
57
+ return hostilePlayerNames.includes(unit.owner);
58
+ }
59
+ onAiUpdate(game, playerData) {
60
+ const sectorCache = this.sectorCache;
61
+ sectorCache.updateSectors(game.getCurrentTick(), SECTORS_TO_UPDATE_PER_CYCLE, game.mapApi, playerData);
62
+ let updateRatio = sectorCache?.getSectorUpdateRatio(game.getCurrentTick() - game.getTickRate() * 60);
63
+ if (updateRatio && updateRatio < 1.0) {
64
+ this.logger(`${updateRatio * 100.0}% of sectors updated in last 60 seconds.`);
65
+ }
66
+ const hostilePlayerNames = game
67
+ .getPlayers()
68
+ .map((name) => game.getPlayerData(name))
69
+ .filter((other) => other.name !== playerData.name &&
70
+ other.isCombatant &&
71
+ !game.areAlliedPlayers(playerData.name, other.name))
72
+ .map((other) => other.name);
73
+ // Build the quadtree, if this is too slow we should consider doing this periodically.
74
+ const hostileUnitIds = game.getVisibleUnits(playerData.name, "hostile", (r) => r.isSelectableCombatant || r.type === ObjectType.Building);
75
+ try {
76
+ const hostileUnits = hostileUnitIds
77
+ .map((id) => game.getUnitData(id))
78
+ .filter((unit) => this.isHostileUnit(unit, hostilePlayerNames));
79
+ rebuildQuadtree(this.hostileQuadTree, hostileUnits);
80
+ }
81
+ catch (err) {
82
+ // Hack. Will be fixed soon.
83
+ console.error(`caught error`, hostileUnitIds);
84
+ }
85
+ if (game.getCurrentTick() % THREAT_UPDATE_INTERVAL_TICKS == 0) {
86
+ let visibility = sectorCache?.getOverallVisibility();
87
+ if (visibility) {
88
+ this.logger(`${Math.round(visibility * 1000.0) / 10}% of tiles visible. Calculating threat.`);
89
+ // Update the global threat cache
90
+ this.threatCache = calculateGlobalThreat(game, playerData, visibility);
91
+ this.logger(`Threat LAND: Them ${Math.round(this.threatCache.totalOffensiveLandThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiGroundFirepower)}.`);
92
+ this.logger(`Threat DEFENSIVE: Them ${Math.round(this.threatCache.totalDefensiveThreat)}, us: ${Math.round(this.threatCache.totalDefensivePower)}.`);
93
+ this.logger(`Threat AIR: Them ${Math.round(this.threatCache.totalOffensiveAirThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiAirFirepower)}.`);
94
+ // As the game approaches 2 hours, be more willing to attack. (15 ticks per second)
95
+ const gameLengthFactor = Math.max(0, 1.0 - game.getCurrentTick() / (15 * 7200.0));
96
+ this.logger(`Game length multiplier: ${gameLengthFactor}`);
97
+ if (!this._shouldAttack) {
98
+ // If not attacking, make it harder to switch to attack mode by multiplying the opponent's threat.
99
+ this._shouldAttack = this.checkShouldAttack(this.threatCache, 1.25 * gameLengthFactor);
100
+ if (this._shouldAttack) {
101
+ this.logger(`Globally switched to attack mode.`);
102
+ }
103
+ }
104
+ else {
105
+ // If currently attacking, make it harder to switch to defence mode my dampening the opponent's threat.
106
+ this._shouldAttack = this.checkShouldAttack(this.threatCache, 0.75 * gameLengthFactor);
107
+ if (!this._shouldAttack) {
108
+ this.logger(`Globally switched to defence mode.`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ // Update rally point every few ticks.
114
+ if (game.getCurrentTick() % RALLY_POINT_UPDATE_INTERVAL_TICKS === 0) {
115
+ const enemyPlayers = game
116
+ .getPlayers()
117
+ .filter((p) => p !== playerData.name && !game.areAlliedPlayers(playerData.name, p));
118
+ const enemy = game.getPlayerData(enemyPlayers[0]);
119
+ this.mainRallyPoint = getPointTowardsOtherPoint(game, playerData.startLocation, enemy.startLocation, 10, 10, 0);
120
+ }
121
+ }
122
+ }
@@ -11,8 +11,9 @@ export class BasicGroundUnit {
11
11
  return undefined;
12
12
  }
13
13
  getPriority(game, playerData, technoRules, threatCache) {
14
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
14
15
  if (threatCache) {
15
- let priority = 1;
16
+ let priority = this.basePriority;
16
17
  if (this.antiGroundPower > 0) {
17
18
  // If the enemy's power is increasing we should try to keep up.
18
19
  if (threatCache.totalOffensiveLandThreat > threatCache.totalAvailableAntiGroundFirepower) {
@@ -25,10 +26,11 @@ export class BasicGroundUnit {
25
26
  else {
26
27
  // But also, if our power dwarfs the enemy, keep pressing the advantage.
27
28
  priority +=
28
- this.antiGroundPower *
29
+ (this.antiGroundPower *
29
30
  this.basePriority *
30
31
  Math.sqrt(threatCache.totalAvailableAntiGroundFirepower /
31
- Math.max(1, threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat));
32
+ Math.max(1, threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat))) /
33
+ (numOwned + 1);
32
34
  }
33
35
  }
34
36
  if (this.antiAirPower > 0) {
@@ -40,15 +42,15 @@ export class BasicGroundUnit {
40
42
  }
41
43
  else {
42
44
  priority +=
43
- this.antiAirPower *
45
+ (this.antiAirPower *
44
46
  this.basePriority *
45
47
  Math.sqrt(threatCache.totalAvailableAntiAirFirepower /
46
- Math.max(1, threatCache.totalOffensiveAirThreat + threatCache.totalDefensiveThreat));
48
+ Math.max(1, threatCache.totalOffensiveAirThreat + threatCache.totalDefensiveThreat))) /
49
+ (numOwned + 1);
47
50
  }
48
51
  }
49
52
  return priority;
50
53
  }
51
- const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
52
54
  return this.basePriority * (1.0 - numOwned / this.baseAmount);
53
55
  }
54
56
  getMaxCount(game, playerData, technoRules, threatCache) {
@@ -31,7 +31,6 @@ export function getDefaultPlacementLocation(game, playerData, startPoint, techno
31
31
  }
32
32
  }
33
33
  }
34
- console.log("Can't find a place to put the " + technoRules.name);
35
34
  return undefined;
36
35
  }
37
36
  export const DEFAULT_BUILDING_PRIORITY = 1;
@@ -45,14 +44,16 @@ export const BUILDING_NAME_TO_RULES = new Map([
45
44
  ["ENGINEER", new BasicBuilding(1, 1, 10000)],
46
45
  ["GADEPT", new BasicBuilding(1, 1, 10000)],
47
46
  ["GAAIRC", new BasicBuilding(8, 1, 6000)],
47
+ ["GATECH", new BasicBuilding(20, 1, 4000)],
48
48
  ["GAPILL", new AntiGroundStaticDefence(5, 1, 5)],
49
49
  ["ATESLA", new AntiGroundStaticDefence(5, 1, 10)],
50
50
  ["GAWALL", new AntiGroundStaticDefence(0, 0, 0)],
51
- ["E1", new BasicGroundUnit(5, 3, 0.25, 0)],
51
+ ["E1", new BasicGroundUnit(2, 3, 0.25, 0)],
52
52
  ["MTNK", new BasicGroundUnit(10, 3, 2, 0)],
53
53
  ["MGTK", new BasicGroundUnit(10, 1, 2.5, 0)],
54
54
  ["FV", new BasicGroundUnit(5, 2, 0.5, 1)],
55
55
  ["JUMPJET", new BasicAirUnit(10, 1, 1, 1)],
56
+ ["ORCA", new BasicAirUnit(7, 1, 2, 0)],
56
57
  ["SREF", new ArtilleryUnit(9, 1)],
57
58
  ["CLEG", new BasicGroundUnit(0, 0)],
58
59
  ["SHAD", new BasicGroundUnit(0, 0)],
@@ -65,10 +66,12 @@ export const BUILDING_NAME_TO_RULES = new Map([
65
66
  ["SENGINEER", new BasicBuilding(1, 1, 10000)],
66
67
  ["NADEPT", new BasicBuilding(1, 1, 10000)],
67
68
  ["NARADR", new BasicBuilding(8, 1, 4000)],
69
+ ["NANRCT", new PowerPlant()],
70
+ ["NATECH", new BasicBuilding(20, 1, 4000)],
68
71
  ["NALASR", new AntiGroundStaticDefence(5, 1, 5)],
69
72
  ["TESLA", new AntiGroundStaticDefence(5, 1, 10)],
70
73
  ["NAWALL", new AntiGroundStaticDefence(0, 0, 0)],
71
- ["E2", new BasicGroundUnit(5, 3, 0.25, 0)],
74
+ ["E2", new BasicGroundUnit(2, 3, 0.25, 0)],
72
75
  ["HTNK", new BasicGroundUnit(10, 3, 3, 0)],
73
76
  ["APOC", new BasicGroundUnit(6, 1, 5, 0)],
74
77
  ["HTK", new BasicGroundUnit(5, 2, 0.33, 1.5)],
@@ -9,7 +9,7 @@ export class Harvester extends BasicGroundUnit {
9
9
  getPriority(game, playerData, technoRules, threatCache) {
10
10
  const refineries = game.getVisibleUnits(playerData.name, "self", (r) => r.refinery).length;
11
11
  const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
12
- const boost = harvesters < this.minNeeded ? 5 : 1;
12
+ const boost = harvesters < this.minNeeded ? 3 : 1;
13
13
  return this.basePriority * (refineries / Math.max(harvesters / IDEAL_HARVESTERS_PER_REFINERY, 1)) * boost;
14
14
  }
15
15
  }
@@ -26,16 +26,9 @@ export const queueTypeToName = (queue) => {
26
26
  return "Unknown";
27
27
  }
28
28
  };
29
- // Repair buildings at this ratio of the maxHitpoints.
30
- const REPAIR_HITPOINTS_RATIO = 0.9;
31
- // Don't repair buildings more often than this.
32
- const REPAIR_COOLDOWN_TICKS = 15;
33
29
  const DEBUG_BUILD_QUEUES = true;
34
30
  export class QueueController {
35
- constructor(buildingIdToLastHitpoints = new Map(), buildingIdLastRepairedAtTick = new Map()) {
36
- this.buildingIdToLastHitpoints = buildingIdToLastHitpoints;
37
- this.buildingIdLastRepairedAtTick = buildingIdLastRepairedAtTick;
38
- }
31
+ constructor() { }
39
32
  onAiUpdate(game, productionApi, actionsApi, playerData, threatCache, logger) {
40
33
  const decisions = QUEUES.map((queueType) => {
41
34
  const options = productionApi.getAvailableObjects(queueType);
@@ -54,27 +47,17 @@ export class QueueController {
54
47
  this.updateBuildQueue(game, productionApi, actionsApi, playerData, threatCache, decision.queue, decision.decision, totalWeightAcrossQueues, totalCostAcrossQueues, logger);
55
48
  });
56
49
  // Repair is simple - just repair everything that's damaged.
57
- // Unfortunately there doesn't seem to be an API to determine if something is being repaired, so we have to remember it.
58
50
  game.getVisibleUnits(playerData.name, "self", (r) => r.repairable).forEach((unitId) => {
59
51
  const unit = game.getUnitData(unitId);
60
- if (!unit || !unit.hitPoints || !unit.maxHitPoints) {
52
+ if (!unit || !unit.hitPoints || !unit.maxHitPoints || unit.hasWrenchRepair) {
61
53
  return;
62
54
  }
63
- const lastKnownHitpoints = this.buildingIdToLastHitpoints.get(unitId) || unit.hitPoints;
64
- const buildingLastRepairedAt = this.buildingIdLastRepairedAtTick.get(unitId) || 0;
65
- // Only repair if HP is going down and if we haven't recently repaired it
66
- if (unit.hitPoints <= lastKnownHitpoints &&
67
- game.getCurrentTick() > buildingLastRepairedAt + REPAIR_COOLDOWN_TICKS &&
68
- unit.hitPoints < unit.maxHitPoints * REPAIR_HITPOINTS_RATIO) {
69
- actionsApi.toggleRepairWrench(unitId);
70
- this.buildingIdLastRepairedAtTick.set(unitId, game.getCurrentTick());
71
- }
72
- this.buildingIdToLastHitpoints.set(unitId, unit.hitPoints);
55
+ actionsApi.toggleRepairWrench(unitId);
73
56
  });
74
57
  }
75
58
  updateBuildQueue(game, productionApi, actionsApi, playerData, threatCache, queueType, decision, totalWeightAcrossQueues, totalCostAcrossQueues, logger) {
76
59
  const myCredits = playerData.credits;
77
- let queueData = productionApi.getQueueData(queueType);
60
+ const queueData = productionApi.getQueueData(queueType);
78
61
  if (queueData.status == QueueStatus.Idle) {
79
62
  // Start building the decided item.
80
63
  if (decision !== undefined) {