@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
@@ -0,0 +1,120 @@
1
+ import { BuildingPlacementData, GameApi, PlayerData, Point2D, TechnoRules } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../threat/threat.js";
3
+ import { AntiGroundStaticDefence } from "./antiGroundStaticDefence.js";
4
+ import { ArtilleryUnit } from "./ArtilleryUnit.js";
5
+ import { BasicAirUnit } from "./basicAirUnit.js";
6
+ import { BasicBuilding } from "./basicBuilding.js";
7
+ import { BasicGroundUnit } from "./basicGroundUnit.js";
8
+ import { PowerPlant } from "./powerPlant.js";
9
+ import { ResourceCollectionBuilding } from "./resourceCollectionBuilding.js";
10
+ import { Harvester } from "./harvester.js";
11
+
12
+ export interface AiBuildingRules {
13
+ getPriority(
14
+ game: GameApi,
15
+ playerData: PlayerData,
16
+ technoRules: TechnoRules,
17
+ threatCache: GlobalThreat | undefined
18
+ ): number;
19
+
20
+ getPlacementLocation(
21
+ game: GameApi,
22
+ playerData: PlayerData,
23
+ technoRules: TechnoRules
24
+ ): { rx: number; ry: number } | undefined;
25
+
26
+ getMaxCount(
27
+ game: GameApi,
28
+ playerData: PlayerData,
29
+ technoRules: TechnoRules,
30
+ threatCache: GlobalThreat | undefined
31
+ ): number | null;
32
+ }
33
+
34
+ export function numBuildingsOwnedOfType(game: GameApi, playerData: PlayerData, technoRules: TechnoRules): number {
35
+ return game.getVisibleUnits(playerData.name, "self", (r) => r == technoRules).length;
36
+ }
37
+
38
+ export function numBuildingsOwnedOfName(game: GameApi, playerData: PlayerData, name: string): number {
39
+ return game.getVisibleUnits(playerData.name, "self", (r) => r.name === name).length;
40
+ }
41
+
42
+ export function getDefaultPlacementLocation(
43
+ game: GameApi,
44
+ playerData: PlayerData,
45
+ startPoint: Point2D,
46
+ technoRules: TechnoRules,
47
+ space: number = 1
48
+ ): { rx: number; ry: number } | undefined {
49
+ // Random location, preferably near start location.
50
+ let startX = startPoint.x;
51
+ let startY = startPoint.y;
52
+ let size: BuildingPlacementData = game.getBuildingPlacementData(technoRules.name);
53
+ if (!size) {
54
+ return undefined;
55
+ }
56
+ let largestSize = Math.max(size.foundation.height, size.foundation.width);
57
+ for (let searchRadius = largestSize; searchRadius < 25 + largestSize; ++searchRadius) {
58
+ for (let xx = startX - searchRadius; xx < startX + searchRadius; ++xx) {
59
+ for (let yy = startY - searchRadius; yy < startY + searchRadius; ++yy) {
60
+ let tile = game.mapApi.getTile(xx, yy);
61
+ if (tile && game.canPlaceBuilding(playerData.name, technoRules.name, tile)) {
62
+ return { rx: xx, ry: yy };
63
+ }
64
+ }
65
+ }
66
+ }
67
+ console.log("Can't find a place to put the " + technoRules.name);
68
+ return undefined;
69
+ }
70
+
71
+ // Priority 0 = don't build.
72
+ export type TechnoRulesWithPriority = { unit: TechnoRules; priority: number };
73
+
74
+ export const DEFAULT_BUILDING_PRIORITY = 1;
75
+
76
+ export const BUILDING_NAME_TO_RULES = new Map<string, AiBuildingRules>([
77
+ // Allied
78
+ ["GAPOWR", new PowerPlant()],
79
+ ["GAREFN", new ResourceCollectionBuilding(10, 3)], // Refinery
80
+ ["GAWEAP", new BasicBuilding(15, 1)], // War Factory
81
+ ["GAPILE", new BasicBuilding(12, 1)], // Barracks
82
+ ["CMIN", new Harvester(15, 4, 2)], // Chrono Miner
83
+ ["ENGINEER", new BasicBuilding(1, 1, 10000)], // Engineer
84
+ ["GADEPT", new BasicBuilding(1, 1, 10000)], // Repair Depot
85
+ ["GAAIRC", new BasicBuilding(8, 1, 6000)], // Airforce Command
86
+
87
+ ["GAPILL", new AntiGroundStaticDefence(5, 1, 5)], // Pillbox
88
+ ["ATESLA", new AntiGroundStaticDefence(5, 1, 10)], // Prism Cannon
89
+ ["GAWALL", new AntiGroundStaticDefence(0, 0, 0)], // Walls
90
+
91
+ ["E1", new BasicGroundUnit(5, 3, 0.25, 0)], // GI
92
+ ["MTNK", new BasicGroundUnit(10, 3, 2, 0)], // Grizzly Tank
93
+ ["MGTK", new BasicGroundUnit(10, 1, 2.5, 0)], // Mirage Tank
94
+ ["FV", new BasicGroundUnit(5, 2, 0.5, 1)], // IFV
95
+ ["JUMPJET", new BasicAirUnit(10, 1, 1, 1)], // Rocketeer
96
+ ["SREF", new ArtilleryUnit(9, 1)], // Prism Tank
97
+ ["CLEG", new BasicGroundUnit(0, 0)], // Chrono Legionnaire (Disabled - we don't handle the warped out phase properly and it tends to bug both bots out)
98
+ ["SHAD", new BasicGroundUnit(0, 0)], // Nighthawk (Disabled)
99
+
100
+ // Soviet
101
+ ["NAPOWR", new PowerPlant()],
102
+ ["NAREFN", new ResourceCollectionBuilding(10, 3)], // Refinery
103
+ ["NAWEAP", new BasicBuilding(15, 1)], // War Factory
104
+ ["NAHAND", new BasicBuilding(12, 1)], // Barracks
105
+ ["HARV", new Harvester(15, 4, 2)], // War Miner
106
+ ["SENGINEER", new BasicBuilding(1, 1, 10000)], // Soviet Engineer
107
+ ["NADEPT", new BasicBuilding(1, 1, 10000)], // Repair Depot
108
+ ["NARADR", new BasicBuilding(8, 1, 4000)], // Radar
109
+
110
+ ["NALASR", new AntiGroundStaticDefence(5, 1, 5)], // Sentry Gun
111
+ ["TESLA", new AntiGroundStaticDefence(5, 1, 10)], // Tesla Coil
112
+ ["NAWALL", new AntiGroundStaticDefence(0, 0, 0)], // Walls
113
+
114
+ ["E2", new BasicGroundUnit(5, 3, 0.25, 0)], // Conscript
115
+ ["HTNK", new BasicGroundUnit(10, 3, 3, 0)], // Rhino Tank
116
+ ["APOC", new BasicGroundUnit(6, 1, 5, 0)], // Apocalypse Tank
117
+ ["HTK", new BasicGroundUnit(5, 2, 0.33, 1.5)], // Flak Track
118
+ ["ZEP", new BasicAirUnit(5, 1, 5, 1)], // Kirov
119
+ ["V3", new ArtilleryUnit(9, 1)], // V3 Rocket Launcher
120
+ ]);
@@ -0,0 +1,27 @@
1
+ import { GameApi, PlayerData, Point2D, TechnoRules, Tile } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../threat/threat.js";
3
+ import { BasicBuilding } from "./basicBuilding.js";
4
+ import { BasicGroundUnit } from "./basicGroundUnit.js";
5
+
6
+ const IDEAL_HARVESTERS_PER_REFINERY = 2;
7
+
8
+ export class Harvester extends BasicGroundUnit {
9
+ constructor(basePriority: number, baseAmount: number, private minNeeded: number) {
10
+ super(basePriority, baseAmount, 0, 0);
11
+ }
12
+
13
+ // Priority goes up when we have fewer than this many refineries.
14
+ getPriority(
15
+ game: GameApi,
16
+ playerData: PlayerData,
17
+ technoRules: TechnoRules,
18
+ threatCache: GlobalThreat | undefined
19
+ ): number {
20
+ const refineries = game.getVisibleUnits(playerData.name, "self", (r) => r.refinery).length;
21
+ const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
22
+
23
+ const boost = harvesters < this.minNeeded ? 5 : 1;
24
+
25
+ return this.basePriority * (refineries / Math.max(harvesters / IDEAL_HARVESTERS_PER_REFINERY, 1)) * boost;
26
+ }
27
+ }
@@ -0,0 +1,32 @@
1
+ import { GameApi, PlayerData, TechnoRules } from "@chronodivide/game-api";
2
+ import { AiBuildingRules, getDefaultPlacementLocation } from "./building.js";
3
+ import { GlobalThreat } from "../threat/threat.js";
4
+
5
+ export class PowerPlant implements AiBuildingRules {
6
+ getPlacementLocation(
7
+ game: GameApi,
8
+ playerData: PlayerData,
9
+ technoRules: TechnoRules
10
+ ): { rx: number; ry: number } | undefined {
11
+ return getDefaultPlacementLocation(game, playerData, playerData.startLocation, technoRules);
12
+ }
13
+
14
+ getPriority(game: GameApi, playerData: PlayerData, technoRules: TechnoRules): number {
15
+ if (playerData.power.total < playerData.power.drain) {
16
+ return 100;
17
+ } else if (playerData.power.total < playerData.power.drain + technoRules.power / 2) {
18
+ return 20;
19
+ } else {
20
+ return 0;
21
+ }
22
+ }
23
+
24
+ getMaxCount(
25
+ game: GameApi,
26
+ playerData: PlayerData,
27
+ technoRules: TechnoRules,
28
+ threatCache: GlobalThreat | undefined
29
+ ): number | null {
30
+ return null;
31
+ }
32
+ }
@@ -0,0 +1,255 @@
1
+ import {
2
+ ActionsApi,
3
+ GameApi,
4
+ PlayerData,
5
+ ProductionApi,
6
+ QueueStatus,
7
+ QueueType,
8
+ TechnoRules,
9
+ } from "@chronodivide/game-api";
10
+ import { GlobalThreat } from "../threat/threat";
11
+ import {
12
+ TechnoRulesWithPriority,
13
+ BUILDING_NAME_TO_RULES,
14
+ DEFAULT_BUILDING_PRIORITY,
15
+ getDefaultPlacementLocation,
16
+ } from "./building.js";
17
+
18
+ export const QUEUES = [
19
+ QueueType.Structures,
20
+ QueueType.Armory,
21
+ QueueType.Infantry,
22
+ QueueType.Vehicles,
23
+ QueueType.Aircrafts,
24
+ QueueType.Ships,
25
+ ];
26
+
27
+ export const queueTypeToName = (queue: QueueType) => {
28
+ switch (queue) {
29
+ case QueueType.Structures:
30
+ return "Structures";
31
+ case QueueType.Armory:
32
+ return "Armory";
33
+ case QueueType.Infantry:
34
+ return "Infantry";
35
+ case QueueType.Vehicles:
36
+ return "Vehicles";
37
+ case QueueType.Aircrafts:
38
+ return "Aircrafts";
39
+ case QueueType.Ships:
40
+ return "Ships";
41
+ default:
42
+ return "Unknown";
43
+ }
44
+ };
45
+
46
+ // Repair buildings at this ratio of the maxHitpoints.
47
+ const REPAIR_HITPOINTS_RATIO = 0.9;
48
+
49
+ // Don't repair buildings more often than this.
50
+ const REPAIR_COOLDOWN_TICKS = 15;
51
+
52
+ const DEBUG_BUILD_QUEUES = true;
53
+
54
+ export class QueueController {
55
+ constructor(
56
+ private buildingIdToLastHitpoints: Map<number, number> = new Map(),
57
+ private buildingIdLastRepairedAtTick: Map<number, number> = new Map()
58
+ ) {}
59
+
60
+ public onAiUpdate(
61
+ game: GameApi,
62
+ productionApi: ProductionApi,
63
+ actionsApi: ActionsApi,
64
+ playerData: PlayerData,
65
+ threatCache: GlobalThreat | undefined,
66
+ logger: (message: string) => void
67
+ ) {
68
+ const decisions = QUEUES.map((queueType) => {
69
+ const options = productionApi.getAvailableObjects(queueType);
70
+ return {
71
+ queue: queueType,
72
+ decision: this.getBestOptionForBuilding(game, options, threatCache, playerData, logger),
73
+ };
74
+ }).filter((decision) => decision.decision != null);
75
+ let totalWeightAcrossQueues = decisions
76
+ .map((decision) => decision.decision?.priority!)
77
+ .reduce((pV, cV) => pV + cV, 0);
78
+ let totalCostAcrossQueues = decisions
79
+ .map((decision) => decision.decision?.unit.cost!)
80
+ .reduce((pV, cV) => pV + cV, 0);
81
+
82
+ decisions.forEach((decision) => {
83
+ this.updateBuildQueue(
84
+ game,
85
+ productionApi,
86
+ actionsApi,
87
+ playerData,
88
+ threatCache,
89
+ decision.queue,
90
+ decision.decision,
91
+ totalWeightAcrossQueues,
92
+ totalCostAcrossQueues,
93
+ logger
94
+ );
95
+ });
96
+
97
+ // Repair is simple - just repair everything that's damaged.
98
+ // Unfortunately there doesn't seem to be an API to determine if something is being repaired, so we have to remember it.
99
+ game.getVisibleUnits(playerData.name, "self", (r) => r.repairable).forEach((unitId) => {
100
+ const unit = game.getUnitData(unitId);
101
+ if (!unit || !unit.hitPoints || !unit.maxHitPoints) {
102
+ return;
103
+ }
104
+ const lastKnownHitpoints = this.buildingIdToLastHitpoints.get(unitId) || unit.hitPoints;
105
+ const buildingLastRepairedAt = this.buildingIdLastRepairedAtTick.get(unitId) || 0;
106
+ // Only repair if HP is going down and if we haven't recently repaired it
107
+ if (
108
+ unit.hitPoints <= lastKnownHitpoints &&
109
+ game.getCurrentTick() > buildingLastRepairedAt + REPAIR_COOLDOWN_TICKS &&
110
+ unit.hitPoints < unit.maxHitPoints * REPAIR_HITPOINTS_RATIO
111
+ ) {
112
+ actionsApi.toggleRepairWrench(unitId);
113
+ this.buildingIdLastRepairedAtTick.set(unitId, game.getCurrentTick());
114
+ }
115
+ this.buildingIdToLastHitpoints.set(unitId, unit.hitPoints);
116
+ });
117
+ }
118
+
119
+ private updateBuildQueue(
120
+ game: GameApi,
121
+ productionApi: ProductionApi,
122
+ actionsApi: ActionsApi,
123
+ playerData: PlayerData,
124
+ threatCache: GlobalThreat | undefined,
125
+ queueType: QueueType,
126
+ decision: TechnoRulesWithPriority | undefined,
127
+ totalWeightAcrossQueues: number,
128
+ totalCostAcrossQueues: number,
129
+ logger: (message: string) => void
130
+ ): void {
131
+ const myCredits = playerData.credits;
132
+
133
+ let queueData = productionApi.getQueueData(queueType);
134
+ if (queueData.status == QueueStatus.Idle) {
135
+ // Start building the decided item.
136
+ if (decision !== undefined) {
137
+ logger(`Decision (${queueTypeToName(queueType)}): ${decision.unit.name}`);
138
+ actionsApi.queueForProduction(queueType, decision.unit.name, decision.unit.type, 1);
139
+ }
140
+ } else if (queueData.status == QueueStatus.Ready && queueData.items.length > 0) {
141
+ // Consider placing it.
142
+ const objectReady: TechnoRules = queueData.items[0].rules;
143
+ if (queueType == QueueType.Structures || queueType == QueueType.Armory) {
144
+ logger(`Complete ${queueTypeToName(queueType)}: ${objectReady.name}`);
145
+ let location: { rx: number; ry: number } | undefined = this.getBestLocationForStructure(
146
+ game,
147
+ playerData,
148
+ objectReady
149
+ );
150
+ if (location !== undefined) {
151
+ actionsApi.placeBuilding(objectReady.name, location.rx, location.ry);
152
+ }
153
+ }
154
+ } else if (queueData.status == QueueStatus.Active && queueData.items.length > 0 && decision != null) {
155
+ // Consider cancelling if something else is significantly higher priority.
156
+ const current = queueData.items[0].rules;
157
+ const options = productionApi.getAvailableObjects(queueType);
158
+ if (decision.unit != current) {
159
+ // Changing our mind.
160
+ let currentItemPriority = this.getPriorityForBuildingOption(current, game, playerData, threatCache);
161
+ let newItemPriority = decision.priority;
162
+ if (newItemPriority > currentItemPriority * 2) {
163
+ logger(
164
+ `Dequeueing queue ${queueTypeToName(queueData.type)} unit ${current.name} because ${
165
+ decision.unit.name
166
+ } has 2x higher priority.`
167
+ );
168
+ actionsApi.unqueueFromProduction(queueData.type, current.name, current.type, 1);
169
+ }
170
+ } else {
171
+ // Not changing our mind, but maybe other queues are more important for now.
172
+ if (totalCostAcrossQueues > myCredits && decision.priority < totalWeightAcrossQueues * 0.25) {
173
+ logger(
174
+ `Pausing queue ${queueTypeToName(queueData.type)} because weight is low (${
175
+ decision.priority
176
+ }/${totalWeightAcrossQueues})`
177
+ );
178
+ actionsApi.pauseProduction(queueData.type);
179
+ }
180
+ }
181
+ } else if (queueData.status == QueueStatus.OnHold) {
182
+ // Consider resuming queue if priority is high relative to other queues.
183
+ if (myCredits >= totalCostAcrossQueues) {
184
+ logger(`Resuming queue ${queueTypeToName(queueData.type)} because credits are high`);
185
+ actionsApi.resumeProduction(queueData.type);
186
+ } else if (decision && decision.priority >= totalWeightAcrossQueues * 0.25) {
187
+ logger(
188
+ `Resuming queue ${queueTypeToName(queueData.type)} because weight is high (${
189
+ decision.priority
190
+ }/${totalWeightAcrossQueues})`
191
+ );
192
+ actionsApi.resumeProduction(queueData.type);
193
+ }
194
+ }
195
+ }
196
+
197
+ private getBestOptionForBuilding(
198
+ game: GameApi,
199
+ options: TechnoRules[],
200
+ threatCache: GlobalThreat | undefined,
201
+ playerData: PlayerData,
202
+ logger: (message: string) => void
203
+ ): TechnoRulesWithPriority | undefined {
204
+ let priorityQueue: TechnoRulesWithPriority[] = [];
205
+ options.forEach((option) => {
206
+ let priority = this.getPriorityForBuildingOption(option, game, playerData, threatCache);
207
+ if (priority > 0) {
208
+ priorityQueue.push({ unit: option, priority: priority });
209
+ }
210
+ });
211
+
212
+ priorityQueue = priorityQueue.sort((a, b) => {
213
+ return a.priority - b.priority;
214
+ });
215
+ if (priorityQueue.length > 0) {
216
+ if (DEBUG_BUILD_QUEUES && game.getCurrentTick() % 100 === 0) {
217
+ let queueString = priorityQueue.map((item) => item.unit.name + "(" + item.priority + ")").join(", ");
218
+ logger(`Build priority currently: ${queueString}`);
219
+ }
220
+ }
221
+
222
+ return priorityQueue.pop();
223
+ }
224
+
225
+ private getPriorityForBuildingOption(
226
+ option: TechnoRules,
227
+ game: GameApi,
228
+ playerStatus: PlayerData,
229
+ threatCache: GlobalThreat | undefined
230
+ ) {
231
+ if (BUILDING_NAME_TO_RULES.has(option.name)) {
232
+ let logic = BUILDING_NAME_TO_RULES.get(option.name)!;
233
+ return logic.getPriority(game, playerStatus, option, threatCache);
234
+ } else {
235
+ // Fallback priority when there are no rules.
236
+ return (
237
+ DEFAULT_BUILDING_PRIORITY - game.getVisibleUnits(playerStatus.name, "self", (r) => r == option).length
238
+ );
239
+ }
240
+ }
241
+
242
+ private getBestLocationForStructure(
243
+ game: GameApi,
244
+ playerData: PlayerData,
245
+ objectReady: TechnoRules
246
+ ): { rx: number; ry: number } | undefined {
247
+ if (BUILDING_NAME_TO_RULES.has(objectReady.name)) {
248
+ let logic = BUILDING_NAME_TO_RULES.get(objectReady.name)!;
249
+ return logic.getPlacementLocation(game, playerData, objectReady);
250
+ } else {
251
+ // fallback placement logic
252
+ return getDefaultPlacementLocation(game, playerData, playerData.startLocation, objectReady);
253
+ }
254
+ }
255
+ }
@@ -0,0 +1,56 @@
1
+ import { GameApi, PlayerData, Point2D, TechnoRules, Tile } from "@chronodivide/game-api";
2
+ import { GlobalThreat } from "../threat/threat.js";
3
+ import { BasicBuilding } from "./basicBuilding.js";
4
+ import {
5
+ AiBuildingRules,
6
+ getDefaultPlacementLocation,
7
+ numBuildingsOwnedOfName,
8
+ numBuildingsOwnedOfType,
9
+ } from "./building.js";
10
+
11
+ export class ResourceCollectionBuilding extends BasicBuilding {
12
+ constructor(basePriority: number, maxNeeded: number, onlyBuildWhenFloatingCreditsAmount?: number) {
13
+ super(basePriority, maxNeeded, onlyBuildWhenFloatingCreditsAmount);
14
+ }
15
+
16
+ getPlacementLocation(
17
+ game: GameApi,
18
+ playerData: PlayerData,
19
+ technoRules: TechnoRules
20
+ ): { rx: number; ry: number } | undefined {
21
+ // Prefer spawning close to ore.
22
+ let selectedLocation = playerData.startLocation;
23
+
24
+ var closeOre: Tile | undefined;
25
+ var closeOreDist: number | undefined;
26
+ let allTileResourceData = game.mapApi.getAllTilesResourceData();
27
+ for (let i = 0; i < allTileResourceData.length; ++i) {
28
+ let tileResourceData = allTileResourceData[i];
29
+ if (tileResourceData.spawnsOre) {
30
+ let dist = Math.sqrt(
31
+ (selectedLocation.x - tileResourceData.tile.rx) ** 2 +
32
+ (selectedLocation.y - tileResourceData.tile.ry) ** 2
33
+ );
34
+ if (closeOreDist == undefined || dist < closeOreDist) {
35
+ closeOreDist = dist;
36
+ closeOre = tileResourceData.tile;
37
+ }
38
+ }
39
+ }
40
+ if (closeOre) {
41
+ selectedLocation = { x: closeOre.rx, y: closeOre.ry };
42
+ }
43
+ return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules);
44
+ }
45
+
46
+ // Don't build/start selling these if we don't have any harvesters
47
+ getMaxCount(
48
+ game: GameApi,
49
+ playerData: PlayerData,
50
+ technoRules: TechnoRules,
51
+ threatCache: GlobalThreat | undefined
52
+ ): number | null {
53
+ const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
54
+ return Math.max(1, harvesters * 2);
55
+ }
56
+ }
@@ -0,0 +1,76 @@
1
+ import { GameApi, MapApi, PlayerData, Point2D } from "@chronodivide/game-api";
2
+
3
+ // Expensive one-time call to determine the size of the map.
4
+ // The result is a point just outside the bounds of the map.
5
+ export function determineMapBounds(mapApi: MapApi): Point2D {
6
+ // TODO Binary Search this.
7
+ // Start from the last spawn positions to save time.
8
+ let maxX: number = 0;
9
+ let maxY: number = 0;
10
+ mapApi.getStartingLocations().forEach((point) => {
11
+ if (point.x > maxX) {
12
+ maxX = point.x;
13
+ }
14
+ if (point.y > maxY) {
15
+ maxY = point.y;
16
+ }
17
+ });
18
+ // Expand outwards until we find the bounds.
19
+ for (let testX = maxX; testX < 10000; ++testX) {
20
+ if (mapApi.getTile(testX, 0) == undefined) {
21
+ maxX = testX;
22
+ break;
23
+ }
24
+ }
25
+ for (let testY = maxY; testY < 10000; ++testY) {
26
+ if (mapApi.getTile(testY, 0) == undefined) {
27
+ maxY = testY;
28
+ break;
29
+ }
30
+ }
31
+ return { x: maxX, y: maxY };
32
+ }
33
+
34
+ export function calculateAreaVisibility(
35
+ mapApi: MapApi,
36
+ playerData: PlayerData,
37
+ startPoint: Point2D,
38
+ endPoint: Point2D,
39
+ ): { visibleTiles: number; validTiles: number } {
40
+ let validTiles: number = 0,
41
+ visibleTiles: number = 0;
42
+ for (let xx = startPoint.x; xx < endPoint.x; ++xx) {
43
+ for (let yy = startPoint.y; yy < endPoint.y; ++yy) {
44
+ let tile = mapApi.getTile(xx, yy);
45
+ if (tile) {
46
+ ++validTiles;
47
+ if (mapApi.isVisibleTile(tile, playerData.name)) {
48
+ ++visibleTiles;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ let result = { visibleTiles, validTiles };
54
+ return result;
55
+ }
56
+
57
+ export function getPointTowardsOtherPoint(
58
+ gameApi: GameApi,
59
+ startLocation: Point2D,
60
+ endLocation: Point2D,
61
+ minRadius: number,
62
+ maxRadius: number,
63
+ randomAngle: number,
64
+ ): Point2D {
65
+ let radius = minRadius + Math.round(gameApi.generateRandom() * (maxRadius - minRadius));
66
+ let directionToSpawn = Math.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);
67
+ let randomisedDirection =
68
+ directionToSpawn - (randomAngle * (Math.PI / 12) + 2 * randomAngle * gameApi.generateRandom() * (Math.PI / 12));
69
+ let candidatePointX = Math.round(startLocation.x + Math.cos(randomisedDirection) * radius);
70
+ let candidatePointY = Math.round(startLocation.y + Math.sin(randomisedDirection) * radius);
71
+ return { x: candidatePointX, y: candidatePointY };
72
+ }
73
+
74
+ export function getDistanceBetweenPoints(startLocation: Point2D, endLocation: Point2D): number {
75
+ return Math.sqrt((startLocation.x - endLocation.x) ** 2 + (startLocation.y - endLocation.y) ** 2);
76
+ }