@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.
- package/.prettierrc +5 -0
- package/README.md +46 -0
- package/dist/bot/bot.js +269 -0
- package/dist/bot/logic/building/ArtilleryUnit.js +24 -0
- package/dist/bot/logic/building/antiGroundStaticDefence.js +40 -0
- package/dist/bot/logic/building/basicAirUnit.js +39 -0
- package/dist/bot/logic/building/basicBuilding.js +25 -0
- package/dist/bot/logic/building/basicGroundUnit.js +57 -0
- package/dist/bot/logic/building/building.js +77 -0
- package/dist/bot/logic/building/harvester.js +15 -0
- package/dist/bot/logic/building/massedAntiGroundUnit.js +20 -0
- package/dist/bot/logic/building/powerPlant.js +20 -0
- package/dist/bot/logic/building/queueController.js +168 -0
- package/dist/bot/logic/building/queues.js +19 -0
- package/dist/bot/logic/building/resourceCollectionBuilding.js +34 -0
- package/dist/bot/logic/map/map.js +57 -0
- package/dist/bot/logic/map/sector.js +104 -0
- package/dist/bot/logic/mission/basicMission.js +30 -0
- package/dist/bot/logic/mission/expansionMission.js +14 -0
- package/dist/bot/logic/mission/mission.js +2 -0
- package/dist/bot/logic/mission/missionController.js +47 -0
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +18 -0
- package/dist/bot/logic/squad/behaviours/squadScouters.js +8 -0
- package/dist/bot/logic/squad/squad.js +73 -0
- package/dist/bot/logic/squad/squadBehaviour.js +5 -0
- package/dist/bot/logic/squad/squadController.js +58 -0
- package/dist/bot/logic/threat/threat.js +22 -0
- package/dist/bot/logic/threat/threatCalculator.js +72 -0
- package/dist/exampleBot.js +38 -0
- package/package.json +24 -0
- package/rules.ini +23126 -0
- package/src/bot/bot.ts +378 -0
- package/src/bot/logic/building/ArtilleryUnit.ts +43 -0
- package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -0
- package/src/bot/logic/building/basicAirUnit.ts +68 -0
- package/src/bot/logic/building/basicBuilding.ts +47 -0
- package/src/bot/logic/building/basicGroundUnit.ts +78 -0
- package/src/bot/logic/building/building.ts +120 -0
- package/src/bot/logic/building/harvester.ts +27 -0
- package/src/bot/logic/building/powerPlant.ts +32 -0
- package/src/bot/logic/building/queueController.ts +255 -0
- package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -0
- package/src/bot/logic/map/map.ts +76 -0
- package/src/bot/logic/map/sector.ts +130 -0
- package/src/bot/logic/mission/basicMission.ts +42 -0
- package/src/bot/logic/mission/expansionMission.ts +25 -0
- package/src/bot/logic/mission/mission.ts +47 -0
- package/src/bot/logic/mission/missionController.ts +51 -0
- package/src/bot/logic/squad/behaviours/squadExpansion.ts +33 -0
- package/src/bot/logic/squad/squad.ts +97 -0
- package/src/bot/logic/squad/squadBehaviour.ts +43 -0
- package/src/bot/logic/squad/squadController.ts +66 -0
- package/src/bot/logic/threat/threat.ts +15 -0
- package/src/bot/logic/threat/threatCalculator.ts +99 -0
- package/src/exampleBot.ts +44 -0
- package/tsconfig.json +73 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { QueueStatus, QueueType, } from "@chronodivide/game-api";
|
|
2
|
+
import { BUILDING_NAME_TO_RULES, DEFAULT_BUILDING_PRIORITY, getDefaultPlacementLocation, } from "./building.js";
|
|
3
|
+
export const QUEUES = [
|
|
4
|
+
QueueType.Structures,
|
|
5
|
+
QueueType.Armory,
|
|
6
|
+
QueueType.Infantry,
|
|
7
|
+
QueueType.Vehicles,
|
|
8
|
+
QueueType.Aircrafts,
|
|
9
|
+
QueueType.Ships,
|
|
10
|
+
];
|
|
11
|
+
export const queueTypeToName = (queue) => {
|
|
12
|
+
switch (queue) {
|
|
13
|
+
case QueueType.Structures:
|
|
14
|
+
return "Structures";
|
|
15
|
+
case QueueType.Armory:
|
|
16
|
+
return "Armory";
|
|
17
|
+
case QueueType.Infantry:
|
|
18
|
+
return "Infantry";
|
|
19
|
+
case QueueType.Vehicles:
|
|
20
|
+
return "Vehicles";
|
|
21
|
+
case QueueType.Aircrafts:
|
|
22
|
+
return "Aircrafts";
|
|
23
|
+
case QueueType.Ships:
|
|
24
|
+
return "Ships";
|
|
25
|
+
default:
|
|
26
|
+
return "Unknown";
|
|
27
|
+
}
|
|
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
|
+
const DEBUG_BUILD_QUEUES = true;
|
|
34
|
+
export class QueueController {
|
|
35
|
+
constructor(buildingIdToLastHitpoints = new Map(), buildingIdLastRepairedAtTick = new Map()) {
|
|
36
|
+
this.buildingIdToLastHitpoints = buildingIdToLastHitpoints;
|
|
37
|
+
this.buildingIdLastRepairedAtTick = buildingIdLastRepairedAtTick;
|
|
38
|
+
}
|
|
39
|
+
onAiUpdate(game, productionApi, actionsApi, playerData, threatCache, logger) {
|
|
40
|
+
const decisions = QUEUES.map((queueType) => {
|
|
41
|
+
const options = productionApi.getAvailableObjects(queueType);
|
|
42
|
+
return {
|
|
43
|
+
queue: queueType,
|
|
44
|
+
decision: this.getBestOptionForBuilding(game, options, threatCache, playerData, logger),
|
|
45
|
+
};
|
|
46
|
+
}).filter((decision) => decision.decision != null);
|
|
47
|
+
let totalWeightAcrossQueues = decisions
|
|
48
|
+
.map((decision) => decision.decision?.priority)
|
|
49
|
+
.reduce((pV, cV) => pV + cV, 0);
|
|
50
|
+
let totalCostAcrossQueues = decisions
|
|
51
|
+
.map((decision) => decision.decision?.unit.cost)
|
|
52
|
+
.reduce((pV, cV) => pV + cV, 0);
|
|
53
|
+
decisions.forEach((decision) => {
|
|
54
|
+
this.updateBuildQueue(game, productionApi, actionsApi, playerData, threatCache, decision.queue, decision.decision, totalWeightAcrossQueues, totalCostAcrossQueues, logger);
|
|
55
|
+
});
|
|
56
|
+
// 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
|
+
game.getVisibleUnits(playerData.name, "self", (r) => r.repairable).forEach((unitId) => {
|
|
59
|
+
const unit = game.getUnitData(unitId);
|
|
60
|
+
if (!unit || !unit.hitPoints || !unit.maxHitPoints) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
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);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
updateBuildQueue(game, productionApi, actionsApi, playerData, threatCache, queueType, decision, totalWeightAcrossQueues, totalCostAcrossQueues, logger) {
|
|
76
|
+
const myCredits = playerData.credits;
|
|
77
|
+
let queueData = productionApi.getQueueData(queueType);
|
|
78
|
+
if (queueData.status == QueueStatus.Idle) {
|
|
79
|
+
// Start building the decided item.
|
|
80
|
+
if (decision !== undefined) {
|
|
81
|
+
logger(`Decision (${queueTypeToName(queueType)}): ${decision.unit.name}`);
|
|
82
|
+
actionsApi.queueForProduction(queueType, decision.unit.name, decision.unit.type, 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (queueData.status == QueueStatus.Ready && queueData.items.length > 0) {
|
|
86
|
+
// Consider placing it.
|
|
87
|
+
const objectReady = queueData.items[0].rules;
|
|
88
|
+
if (queueType == QueueType.Structures || queueType == QueueType.Armory) {
|
|
89
|
+
logger(`Complete ${queueTypeToName(queueType)}: ${objectReady.name}`);
|
|
90
|
+
let location = this.getBestLocationForStructure(game, playerData, objectReady);
|
|
91
|
+
if (location !== undefined) {
|
|
92
|
+
actionsApi.placeBuilding(objectReady.name, location.rx, location.ry);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (queueData.status == QueueStatus.Active && queueData.items.length > 0 && decision != null) {
|
|
97
|
+
// Consider cancelling if something else is significantly higher priority.
|
|
98
|
+
const current = queueData.items[0].rules;
|
|
99
|
+
const options = productionApi.getAvailableObjects(queueType);
|
|
100
|
+
if (decision.unit != current) {
|
|
101
|
+
// Changing our mind.
|
|
102
|
+
let currentItemPriority = this.getPriorityForBuildingOption(current, game, playerData, threatCache);
|
|
103
|
+
let newItemPriority = decision.priority;
|
|
104
|
+
if (newItemPriority > currentItemPriority * 2) {
|
|
105
|
+
logger(`Dequeueing queue ${queueTypeToName(queueData.type)} unit ${current.name} because ${decision.unit.name} has 2x higher priority.`);
|
|
106
|
+
actionsApi.unqueueFromProduction(queueData.type, current.name, current.type, 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Not changing our mind, but maybe other queues are more important for now.
|
|
111
|
+
if (totalCostAcrossQueues > myCredits && decision.priority < totalWeightAcrossQueues * 0.25) {
|
|
112
|
+
logger(`Pausing queue ${queueTypeToName(queueData.type)} because weight is low (${decision.priority}/${totalWeightAcrossQueues})`);
|
|
113
|
+
actionsApi.pauseProduction(queueData.type);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (queueData.status == QueueStatus.OnHold) {
|
|
118
|
+
// Consider resuming queue if priority is high relative to other queues.
|
|
119
|
+
if (myCredits >= totalCostAcrossQueues) {
|
|
120
|
+
logger(`Resuming queue ${queueTypeToName(queueData.type)} because credits are high`);
|
|
121
|
+
actionsApi.resumeProduction(queueData.type);
|
|
122
|
+
}
|
|
123
|
+
else if (decision && decision.priority >= totalWeightAcrossQueues * 0.25) {
|
|
124
|
+
logger(`Resuming queue ${queueTypeToName(queueData.type)} because weight is high (${decision.priority}/${totalWeightAcrossQueues})`);
|
|
125
|
+
actionsApi.resumeProduction(queueData.type);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
getBestOptionForBuilding(game, options, threatCache, playerData, logger) {
|
|
130
|
+
let priorityQueue = [];
|
|
131
|
+
options.forEach((option) => {
|
|
132
|
+
let priority = this.getPriorityForBuildingOption(option, game, playerData, threatCache);
|
|
133
|
+
if (priority > 0) {
|
|
134
|
+
priorityQueue.push({ unit: option, priority: priority });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
priorityQueue = priorityQueue.sort((a, b) => {
|
|
138
|
+
return a.priority - b.priority;
|
|
139
|
+
});
|
|
140
|
+
if (priorityQueue.length > 0) {
|
|
141
|
+
if (DEBUG_BUILD_QUEUES && game.getCurrentTick() % 100 === 0) {
|
|
142
|
+
let queueString = priorityQueue.map((item) => item.unit.name + "(" + item.priority + ")").join(", ");
|
|
143
|
+
logger(`Build priority currently: ${queueString}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return priorityQueue.pop();
|
|
147
|
+
}
|
|
148
|
+
getPriorityForBuildingOption(option, game, playerStatus, threatCache) {
|
|
149
|
+
if (BUILDING_NAME_TO_RULES.has(option.name)) {
|
|
150
|
+
let logic = BUILDING_NAME_TO_RULES.get(option.name);
|
|
151
|
+
return logic.getPriority(game, playerStatus, option, threatCache);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Fallback priority when there are no rules.
|
|
155
|
+
return (DEFAULT_BUILDING_PRIORITY - game.getVisibleUnits(playerStatus.name, "self", (r) => r == option).length);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
getBestLocationForStructure(game, playerData, objectReady) {
|
|
159
|
+
if (BUILDING_NAME_TO_RULES.has(objectReady.name)) {
|
|
160
|
+
let logic = BUILDING_NAME_TO_RULES.get(objectReady.name);
|
|
161
|
+
return logic.getPlacementLocation(game, playerData, objectReady);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// fallback placement logic
|
|
165
|
+
return getDefaultPlacementLocation(game, playerData, playerData.startLocation, objectReady);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { QueueType } from "@chronodivide/game-api";
|
|
2
|
+
export const queueTypeToName = (queue) => {
|
|
3
|
+
switch (queue) {
|
|
4
|
+
case QueueType.Structures:
|
|
5
|
+
return "Structures";
|
|
6
|
+
case QueueType.Armory:
|
|
7
|
+
return "Armory";
|
|
8
|
+
case QueueType.Infantry:
|
|
9
|
+
return "Infantry";
|
|
10
|
+
case QueueType.Vehicles:
|
|
11
|
+
return "Vehicles";
|
|
12
|
+
case QueueType.Aircrafts:
|
|
13
|
+
return "Aircrafts";
|
|
14
|
+
case QueueType.Ships:
|
|
15
|
+
return "Ships";
|
|
16
|
+
default:
|
|
17
|
+
return "Unknown";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BasicBuilding } from "./basicBuilding.js";
|
|
2
|
+
import { getDefaultPlacementLocation, } from "./building.js";
|
|
3
|
+
export class ResourceCollectionBuilding extends BasicBuilding {
|
|
4
|
+
constructor(basePriority, maxNeeded, onlyBuildWhenFloatingCreditsAmount) {
|
|
5
|
+
super(basePriority, maxNeeded, onlyBuildWhenFloatingCreditsAmount);
|
|
6
|
+
}
|
|
7
|
+
getPlacementLocation(game, playerData, technoRules) {
|
|
8
|
+
// Prefer spawning close to ore.
|
|
9
|
+
let selectedLocation = playerData.startLocation;
|
|
10
|
+
var closeOre;
|
|
11
|
+
var closeOreDist;
|
|
12
|
+
let allTileResourceData = game.mapApi.getAllTilesResourceData();
|
|
13
|
+
for (let i = 0; i < allTileResourceData.length; ++i) {
|
|
14
|
+
let tileResourceData = allTileResourceData[i];
|
|
15
|
+
if (tileResourceData.spawnsOre) {
|
|
16
|
+
let dist = Math.sqrt((selectedLocation.x - tileResourceData.tile.rx) ** 2 +
|
|
17
|
+
(selectedLocation.y - tileResourceData.tile.ry) ** 2);
|
|
18
|
+
if (closeOreDist == undefined || dist < closeOreDist) {
|
|
19
|
+
closeOreDist = dist;
|
|
20
|
+
closeOre = tileResourceData.tile;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (closeOre) {
|
|
25
|
+
selectedLocation = { x: closeOre.rx, y: closeOre.ry };
|
|
26
|
+
}
|
|
27
|
+
return getDefaultPlacementLocation(game, playerData, selectedLocation, technoRules);
|
|
28
|
+
}
|
|
29
|
+
// Don't build/start selling these if we don't have any harvesters
|
|
30
|
+
getMaxCount(game, playerData, technoRules, threatCache) {
|
|
31
|
+
const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
|
|
32
|
+
return Math.max(1, harvesters * 2);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Expensive one-time call to determine the size of the map.
|
|
2
|
+
// The result is a point just outside the bounds of the map.
|
|
3
|
+
export function determineMapBounds(mapApi) {
|
|
4
|
+
// TODO Binary Search this.
|
|
5
|
+
// Start from the last spawn positions to save time.
|
|
6
|
+
let maxX = 0;
|
|
7
|
+
let maxY = 0;
|
|
8
|
+
mapApi.getStartingLocations().forEach((point) => {
|
|
9
|
+
if (point.x > maxX) {
|
|
10
|
+
maxX = point.x;
|
|
11
|
+
}
|
|
12
|
+
if (point.y > maxY) {
|
|
13
|
+
maxY = point.y;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
// Expand outwards until we find the bounds.
|
|
17
|
+
for (let testX = maxX; testX < 10000; ++testX) {
|
|
18
|
+
if (mapApi.getTile(testX, 0) == undefined) {
|
|
19
|
+
maxX = testX;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (let testY = maxY; testY < 10000; ++testY) {
|
|
24
|
+
if (mapApi.getTile(testY, 0) == undefined) {
|
|
25
|
+
maxY = testY;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { x: maxX, y: maxY };
|
|
30
|
+
}
|
|
31
|
+
export function calculateAreaVisibility(mapApi, playerData, startPoint, endPoint) {
|
|
32
|
+
let validTiles = 0, visibleTiles = 0;
|
|
33
|
+
for (let xx = startPoint.x; xx < endPoint.x; ++xx) {
|
|
34
|
+
for (let yy = startPoint.y; yy < endPoint.y; ++yy) {
|
|
35
|
+
let tile = mapApi.getTile(xx, yy);
|
|
36
|
+
if (tile) {
|
|
37
|
+
++validTiles;
|
|
38
|
+
if (mapApi.isVisibleTile(tile, playerData.name)) {
|
|
39
|
+
++visibleTiles;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let result = { visibleTiles, validTiles };
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
export function getPointTowardsOtherPoint(gameApi, startLocation, endLocation, minRadius, maxRadius, randomAngle) {
|
|
48
|
+
let radius = minRadius + Math.round(gameApi.generateRandom() * (maxRadius - minRadius));
|
|
49
|
+
let directionToSpawn = Math.atan2(endLocation.y - startLocation.y, endLocation.x - startLocation.x);
|
|
50
|
+
let randomisedDirection = directionToSpawn - (randomAngle * (Math.PI / 12) + 2 * randomAngle * gameApi.generateRandom() * (Math.PI / 12));
|
|
51
|
+
let candidatePointX = Math.round(startLocation.x + Math.cos(randomisedDirection) * radius);
|
|
52
|
+
let candidatePointY = Math.round(startLocation.y + Math.sin(randomisedDirection) * radius);
|
|
53
|
+
return { x: candidatePointX, y: candidatePointY };
|
|
54
|
+
}
|
|
55
|
+
export function getDistanceBetweenPoints(startLocation, endLocation) {
|
|
56
|
+
return Math.sqrt((startLocation.x - endLocation.x) ** 2 + (startLocation.y - endLocation.y) ** 2);
|
|
57
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// A sector is a uniform-sized segment of the map.
|
|
2
|
+
import { calculateAreaVisibility } from "./map.js";
|
|
3
|
+
export const SECTOR_SIZE = 8;
|
|
4
|
+
export class Sector {
|
|
5
|
+
constructor(sectorStartPoint, sectorStartTile, sectorVisibilityPct, sectorVisibilityLastCheckTick) {
|
|
6
|
+
this.sectorStartPoint = sectorStartPoint;
|
|
7
|
+
this.sectorStartTile = sectorStartTile;
|
|
8
|
+
this.sectorVisibilityPct = sectorVisibilityPct;
|
|
9
|
+
this.sectorVisibilityLastCheckTick = sectorVisibilityLastCheckTick;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class SectorCache {
|
|
13
|
+
constructor(mapApi, mapBounds) {
|
|
14
|
+
this.sectors = [];
|
|
15
|
+
this.mapBounds = mapBounds;
|
|
16
|
+
this.sectorsX = Math.ceil(mapBounds.x / SECTOR_SIZE);
|
|
17
|
+
this.sectorsY = Math.ceil(mapBounds.y / SECTOR_SIZE);
|
|
18
|
+
this.sectors = new Array(this.sectorsX);
|
|
19
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
20
|
+
this.sectors[xx] = new Array(this.sectorsY);
|
|
21
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
22
|
+
this.sectors[xx][yy] = new Sector({ x: xx * SECTOR_SIZE, y: yy * SECTOR_SIZE }, mapApi.getTile(xx * SECTOR_SIZE, yy * SECTOR_SIZE), undefined, undefined);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
updateSectors(currentGameTick, maxSectorsToUpdate, mapApi, playerData) {
|
|
27
|
+
let nextSectorX = this.lastUpdatedSectorX ? this.lastUpdatedSectorX + 1 : 0;
|
|
28
|
+
let nextSectorY = this.lastUpdatedSectorY ? this.lastUpdatedSectorY : 0;
|
|
29
|
+
let updatedThisCycle = 0;
|
|
30
|
+
while (updatedThisCycle < maxSectorsToUpdate) {
|
|
31
|
+
if (nextSectorX >= this.sectorsX) {
|
|
32
|
+
nextSectorX = 0;
|
|
33
|
+
++nextSectorY;
|
|
34
|
+
}
|
|
35
|
+
if (nextSectorY >= this.sectorsY) {
|
|
36
|
+
nextSectorY = 0;
|
|
37
|
+
nextSectorX = 0;
|
|
38
|
+
}
|
|
39
|
+
let sector = this.getSector(nextSectorX, nextSectorY);
|
|
40
|
+
if (sector) {
|
|
41
|
+
sector.sectorVisibilityLastCheckTick = currentGameTick;
|
|
42
|
+
let sp = sector.sectorStartPoint;
|
|
43
|
+
let ep = {
|
|
44
|
+
x: sector.sectorStartPoint.x + SECTOR_SIZE,
|
|
45
|
+
y: sector.sectorStartPoint.y + SECTOR_SIZE,
|
|
46
|
+
};
|
|
47
|
+
let visibility = calculateAreaVisibility(mapApi, playerData, sp, ep);
|
|
48
|
+
if (visibility.validTiles > 0) {
|
|
49
|
+
sector.sectorVisibilityPct = visibility.visibleTiles / visibility.validTiles;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
sector.sectorVisibilityPct = undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.lastUpdatedSectorX = nextSectorX;
|
|
56
|
+
this.lastUpdatedSectorY = nextSectorY;
|
|
57
|
+
++nextSectorX;
|
|
58
|
+
++updatedThisCycle;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Return % of sectors that are updated.
|
|
62
|
+
getSectorUpdateRatio(sectorsUpdatedSinceGameTick) {
|
|
63
|
+
let updated = 0, total = 0;
|
|
64
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
65
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
66
|
+
let sector = this.sectors[xx][yy];
|
|
67
|
+
if (sector &&
|
|
68
|
+
sector.sectorVisibilityLastCheckTick &&
|
|
69
|
+
sector.sectorVisibilityLastCheckTick >= sectorsUpdatedSinceGameTick) {
|
|
70
|
+
++updated;
|
|
71
|
+
}
|
|
72
|
+
++total;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return updated / total;
|
|
76
|
+
}
|
|
77
|
+
// Return % of tiles that are visible. Returns undefined if we haven't scanned the whole map yet.
|
|
78
|
+
getOverallVisibility() {
|
|
79
|
+
let visible = 0, total = 0;
|
|
80
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
81
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
82
|
+
let sector = this.sectors[xx][yy];
|
|
83
|
+
// Undefined visibility.
|
|
84
|
+
if (sector.sectorVisibilityPct != undefined) {
|
|
85
|
+
visible += sector.sectorVisibilityPct;
|
|
86
|
+
total += 1.0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return visible / total;
|
|
91
|
+
}
|
|
92
|
+
getSector(sectorX, sectorY) {
|
|
93
|
+
if (sectorX < 0 || sectorX >= this.sectorsX || sectorY < 0 || sectorY >= this.sectorsY) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return this.sectors[sectorX][sectorY];
|
|
97
|
+
}
|
|
98
|
+
getSectorForWorldPosition(x, y) {
|
|
99
|
+
if (x < 0 || x >= this.mapBounds.x || y < 0 || y >= this.mapBounds.y) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return this.sectors[Math.floor(x / SECTOR_SIZE)][Math.floor(y / SECTOR_SIZE)];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// A basic mission requests specific units and does nothing with them. It is not recommended
|
|
2
|
+
// to actually create this in a game as they'll just sit around idle.
|
|
3
|
+
export class BasicMission {
|
|
4
|
+
constructor(uniqueName, priority = 1, squads = []) {
|
|
5
|
+
this.uniqueName = uniqueName;
|
|
6
|
+
this.priority = priority;
|
|
7
|
+
this.squads = squads;
|
|
8
|
+
}
|
|
9
|
+
getUniqueName() {
|
|
10
|
+
return this.uniqueName;
|
|
11
|
+
}
|
|
12
|
+
isActive() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
removeSquad(squad) {
|
|
16
|
+
this.squads = this.squads.filter((s) => s != squad);
|
|
17
|
+
}
|
|
18
|
+
addSquad(squad) {
|
|
19
|
+
if (!this.squads.find((s) => s == squad)) {
|
|
20
|
+
this.squads.push(squad);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getSquads() {
|
|
24
|
+
return this.squads;
|
|
25
|
+
}
|
|
26
|
+
onAiUpdate(gameApi, playerData, threatData) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
onSquadAdded(gameApi, playerData, threatData) { }
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BasicMission } from "./basicMission.js";
|
|
2
|
+
export class ExpansionMission extends BasicMission {
|
|
3
|
+
constructor(uniqueName, priority) {
|
|
4
|
+
super(uniqueName, priority);
|
|
5
|
+
}
|
|
6
|
+
onAiUpdate(gameApi, playerData, threatData) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ExpansionMissionFactory {
|
|
11
|
+
maybeCreateMission(gameApi, playerData, threatData, existingMissions) {
|
|
12
|
+
return new ExpansionMission("expansion", 10);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Meta-controller for forming and controlling squads.
|
|
2
|
+
import { missionFactories } from "./mission.js";
|
|
3
|
+
export class MissionController {
|
|
4
|
+
constructor(missions = []) {
|
|
5
|
+
this.missions = missions;
|
|
6
|
+
}
|
|
7
|
+
onAiUpdate(gameApi, playerData, threatData) {
|
|
8
|
+
// Remove disbanded missions.
|
|
9
|
+
this.missions = this.missions.filter((missions) => missions.isActive());
|
|
10
|
+
let missionActions = this.missions.map((mission) => {
|
|
11
|
+
return {
|
|
12
|
+
mission,
|
|
13
|
+
action: mission.onAiUpdate(gameApi, playerData, threatData),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
// Handle disbands and merges.
|
|
17
|
+
const isDisband = (a) => a.type == "disband";
|
|
18
|
+
let disbandedMissions = new Set();
|
|
19
|
+
missionActions
|
|
20
|
+
.filter((a) => isDisband(a.action))
|
|
21
|
+
.forEach((a) => {
|
|
22
|
+
a.mission.getSquads().forEach((squad) => {
|
|
23
|
+
squad.setMission(undefined);
|
|
24
|
+
});
|
|
25
|
+
disbandedMissions.add(a.mission.getUniqueName());
|
|
26
|
+
});
|
|
27
|
+
// remove disbanded and merged squads.
|
|
28
|
+
this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
|
|
29
|
+
// Create missions.
|
|
30
|
+
let newMissions;
|
|
31
|
+
let missionNames = new Set();
|
|
32
|
+
this.missions.forEach((mission) => missionNames.add(mission.getUniqueName()));
|
|
33
|
+
missionFactories.forEach((missionFactory) => {
|
|
34
|
+
let maybeMission = missionFactory.maybeCreateMission(gameApi, playerData, threatData, this.missions);
|
|
35
|
+
if (maybeMission) {
|
|
36
|
+
if (missionNames.has(maybeMission.getUniqueName())) {
|
|
37
|
+
//console.log(`Rejecting new mission ${maybeMission.getUniqueName()} as another mission exists.`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(`Starting new mission ${maybeMission.getUniqueName()}.`);
|
|
41
|
+
this.missions.push(maybeMission);
|
|
42
|
+
missionNames.add(maybeMission.getUniqueName());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SideType } from "@chronodivide/game-api";
|
|
2
|
+
// Expansion or initial base.
|
|
3
|
+
export class SquadExpansion {
|
|
4
|
+
getDesiredComposition(gameApi, playerData, squad, threatData) {
|
|
5
|
+
// This squad desires an MCV.
|
|
6
|
+
let myMcvName = playerData.country?.side == SideType.GDI ? "AMCV" : "SMCV";
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
unitName: myMcvName,
|
|
10
|
+
priority: 10,
|
|
11
|
+
amount: 1,
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
onAiUpdate(gameApi, playerData, squad, threatData) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export var SquadLiveness;
|
|
2
|
+
(function (SquadLiveness) {
|
|
3
|
+
SquadLiveness[SquadLiveness["SquadDead"] = 0] = "SquadDead";
|
|
4
|
+
SquadLiveness[SquadLiveness["SquadActive"] = 1] = "SquadActive";
|
|
5
|
+
})(SquadLiveness || (SquadLiveness = {}));
|
|
6
|
+
export class Squad {
|
|
7
|
+
constructor(name, behaviour, mission, unitIds = [], liveness = SquadLiveness.SquadActive, lastLivenessUpdateTick = 0) {
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.behaviour = behaviour;
|
|
10
|
+
this.mission = mission;
|
|
11
|
+
this.unitIds = unitIds;
|
|
12
|
+
this.liveness = liveness;
|
|
13
|
+
this.lastLivenessUpdateTick = lastLivenessUpdateTick;
|
|
14
|
+
}
|
|
15
|
+
getName() {
|
|
16
|
+
return this.name;
|
|
17
|
+
}
|
|
18
|
+
onAiUpdate(gameApi, playerData, threatData) {
|
|
19
|
+
this.updateLiveness(gameApi);
|
|
20
|
+
if (this.mission && this.mission.isActive() == false) {
|
|
21
|
+
// Orphaned squad, might get picked up later.
|
|
22
|
+
this.mission.removeSquad(this);
|
|
23
|
+
this.mission = undefined;
|
|
24
|
+
}
|
|
25
|
+
let outcome = this.behaviour.onAiUpdate(gameApi, playerData, this, threatData);
|
|
26
|
+
return outcome;
|
|
27
|
+
}
|
|
28
|
+
getMission() {
|
|
29
|
+
return this.mission;
|
|
30
|
+
}
|
|
31
|
+
setMission(mission) {
|
|
32
|
+
if (this.mission != undefined && this.mission != mission) {
|
|
33
|
+
this.mission.removeSquad(this);
|
|
34
|
+
}
|
|
35
|
+
this.mission = mission;
|
|
36
|
+
}
|
|
37
|
+
getUnitIds() {
|
|
38
|
+
return this.unitIds;
|
|
39
|
+
}
|
|
40
|
+
getUnits(gameApi) {
|
|
41
|
+
return this.unitIds
|
|
42
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
43
|
+
.filter((unit) => unit != null)
|
|
44
|
+
.map((unit) => unit);
|
|
45
|
+
}
|
|
46
|
+
getUnitsOfType(gameApi, f) {
|
|
47
|
+
return this.unitIds
|
|
48
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
49
|
+
.filter(f)
|
|
50
|
+
.map((unit) => unit);
|
|
51
|
+
}
|
|
52
|
+
removeUnit(unitIdToRemove) {
|
|
53
|
+
this.unitIds = this.unitIds.filter((unitId) => unitId != unitIdToRemove);
|
|
54
|
+
}
|
|
55
|
+
clearUnits() {
|
|
56
|
+
this.unitIds = [];
|
|
57
|
+
}
|
|
58
|
+
addUnit(unitIdToAdd) {
|
|
59
|
+
this.unitIds.push(unitIdToAdd);
|
|
60
|
+
}
|
|
61
|
+
updateLiveness(gameApi) {
|
|
62
|
+
this.unitIds = this.unitIds.filter((unitId) => gameApi.getUnitData(unitId));
|
|
63
|
+
this.lastLivenessUpdateTick = gameApi.getCurrentTick();
|
|
64
|
+
if (this.unitIds.length == 0) {
|
|
65
|
+
if (this.liveness == SquadLiveness.SquadActive) {
|
|
66
|
+
this.liveness = SquadLiveness.SquadDead;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getLiveness() {
|
|
71
|
+
return this.liveness;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Meta-controller for forming and controlling squads.
|
|
2
|
+
import { SquadLiveness } from "./squad.js";
|
|
3
|
+
export class SquadController {
|
|
4
|
+
constructor(squads = [], unitIdToSquad = new Map()) {
|
|
5
|
+
this.squads = squads;
|
|
6
|
+
this.unitIdToSquad = unitIdToSquad;
|
|
7
|
+
}
|
|
8
|
+
onAiUpdate(gameApi, playerData, threatData) {
|
|
9
|
+
// Remove dead squads.
|
|
10
|
+
this.squads = this.squads.filter((squad) => squad.getLiveness() == SquadLiveness.SquadDead);
|
|
11
|
+
this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
12
|
+
// Check for units in multiple squads, this shouldn't happen.
|
|
13
|
+
this.unitIdToSquad = new Map();
|
|
14
|
+
this.squads.forEach((squad) => {
|
|
15
|
+
squad.getUnitIds().forEach((unitId) => {
|
|
16
|
+
if (this.unitIdToSquad.has(unitId)) {
|
|
17
|
+
console.log(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
this.unitIdToSquad.set(unitId, squad);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
let squadActions = this.squads.map((squad) => {
|
|
25
|
+
return {
|
|
26
|
+
squad,
|
|
27
|
+
action: squad.onAiUpdate(gameApi, playerData, threatData),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
// Handle disbands and merges.
|
|
31
|
+
const isDisband = (a) => a.type == "disband";
|
|
32
|
+
const isMerge = (a) => a.type == "mergeInto";
|
|
33
|
+
let disbandedSquads = new Set();
|
|
34
|
+
squadActions
|
|
35
|
+
.filter((a) => isDisband(a.action))
|
|
36
|
+
.forEach((a) => {
|
|
37
|
+
a.squad.getUnitIds().forEach((unitId) => {
|
|
38
|
+
this.unitIdToSquad.delete(unitId);
|
|
39
|
+
});
|
|
40
|
+
a.squad.clearUnits();
|
|
41
|
+
disbandedSquads.add(a.squad.getName());
|
|
42
|
+
});
|
|
43
|
+
squadActions
|
|
44
|
+
.filter((a) => isMerge(a.action))
|
|
45
|
+
.forEach((a) => {
|
|
46
|
+
let mergeInto = a.action;
|
|
47
|
+
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
48
|
+
console.log("Merging into a disbanded squad, cancelling.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
|
|
52
|
+
disbandedSquads.add(a.squad.getName());
|
|
53
|
+
});
|
|
54
|
+
// remove disbanded and merged squads.
|
|
55
|
+
this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
56
|
+
// Form squads.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// A periodically-refreshed cache of known threats to a bot so we can use it in decision making.
|
|
2
|
+
export class GlobalThreat {
|
|
3
|
+
constructor(certainty, // 0.0 - 1.0 based on approximate visibility around the map.
|
|
4
|
+
totalOffensiveLandThreat, // a number that approximates how much land-based firepower our opponents have.
|
|
5
|
+
totalOffensiveAirThreat, // a number that approximates how much airborne firepower our opponents have.
|
|
6
|
+
totalOffensiveAntiAirThreat, // a number that approximates how much anti-air firepower our opponents have.
|
|
7
|
+
totalDefensiveThreat, // a number that approximates how much defensive power our opponents have.
|
|
8
|
+
totalDefensivePower, // a number that approximates how much defensive power we have.
|
|
9
|
+
totalAvailableAntiGroundFirepower, // how much anti-ground power we have
|
|
10
|
+
totalAvailableAntiAirFirepower, // how much anti-air power we have
|
|
11
|
+
totalAvailableAirPower) {
|
|
12
|
+
this.certainty = certainty;
|
|
13
|
+
this.totalOffensiveLandThreat = totalOffensiveLandThreat;
|
|
14
|
+
this.totalOffensiveAirThreat = totalOffensiveAirThreat;
|
|
15
|
+
this.totalOffensiveAntiAirThreat = totalOffensiveAntiAirThreat;
|
|
16
|
+
this.totalDefensiveThreat = totalDefensiveThreat;
|
|
17
|
+
this.totalDefensivePower = totalDefensivePower;
|
|
18
|
+
this.totalAvailableAntiGroundFirepower = totalAvailableAntiGroundFirepower;
|
|
19
|
+
this.totalAvailableAntiAirFirepower = totalAvailableAntiAirFirepower;
|
|
20
|
+
this.totalAvailableAirPower = totalAvailableAirPower;
|
|
21
|
+
}
|
|
22
|
+
}
|