@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,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
|
+
}
|