@supalosa/chronodivide-bot 0.1.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 (56) hide show
  1. package/.prettierrc +5 -0
  2. package/README.md +46 -0
  3. package/dist/bot/bot.js +269 -0
  4. package/dist/bot/logic/building/ArtilleryUnit.js +24 -0
  5. package/dist/bot/logic/building/antiGroundStaticDefence.js +40 -0
  6. package/dist/bot/logic/building/basicAirUnit.js +39 -0
  7. package/dist/bot/logic/building/basicBuilding.js +25 -0
  8. package/dist/bot/logic/building/basicGroundUnit.js +57 -0
  9. package/dist/bot/logic/building/building.js +77 -0
  10. package/dist/bot/logic/building/harvester.js +15 -0
  11. package/dist/bot/logic/building/massedAntiGroundUnit.js +20 -0
  12. package/dist/bot/logic/building/powerPlant.js +20 -0
  13. package/dist/bot/logic/building/queueController.js +168 -0
  14. package/dist/bot/logic/building/queues.js +19 -0
  15. package/dist/bot/logic/building/resourceCollectionBuilding.js +34 -0
  16. package/dist/bot/logic/map/map.js +57 -0
  17. package/dist/bot/logic/map/sector.js +104 -0
  18. package/dist/bot/logic/mission/basicMission.js +30 -0
  19. package/dist/bot/logic/mission/expansionMission.js +14 -0
  20. package/dist/bot/logic/mission/mission.js +2 -0
  21. package/dist/bot/logic/mission/missionController.js +47 -0
  22. package/dist/bot/logic/squad/behaviours/squadExpansion.js +18 -0
  23. package/dist/bot/logic/squad/behaviours/squadScouters.js +8 -0
  24. package/dist/bot/logic/squad/squad.js +73 -0
  25. package/dist/bot/logic/squad/squadBehaviour.js +5 -0
  26. package/dist/bot/logic/squad/squadController.js +58 -0
  27. package/dist/bot/logic/threat/threat.js +22 -0
  28. package/dist/bot/logic/threat/threatCalculator.js +72 -0
  29. package/dist/exampleBot.js +38 -0
  30. package/package.json +24 -0
  31. package/rules.ini +23126 -0
  32. package/src/bot/bot.ts +378 -0
  33. package/src/bot/logic/building/ArtilleryUnit.ts +43 -0
  34. package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -0
  35. package/src/bot/logic/building/basicAirUnit.ts +68 -0
  36. package/src/bot/logic/building/basicBuilding.ts +47 -0
  37. package/src/bot/logic/building/basicGroundUnit.ts +78 -0
  38. package/src/bot/logic/building/building.ts +120 -0
  39. package/src/bot/logic/building/harvester.ts +27 -0
  40. package/src/bot/logic/building/powerPlant.ts +32 -0
  41. package/src/bot/logic/building/queueController.ts +255 -0
  42. package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -0
  43. package/src/bot/logic/map/map.ts +76 -0
  44. package/src/bot/logic/map/sector.ts +130 -0
  45. package/src/bot/logic/mission/basicMission.ts +42 -0
  46. package/src/bot/logic/mission/expansionMission.ts +25 -0
  47. package/src/bot/logic/mission/mission.ts +47 -0
  48. package/src/bot/logic/mission/missionController.ts +51 -0
  49. package/src/bot/logic/squad/behaviours/squadExpansion.ts +33 -0
  50. package/src/bot/logic/squad/squad.ts +97 -0
  51. package/src/bot/logic/squad/squadBehaviour.ts +43 -0
  52. package/src/bot/logic/squad/squadController.ts +66 -0
  53. package/src/bot/logic/threat/threat.ts +15 -0
  54. package/src/bot/logic/threat/threatCalculator.ts +99 -0
  55. package/src/exampleBot.ts +44 -0
  56. package/tsconfig.json +73 -0
package/.prettierrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "tabWidth": 4,
3
+ "useTabs": false,
4
+ "printWidth": 120
5
+ }
package/README.md ADDED
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,269 @@
1
+ import { OrderType, ApiEventType, Bot, QueueStatus, ObjectType, FactoryType, } from "@chronodivide/game-api";
2
+ import { Duration } from "luxon";
3
+ import { determineMapBounds, getDistanceBetweenPoints, getPointTowardsOtherPoint } from "./logic/map/map.js";
4
+ import { SectorCache } from "./logic/map/sector.js";
5
+ import { MissionController } from "./logic/mission/missionController.js";
6
+ import { SquadController } from "./logic/squad/squadController.js";
7
+ import { calculateGlobalThreat } from "./logic/threat/threatCalculator.js";
8
+ 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 = {}));
18
+ const DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS = 60;
19
+ 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 {
22
+ constructor(name, country, enableLogging = true) {
23
+ super(name, country);
24
+ this.botState = BotState.Initial;
25
+ this.tickOfLastAttackOrder = 0;
26
+ this.squadController = new SquadController();
27
+ this.missionController = new MissionController();
28
+ this.queueController = new QueueController();
29
+ this.enableLogging = enableLogging;
30
+ }
31
+ onGameStart(game) {
32
+ const gameRate = game.getTickRate();
33
+ const botApm = 300;
34
+ const botRate = botApm / 60;
35
+ this.tickRatio = Math.ceil(gameRate / botRate);
36
+ this.enemyPlayers = game.getPlayers().filter((p) => p !== this.name && !game.areAlliedPlayers(this.name, p));
37
+ this.knownMapBounds = determineMapBounds(game.mapApi);
38
+ this.sectorCache = new SectorCache(game.mapApi, this.knownMapBounds);
39
+ this.threatCache = undefined;
40
+ this.logBotStatus(`Map bounds: ${this.knownMapBounds.x}, ${this.knownMapBounds.y}`);
41
+ }
42
+ onGameTick(game) {
43
+ if ((game.getCurrentTick() / NATURAL_TICK_RATE) % DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS === 0) {
44
+ this.logDebugState(game);
45
+ }
46
+ if (game.getCurrentTick() % this.tickRatio === 0) {
47
+ 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
+ }
69
+ if (game.getCurrentTick() / NATURAL_TICK_RATE > BOT_AUTO_SURRENDER_TIME_SECONDS) {
70
+ this.logBotStatus(`Auto-surrendering after ${BOT_AUTO_SURRENDER_TIME_SECONDS} seconds.`);
71
+ this.botState = BotState.Defeated;
72
+ this.actionsApi.quitGame();
73
+ }
74
+ // hacky resign condition
75
+ const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
76
+ const mcvUnits = game.getVisibleUnits(this.name, "self", (r) => !!r.deploysInto && game.getGeneralRules().baseUnit.includes(r.name));
77
+ const productionBuildings = game.getVisibleUnits(this.name, "self", (r) => r.type == ObjectType.Building && r.factory != FactoryType.None);
78
+ if (armyUnits.length == 0 && productionBuildings.length == 0 && mcvUnits.length == 0) {
79
+ this.logBotStatus(`No army or production left, quitting.`);
80
+ this.botState = BotState.Defeated;
81
+ this.actionsApi.quitGame();
82
+ }
83
+ // 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;
210
+ }
211
+ }
212
+ }
213
+ getHumanTimestamp(game) {
214
+ return Duration.fromMillis((game.getCurrentTick() / NATURAL_TICK_RATE) * 1000).toFormat("hh:mm:ss");
215
+ }
216
+ logBotStatus(message) {
217
+ if (!this.enableLogging) {
218
+ return;
219
+ }
220
+ console.log(`[${this.getHumanTimestamp(this.gameApi)} ${this.name} ${this.botState}] ${message}`);
221
+ }
222
+ logDebugState(game) {
223
+ const myPlayer = game.getPlayerData(this.name);
224
+ const queueState = QUEUES.reduce((prev, queueType) => {
225
+ if (this.productionApi.getQueueData(queueType).size === 0) {
226
+ return prev;
227
+ }
228
+ const paused = this.productionApi.getQueueData(queueType).status === QueueStatus.OnHold;
229
+ return (prev +
230
+ " [" +
231
+ queueTypeToName(queueType) +
232
+ (paused ? " PAUSED" : "") +
233
+ ": " +
234
+ this.productionApi.getQueueData(queueType).items.map((item) => item.rules.name + "x" + item.quantity) +
235
+ "]");
236
+ }, "");
237
+ this.logBotStatus(`----- Cash: ${myPlayer.credits} ----- | Queues: ${queueState}`);
238
+ const harvesters = game.getVisibleUnits(this.name, "self", (r) => r.harvester).length;
239
+ this.logBotStatus(`Harvesters: ${harvesters}`);
240
+ 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;
255
+ }
256
+ onGameEvent(ev) {
257
+ switch (ev.type) {
258
+ case ApiEventType.ObjectDestroy: {
259
+ // Add to the stalemate detection.
260
+ if (ev.attackerInfo?.playerName == this.name) {
261
+ this.tickOfLastAttackOrder += (this.gameApi.getCurrentTick() - this.tickOfLastAttackOrder) / 2;
262
+ }
263
+ break;
264
+ }
265
+ default:
266
+ break;
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,24 @@
1
+ import { numBuildingsOwnedOfType } from "./building.js";
2
+ export class ArtilleryUnit {
3
+ constructor(basePriority, baseAmount) {
4
+ this.basePriority = basePriority;
5
+ this.baseAmount = baseAmount;
6
+ }
7
+ getPlacementLocation(game, playerData, technoRules) {
8
+ return undefined;
9
+ }
10
+ getPriority(game, playerData, technoRules, threatCache) {
11
+ // If the enemy's defensive power is increasing we will start to build these.
12
+ if (threatCache) {
13
+ if (threatCache.totalDefensivePower > threatCache.totalAvailableAntiGroundFirepower) {
14
+ return (this.basePriority *
15
+ (threatCache.totalAvailableAntiGroundFirepower / Math.max(1, threatCache.totalDefensivePower)));
16
+ }
17
+ }
18
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
19
+ return this.basePriority * (1.0 - numOwned / this.baseAmount);
20
+ }
21
+ getMaxCount(game, playerData, technoRules, threatCache) {
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ import { getPointTowardsOtherPoint } from "../map/map.js";
2
+ import { getDefaultPlacementLocation, numBuildingsOwnedOfType } from "./building.js";
3
+ export class AntiGroundStaticDefence {
4
+ constructor(basePriority, baseAmount, strength) {
5
+ this.basePriority = basePriority;
6
+ this.baseAmount = baseAmount;
7
+ this.strength = strength;
8
+ }
9
+ getPlacementLocation(game, playerData, technoRules) {
10
+ // Prefer front towards enemy.
11
+ let startLocation = playerData.startLocation;
12
+ let players = game.getPlayers();
13
+ let enemyFacingLocationCandidates = [];
14
+ for (let i = 0; i < players.length; ++i) {
15
+ let playerName = players[i];
16
+ if (playerName == playerData.name) {
17
+ continue;
18
+ }
19
+ let enemyPlayer = game.getPlayerData(playerName);
20
+ enemyFacingLocationCandidates.push(getPointTowardsOtherPoint(game, startLocation, enemyPlayer.startLocation, 4, 16, 1.5));
21
+ }
22
+ let selectedLocation = enemyFacingLocationCandidates[Math.floor(game.generateRandom() * enemyFacingLocationCandidates.length)];
23
+ return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules);
24
+ }
25
+ getPriority(game, playerData, technoRules, threatCache) {
26
+ // If the enemy's ground power is increasing we should try to keep up.
27
+ if (threatCache) {
28
+ let denominator = threatCache.totalAvailableAntiGroundFirepower + threatCache.totalDefensivePower + this.strength;
29
+ if (threatCache.totalOffensiveLandThreat > denominator * 1.1) {
30
+ return this.basePriority * (threatCache.totalOffensiveLandThreat / Math.max(1, denominator));
31
+ }
32
+ }
33
+ const strengthPerCost = (this.strength / technoRules.cost) * 1000;
34
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
35
+ return this.basePriority * (1.0 - numOwned / this.baseAmount) * strengthPerCost;
36
+ }
37
+ getMaxCount(game, playerData, technoRules, threatCache) {
38
+ return null;
39
+ }
40
+ }
@@ -0,0 +1,39 @@
1
+ import { numBuildingsOwnedOfType } from "./building.js";
2
+ export class BasicAirUnit {
3
+ constructor(basePriority, baseAmount, antiGroundPower = 1, // boolean for now, but will eventually be used in weighting.
4
+ antiAirPower = 0) {
5
+ this.basePriority = basePriority;
6
+ this.baseAmount = baseAmount;
7
+ this.antiGroundPower = antiGroundPower;
8
+ this.antiAirPower = antiAirPower;
9
+ }
10
+ getPlacementLocation(game, playerData, technoRules) {
11
+ return undefined;
12
+ }
13
+ getPriority(game, playerData, technoRules, threatCache) {
14
+ // If the enemy's anti-air power is low we might build more.
15
+ if (threatCache) {
16
+ let priority = 0;
17
+ if (this.antiGroundPower > 0 &&
18
+ threatCache.totalOffensiveLandThreat > threatCache.totalAvailableAntiGroundFirepower) {
19
+ priority +=
20
+ this.basePriority *
21
+ (threatCache.totalOffensiveLandThreat / Math.max(1, threatCache.totalAvailableAntiGroundFirepower));
22
+ }
23
+ if (this.antiAirPower > 0 &&
24
+ threatCache.totalOffensiveAirThreat > threatCache.totalAvailableAntiAirFirepower) {
25
+ priority +=
26
+ this.basePriority *
27
+ (threatCache.totalOffensiveAirThreat / Math.max(1, threatCache.totalAvailableAntiAirFirepower));
28
+ }
29
+ // sqrt so we don't build too much of one unit type.
30
+ priority += Math.min(1.0, Math.max(1, Math.sqrt(threatCache.totalAvailableAirPower / Math.max(1, threatCache.totalOffensiveAntiAirThreat))));
31
+ return this.baseAmount * priority;
32
+ }
33
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
34
+ return this.basePriority * (1.0 - numOwned / this.baseAmount);
35
+ }
36
+ getMaxCount(game, playerData, technoRules, threatCache) {
37
+ return null;
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ import { getDefaultPlacementLocation, numBuildingsOwnedOfType } from "./building.js";
2
+ export class BasicBuilding {
3
+ constructor(basePriority, maxNeeded, onlyBuildWhenFloatingCreditsAmount) {
4
+ this.basePriority = basePriority;
5
+ this.maxNeeded = maxNeeded;
6
+ this.onlyBuildWhenFloatingCreditsAmount = onlyBuildWhenFloatingCreditsAmount;
7
+ }
8
+ getPlacementLocation(game, playerData, technoRules) {
9
+ return getDefaultPlacementLocation(game, playerData, playerData.startLocation, technoRules);
10
+ }
11
+ getPriority(game, playerData, technoRules, threatCache) {
12
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
13
+ const calcMaxCount = this.getMaxCount(game, playerData, technoRules, threatCache);
14
+ if (numOwned >= (calcMaxCount ?? this.maxNeeded)) {
15
+ return -100;
16
+ }
17
+ if (this.onlyBuildWhenFloatingCreditsAmount && playerData.credits < this.onlyBuildWhenFloatingCreditsAmount) {
18
+ return -100;
19
+ }
20
+ return this.basePriority * (1.0 - numOwned / this.maxNeeded);
21
+ }
22
+ getMaxCount(game, playerData, technoRules, threatCache) {
23
+ return this.maxNeeded;
24
+ }
25
+ }
@@ -0,0 +1,57 @@
1
+ import { numBuildingsOwnedOfType } from "./building.js";
2
+ export class BasicGroundUnit {
3
+ constructor(basePriority, baseAmount, antiGroundPower = 1, // boolean for now, but will eventually be used in weighting.
4
+ antiAirPower = 0) {
5
+ this.basePriority = basePriority;
6
+ this.baseAmount = baseAmount;
7
+ this.antiGroundPower = antiGroundPower;
8
+ this.antiAirPower = antiAirPower;
9
+ }
10
+ getPlacementLocation(game, playerData, technoRules) {
11
+ return undefined;
12
+ }
13
+ getPriority(game, playerData, technoRules, threatCache) {
14
+ if (threatCache) {
15
+ let priority = 1;
16
+ if (this.antiGroundPower > 0) {
17
+ // If the enemy's power is increasing we should try to keep up.
18
+ if (threatCache.totalOffensiveLandThreat > threatCache.totalAvailableAntiGroundFirepower) {
19
+ priority +=
20
+ this.antiGroundPower *
21
+ this.basePriority *
22
+ (threatCache.totalOffensiveLandThreat /
23
+ Math.max(1, threatCache.totalAvailableAntiGroundFirepower));
24
+ }
25
+ else {
26
+ // But also, if our power dwarfs the enemy, keep pressing the advantage.
27
+ priority +=
28
+ this.antiGroundPower *
29
+ this.basePriority *
30
+ Math.sqrt(threatCache.totalAvailableAntiGroundFirepower /
31
+ Math.max(1, threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat));
32
+ }
33
+ }
34
+ if (this.antiAirPower > 0) {
35
+ if (threatCache.totalOffensiveAirThreat > threatCache.totalAvailableAntiAirFirepower) {
36
+ priority +=
37
+ this.antiAirPower *
38
+ this.basePriority *
39
+ (threatCache.totalOffensiveAirThreat / Math.max(1, threatCache.totalAvailableAntiAirFirepower));
40
+ }
41
+ else {
42
+ priority +=
43
+ this.antiAirPower *
44
+ this.basePriority *
45
+ Math.sqrt(threatCache.totalAvailableAntiAirFirepower /
46
+ Math.max(1, threatCache.totalOffensiveAirThreat + threatCache.totalDefensiveThreat));
47
+ }
48
+ }
49
+ return priority;
50
+ }
51
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
52
+ return this.basePriority * (1.0 - numOwned / this.baseAmount);
53
+ }
54
+ getMaxCount(game, playerData, technoRules, threatCache) {
55
+ return null;
56
+ }
57
+ }
@@ -0,0 +1,77 @@
1
+ import { AntiGroundStaticDefence } from "./antiGroundStaticDefence.js";
2
+ import { ArtilleryUnit } from "./ArtilleryUnit.js";
3
+ import { BasicAirUnit } from "./basicAirUnit.js";
4
+ import { BasicBuilding } from "./basicBuilding.js";
5
+ import { BasicGroundUnit } from "./basicGroundUnit.js";
6
+ import { PowerPlant } from "./powerPlant.js";
7
+ import { ResourceCollectionBuilding } from "./resourceCollectionBuilding.js";
8
+ import { Harvester } from "./harvester.js";
9
+ export function numBuildingsOwnedOfType(game, playerData, technoRules) {
10
+ return game.getVisibleUnits(playerData.name, "self", (r) => r == technoRules).length;
11
+ }
12
+ export function numBuildingsOwnedOfName(game, playerData, name) {
13
+ return game.getVisibleUnits(playerData.name, "self", (r) => r.name === name).length;
14
+ }
15
+ export function getDefaultPlacementLocation(game, playerData, startPoint, technoRules, space = 1) {
16
+ // Random location, preferably near start location.
17
+ let startX = startPoint.x;
18
+ let startY = startPoint.y;
19
+ let size = game.getBuildingPlacementData(technoRules.name);
20
+ if (!size) {
21
+ return undefined;
22
+ }
23
+ let largestSize = Math.max(size.foundation.height, size.foundation.width);
24
+ for (let searchRadius = largestSize; searchRadius < 25 + largestSize; ++searchRadius) {
25
+ for (let xx = startX - searchRadius; xx < startX + searchRadius; ++xx) {
26
+ for (let yy = startY - searchRadius; yy < startY + searchRadius; ++yy) {
27
+ let tile = game.mapApi.getTile(xx, yy);
28
+ if (tile && game.canPlaceBuilding(playerData.name, technoRules.name, tile)) {
29
+ return { rx: xx, ry: yy };
30
+ }
31
+ }
32
+ }
33
+ }
34
+ console.log("Can't find a place to put the " + technoRules.name);
35
+ return undefined;
36
+ }
37
+ export const DEFAULT_BUILDING_PRIORITY = 1;
38
+ export const BUILDING_NAME_TO_RULES = new Map([
39
+ // Allied
40
+ ["GAPOWR", new PowerPlant()],
41
+ ["GAREFN", new ResourceCollectionBuilding(10, 3)],
42
+ ["GAWEAP", new BasicBuilding(15, 1)],
43
+ ["GAPILE", new BasicBuilding(12, 1)],
44
+ ["CMIN", new Harvester(15, 4, 2)],
45
+ ["ENGINEER", new BasicBuilding(1, 1, 10000)],
46
+ ["GADEPT", new BasicBuilding(1, 1, 10000)],
47
+ ["GAAIRC", new BasicBuilding(8, 1, 6000)],
48
+ ["GAPILL", new AntiGroundStaticDefence(5, 1, 5)],
49
+ ["ATESLA", new AntiGroundStaticDefence(5, 1, 10)],
50
+ ["GAWALL", new AntiGroundStaticDefence(0, 0, 0)],
51
+ ["E1", new BasicGroundUnit(5, 3, 0.25, 0)],
52
+ ["MTNK", new BasicGroundUnit(10, 3, 2, 0)],
53
+ ["MGTK", new BasicGroundUnit(10, 1, 2.5, 0)],
54
+ ["FV", new BasicGroundUnit(5, 2, 0.5, 1)],
55
+ ["JUMPJET", new BasicAirUnit(10, 1, 1, 1)],
56
+ ["SREF", new ArtilleryUnit(9, 1)],
57
+ ["CLEG", new BasicGroundUnit(0, 0)],
58
+ ["SHAD", new BasicGroundUnit(0, 0)],
59
+ // Soviet
60
+ ["NAPOWR", new PowerPlant()],
61
+ ["NAREFN", new ResourceCollectionBuilding(10, 3)],
62
+ ["NAWEAP", new BasicBuilding(15, 1)],
63
+ ["NAHAND", new BasicBuilding(12, 1)],
64
+ ["HARV", new Harvester(15, 4, 2)],
65
+ ["SENGINEER", new BasicBuilding(1, 1, 10000)],
66
+ ["NADEPT", new BasicBuilding(1, 1, 10000)],
67
+ ["NARADR", new BasicBuilding(8, 1, 4000)],
68
+ ["NALASR", new AntiGroundStaticDefence(5, 1, 5)],
69
+ ["TESLA", new AntiGroundStaticDefence(5, 1, 10)],
70
+ ["NAWALL", new AntiGroundStaticDefence(0, 0, 0)],
71
+ ["E2", new BasicGroundUnit(5, 3, 0.25, 0)],
72
+ ["HTNK", new BasicGroundUnit(10, 3, 3, 0)],
73
+ ["APOC", new BasicGroundUnit(6, 1, 5, 0)],
74
+ ["HTK", new BasicGroundUnit(5, 2, 0.33, 1.5)],
75
+ ["ZEP", new BasicAirUnit(5, 1, 5, 1)],
76
+ ["V3", new ArtilleryUnit(9, 1)], // V3 Rocket Launcher
77
+ ]);
@@ -0,0 +1,15 @@
1
+ import { BasicGroundUnit } from "./basicGroundUnit.js";
2
+ const IDEAL_HARVESTERS_PER_REFINERY = 2;
3
+ export class Harvester extends BasicGroundUnit {
4
+ constructor(basePriority, baseAmount, minNeeded) {
5
+ super(basePriority, baseAmount, 0, 0);
6
+ this.minNeeded = minNeeded;
7
+ }
8
+ // Priority goes up when we have fewer than this many refineries.
9
+ getPriority(game, playerData, technoRules, threatCache) {
10
+ const refineries = game.getVisibleUnits(playerData.name, "self", (r) => r.refinery).length;
11
+ const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
12
+ const boost = harvesters < this.minNeeded ? 5 : 1;
13
+ return this.basePriority * (refineries / Math.max(harvesters / IDEAL_HARVESTERS_PER_REFINERY, 1)) * boost;
14
+ }
15
+ }
@@ -0,0 +1,20 @@
1
+ import { numBuildingsOwnedOfType } from "./building.js";
2
+ export class MassedAntiGroundUnit {
3
+ constructor(basePriority, baseAmount) {
4
+ this.basePriority = basePriority;
5
+ this.baseAmount = baseAmount;
6
+ }
7
+ getPlacementLocation(game, playerData, technoRules) {
8
+ return undefined;
9
+ }
10
+ getPriority(game, playerData, technoRules, threatCache) {
11
+ // If the enemy's ground power is increasing we should try to keep up.
12
+ if (threatCache) {
13
+ if (threatCache.totalAvailableAntiGroundFirepower * threatCache.certainty > threatCache.totalAvailableAntiGroundFirepower) {
14
+ return this.basePriority * (threatCache.totalAvailableAntiGroundFirepower / Math.max(1, threatCache.totalAvailableAntiGroundFirepower));
15
+ }
16
+ }
17
+ const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
18
+ return this.basePriority * (1.0 - (numOwned / this.baseAmount));
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import { getDefaultPlacementLocation } from "./building.js";
2
+ export class PowerPlant {
3
+ getPlacementLocation(game, playerData, technoRules) {
4
+ return getDefaultPlacementLocation(game, playerData, playerData.startLocation, technoRules);
5
+ }
6
+ getPriority(game, playerData, technoRules) {
7
+ if (playerData.power.total < playerData.power.drain) {
8
+ return 100;
9
+ }
10
+ else if (playerData.power.total < playerData.power.drain + technoRules.power / 2) {
11
+ return 20;
12
+ }
13
+ else {
14
+ return 0;
15
+ }
16
+ }
17
+ getMaxCount(game, playerData, technoRules, threatCache) {
18
+ return null;
19
+ }
20
+ }