@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,130 @@
|
|
|
1
|
+
// A sector is a uniform-sized segment of the map.
|
|
2
|
+
|
|
3
|
+
import { MapApi, PlayerData, Point2D, Tile } from "@chronodivide/game-api";
|
|
4
|
+
import { calculateAreaVisibility } from "./map.js";
|
|
5
|
+
|
|
6
|
+
export const SECTOR_SIZE = 8;
|
|
7
|
+
|
|
8
|
+
export class Sector {
|
|
9
|
+
constructor(
|
|
10
|
+
public sectorStartPoint: Point2D,
|
|
11
|
+
public sectorStartTile: Tile | undefined,
|
|
12
|
+
public sectorVisibilityPct: number | undefined,
|
|
13
|
+
public sectorVisibilityLastCheckTick: number | undefined,
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class SectorCache {
|
|
18
|
+
private sectors: Sector[][] = [];
|
|
19
|
+
private mapBounds: Point2D;
|
|
20
|
+
private sectorsX: number;
|
|
21
|
+
private sectorsY: number;
|
|
22
|
+
private lastUpdatedSectorX: number | undefined;
|
|
23
|
+
private lastUpdatedSectorY: number | undefined;
|
|
24
|
+
|
|
25
|
+
constructor(mapApi: MapApi, mapBounds: Point2D) {
|
|
26
|
+
this.mapBounds = mapBounds;
|
|
27
|
+
this.sectorsX = Math.ceil(mapBounds.x / SECTOR_SIZE);
|
|
28
|
+
this.sectorsY = Math.ceil(mapBounds.y / SECTOR_SIZE);
|
|
29
|
+
this.sectors = new Array(this.sectorsX);
|
|
30
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
31
|
+
this.sectors[xx] = new Array(this.sectorsY);
|
|
32
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
33
|
+
this.sectors[xx][yy] = new Sector(
|
|
34
|
+
{ x: xx * SECTOR_SIZE, y: yy * SECTOR_SIZE },
|
|
35
|
+
mapApi.getTile(xx * SECTOR_SIZE, yy * SECTOR_SIZE),
|
|
36
|
+
undefined,
|
|
37
|
+
undefined,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public updateSectors(currentGameTick: number, maxSectorsToUpdate: number, mapApi: MapApi, playerData: PlayerData) {
|
|
44
|
+
let nextSectorX = this.lastUpdatedSectorX ? this.lastUpdatedSectorX + 1 : 0;
|
|
45
|
+
let nextSectorY = this.lastUpdatedSectorY ? this.lastUpdatedSectorY : 0;
|
|
46
|
+
let updatedThisCycle = 0;
|
|
47
|
+
|
|
48
|
+
while (updatedThisCycle < maxSectorsToUpdate) {
|
|
49
|
+
if (nextSectorX >= this.sectorsX) {
|
|
50
|
+
nextSectorX = 0;
|
|
51
|
+
++nextSectorY;
|
|
52
|
+
}
|
|
53
|
+
if (nextSectorY >= this.sectorsY) {
|
|
54
|
+
nextSectorY = 0;
|
|
55
|
+
nextSectorX = 0;
|
|
56
|
+
}
|
|
57
|
+
let sector: Sector | undefined = this.getSector(nextSectorX, nextSectorY);
|
|
58
|
+
if (sector) {
|
|
59
|
+
sector.sectorVisibilityLastCheckTick = currentGameTick;
|
|
60
|
+
let sp = sector.sectorStartPoint;
|
|
61
|
+
let ep = {
|
|
62
|
+
x: sector.sectorStartPoint.x + SECTOR_SIZE,
|
|
63
|
+
y: sector.sectorStartPoint.y + SECTOR_SIZE,
|
|
64
|
+
};
|
|
65
|
+
let visibility = calculateAreaVisibility(mapApi, playerData, sp, ep);
|
|
66
|
+
if (visibility.validTiles > 0) {
|
|
67
|
+
sector.sectorVisibilityPct = visibility.visibleTiles / visibility.validTiles;
|
|
68
|
+
} else {
|
|
69
|
+
sector.sectorVisibilityPct = undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.lastUpdatedSectorX = nextSectorX;
|
|
73
|
+
this.lastUpdatedSectorY = nextSectorY;
|
|
74
|
+
++nextSectorX;
|
|
75
|
+
++updatedThisCycle;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Return % of sectors that are updated.
|
|
80
|
+
public getSectorUpdateRatio(sectorsUpdatedSinceGameTick: number): number {
|
|
81
|
+
let updated = 0,
|
|
82
|
+
total = 0;
|
|
83
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
84
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
85
|
+
let sector: Sector = this.sectors[xx][yy];
|
|
86
|
+
if (
|
|
87
|
+
sector &&
|
|
88
|
+
sector.sectorVisibilityLastCheckTick &&
|
|
89
|
+
sector.sectorVisibilityLastCheckTick >= sectorsUpdatedSinceGameTick
|
|
90
|
+
) {
|
|
91
|
+
++updated;
|
|
92
|
+
}
|
|
93
|
+
++total;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return updated / total;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Return % of tiles that are visible. Returns undefined if we haven't scanned the whole map yet.
|
|
100
|
+
public getOverallVisibility(): number | undefined {
|
|
101
|
+
let visible = 0,
|
|
102
|
+
total = 0;
|
|
103
|
+
for (let xx = 0; xx < this.sectorsX; ++xx) {
|
|
104
|
+
for (let yy = 0; yy < this.sectorsY; ++yy) {
|
|
105
|
+
let sector: Sector = this.sectors[xx][yy];
|
|
106
|
+
|
|
107
|
+
// Undefined visibility.
|
|
108
|
+
if (sector.sectorVisibilityPct != undefined) {
|
|
109
|
+
visible += sector.sectorVisibilityPct;
|
|
110
|
+
total += 1.0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return visible / total;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public getSector(sectorX: number, sectorY: number): Sector | undefined {
|
|
118
|
+
if (sectorX < 0 || sectorX >= this.sectorsX || sectorY < 0 || sectorY >= this.sectorsY) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return this.sectors[sectorX][sectorY];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public getSectorForWorldPosition(x: number, y: number): Sector | undefined {
|
|
125
|
+
if (x < 0 || x >= this.mapBounds.x || y < 0 || y >= this.mapBounds.y) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return this.sectors[Math.floor(x / SECTOR_SIZE)][Math.floor(y / SECTOR_SIZE)];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { Squad } from "../squad/squad.js";
|
|
3
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
4
|
+
import { Mission, MissionAction, MissionActionNoop, MissionFactory } from "./mission.js";
|
|
5
|
+
|
|
6
|
+
// A basic mission requests specific units and does nothing with them. It is not recommended
|
|
7
|
+
// to actually create this in a game as they'll just sit around idle.
|
|
8
|
+
export class BasicMission implements Mission {
|
|
9
|
+
constructor(
|
|
10
|
+
private uniqueName: string,
|
|
11
|
+
private priority: number = 1,
|
|
12
|
+
private squads: Squad[] = [],
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
getUniqueName(): string {
|
|
16
|
+
return this.uniqueName;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isActive(): boolean {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
removeSquad(squad: Squad): void {
|
|
24
|
+
this.squads = this.squads.filter((s) => s != squad);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addSquad(squad: Squad): void {
|
|
28
|
+
if (!this.squads.find((s) => s == squad)) {
|
|
29
|
+
this.squads.push(squad);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getSquads(): Squad[] {
|
|
34
|
+
return this.squads;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): MissionAction {
|
|
38
|
+
return {} as MissionActionNoop;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onSquadAdded(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): void {}
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
3
|
+
import { BasicMission } from "./basicMission.js";
|
|
4
|
+
import { Mission, MissionAction, MissionActionNoop, MissionFactory } from "./mission";
|
|
5
|
+
|
|
6
|
+
export class ExpansionMission extends BasicMission {
|
|
7
|
+
constructor(uniqueName: string, priority: number) {
|
|
8
|
+
super(uniqueName, priority);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat): MissionAction {
|
|
12
|
+
return {} as MissionActionNoop;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ExpansionMissionFactory implements MissionFactory {
|
|
17
|
+
maybeCreateMission(
|
|
18
|
+
gameApi: GameApi,
|
|
19
|
+
playerData: PlayerData,
|
|
20
|
+
threatData: GlobalThreat | undefined,
|
|
21
|
+
existingMissions: Mission[],
|
|
22
|
+
): Mission | undefined {
|
|
23
|
+
return new ExpansionMission("expansion", 10);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { Squad } from "../squad/squad.js";
|
|
3
|
+
import { SquadBehaviour } from "../squad/squadBehaviour.js";
|
|
4
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
5
|
+
import { ExpansionMissionFactory } from "./expansionMission.js";
|
|
6
|
+
|
|
7
|
+
// AI starts Missions based on heuristics, which have one or more squads.
|
|
8
|
+
// Missions can create squads (but squads will disband themselves).
|
|
9
|
+
export interface Mission {
|
|
10
|
+
onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat | undefined): MissionAction;
|
|
11
|
+
|
|
12
|
+
isActive(): boolean;
|
|
13
|
+
|
|
14
|
+
removeSquad(squad: Squad): void;
|
|
15
|
+
|
|
16
|
+
addSquad(squad: Squad): void;
|
|
17
|
+
|
|
18
|
+
getSquads(): Squad[];
|
|
19
|
+
|
|
20
|
+
getUniqueName(): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type MissionActionNoop = {
|
|
24
|
+
type: "noop";
|
|
25
|
+
};
|
|
26
|
+
export type MissionActionCreateSquad = {
|
|
27
|
+
type: "createSquad";
|
|
28
|
+
name: string;
|
|
29
|
+
behaviour: SquadBehaviour;
|
|
30
|
+
};
|
|
31
|
+
export type MissionActionDisband = {
|
|
32
|
+
type: "disband";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MissionAction = MissionActionNoop | MissionActionCreateSquad | MissionActionDisband;
|
|
36
|
+
|
|
37
|
+
export interface MissionFactory {
|
|
38
|
+
// Potentially return a new mission.
|
|
39
|
+
maybeCreateMission(
|
|
40
|
+
gameApi: GameApi,
|
|
41
|
+
playerData: PlayerData,
|
|
42
|
+
threatData: GlobalThreat,
|
|
43
|
+
existingMissions: Mission[],
|
|
44
|
+
): Mission | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const missionFactories = [new ExpansionMissionFactory()];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Meta-controller for forming and controlling squads.
|
|
2
|
+
|
|
3
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
4
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
5
|
+
import { Mission, MissionAction, MissionActionDisband, missionFactories } from "./mission.js";
|
|
6
|
+
|
|
7
|
+
export class MissionController {
|
|
8
|
+
constructor(private missions: Mission[] = []) {}
|
|
9
|
+
|
|
10
|
+
public onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat | undefined) {
|
|
11
|
+
// Remove disbanded missions.
|
|
12
|
+
this.missions = this.missions.filter((missions) => missions.isActive());
|
|
13
|
+
|
|
14
|
+
let missionActions = this.missions.map((mission) => {
|
|
15
|
+
return {
|
|
16
|
+
mission,
|
|
17
|
+
action: mission.onAiUpdate(gameApi, playerData, threatData),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
// Handle disbands and merges.
|
|
21
|
+
const isDisband = (a: MissionAction): a is MissionActionDisband => a.type == "disband";
|
|
22
|
+
let disbandedMissions: Set<string> = new Set();
|
|
23
|
+
missionActions
|
|
24
|
+
.filter((a) => isDisband(a.action))
|
|
25
|
+
.forEach((a) => {
|
|
26
|
+
a.mission.getSquads().forEach((squad) => {
|
|
27
|
+
squad.setMission(undefined);
|
|
28
|
+
});
|
|
29
|
+
disbandedMissions.add(a.mission.getUniqueName());
|
|
30
|
+
});
|
|
31
|
+
// remove disbanded and merged squads.
|
|
32
|
+
this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
|
|
33
|
+
|
|
34
|
+
// Create missions.
|
|
35
|
+
let newMissions: Mission[];
|
|
36
|
+
let missionNames: Set<String> = new Set();
|
|
37
|
+
this.missions.forEach((mission) => missionNames.add(mission.getUniqueName()));
|
|
38
|
+
missionFactories.forEach((missionFactory) => {
|
|
39
|
+
let maybeMission = missionFactory.maybeCreateMission(gameApi, playerData, threatData, this.missions);
|
|
40
|
+
if (maybeMission) {
|
|
41
|
+
if (missionNames.has(maybeMission.getUniqueName())) {
|
|
42
|
+
//console.log(`Rejecting new mission ${maybeMission.getUniqueName()} as another mission exists.`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`Starting new mission ${maybeMission.getUniqueName()}.`);
|
|
45
|
+
this.missions.push(maybeMission);
|
|
46
|
+
missionNames.add(maybeMission.getUniqueName());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { GameApi, PlayerData, SideType, TechnoRules } from "@chronodivide/game-api";
|
|
2
|
+
import { GlobalThreat } from "../../threat/threat.js";
|
|
3
|
+
import { Squad } from "../squad.js";
|
|
4
|
+
import { SquadAction, SquadActionNoop, SquadBehaviour } from "../squadBehaviour.js";
|
|
5
|
+
|
|
6
|
+
// Expansion or initial base.
|
|
7
|
+
export class SquadExpansion implements SquadBehaviour {
|
|
8
|
+
public getDesiredComposition(
|
|
9
|
+
gameApi: GameApi,
|
|
10
|
+
playerData: PlayerData,
|
|
11
|
+
squad: Squad,
|
|
12
|
+
threatData: GlobalThreat | undefined,
|
|
13
|
+
): { unitName: string; priority: number; amount: number }[] {
|
|
14
|
+
// This squad desires an MCV.
|
|
15
|
+
let myMcvName = playerData.country?.side == SideType.GDI ? "AMCV" : "SMCV";
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
unitName: myMcvName,
|
|
19
|
+
priority: 10,
|
|
20
|
+
amount: 1,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public onAiUpdate(
|
|
26
|
+
gameApi: GameApi,
|
|
27
|
+
playerData: PlayerData,
|
|
28
|
+
squad: Squad,
|
|
29
|
+
threatData: GlobalThreat | undefined,
|
|
30
|
+
): SquadAction {
|
|
31
|
+
return {} as SquadActionNoop;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { GameApi, PlayerData, TechnoRules, UnitData } from "@chronodivide/game-api";
|
|
2
|
+
import { Mission } from "../mission/mission.js";
|
|
3
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
4
|
+
import { SquadAction, SquadBehaviour } from "./squadBehaviour.js";
|
|
5
|
+
|
|
6
|
+
export enum SquadLiveness {
|
|
7
|
+
SquadDead,
|
|
8
|
+
SquadActive,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SquadConstructionRequest = {
|
|
12
|
+
squad: Squad;
|
|
13
|
+
unitType: TechnoRules;
|
|
14
|
+
priority: number;
|
|
15
|
+
// quantity: number
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class Squad {
|
|
19
|
+
constructor(
|
|
20
|
+
private name: string,
|
|
21
|
+
private behaviour: SquadBehaviour,
|
|
22
|
+
private mission: Mission | undefined,
|
|
23
|
+
private unitIds: number[] = [],
|
|
24
|
+
private liveness: SquadLiveness = SquadLiveness.SquadActive,
|
|
25
|
+
private lastLivenessUpdateTick: number = 0,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
public getName(): string {
|
|
29
|
+
return this.name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat | undefined): SquadAction {
|
|
33
|
+
this.updateLiveness(gameApi);
|
|
34
|
+
if (this.mission && this.mission.isActive() == false) {
|
|
35
|
+
// Orphaned squad, might get picked up later.
|
|
36
|
+
this.mission.removeSquad(this);
|
|
37
|
+
this.mission = undefined;
|
|
38
|
+
}
|
|
39
|
+
let outcome = this.behaviour.onAiUpdate(gameApi, playerData, this, threatData);
|
|
40
|
+
return outcome;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public getMission(): Mission | undefined {
|
|
44
|
+
return this.mission;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public setMission(mission: Mission | undefined) {
|
|
48
|
+
if (this.mission != undefined && this.mission != mission) {
|
|
49
|
+
this.mission.removeSquad(this);
|
|
50
|
+
}
|
|
51
|
+
this.mission = mission;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public getUnitIds(): number[] {
|
|
55
|
+
return this.unitIds;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public getUnits(gameApi: GameApi): UnitData[] {
|
|
59
|
+
return this.unitIds
|
|
60
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
61
|
+
.filter((unit) => unit != null)
|
|
62
|
+
.map((unit) => unit!);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public getUnitsOfType(gameApi: GameApi, f: (r: UnitData | undefined) => boolean): UnitData[] {
|
|
66
|
+
return this.unitIds
|
|
67
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
68
|
+
.filter(f)
|
|
69
|
+
.map((unit) => unit!);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public removeUnit(unitIdToRemove: number): void {
|
|
73
|
+
this.unitIds = this.unitIds.filter((unitId) => unitId != unitIdToRemove);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public clearUnits(): void {
|
|
77
|
+
this.unitIds = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public addUnit(unitIdToAdd: number): void {
|
|
81
|
+
this.unitIds.push(unitIdToAdd);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private updateLiveness(gameApi: GameApi) {
|
|
85
|
+
this.unitIds = this.unitIds.filter((unitId) => gameApi.getUnitData(unitId));
|
|
86
|
+
this.lastLivenessUpdateTick = gameApi.getCurrentTick();
|
|
87
|
+
if (this.unitIds.length == 0) {
|
|
88
|
+
if (this.liveness == SquadLiveness.SquadActive) {
|
|
89
|
+
this.liveness = SquadLiveness.SquadDead;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public getLiveness() {
|
|
95
|
+
return this.liveness;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { GameApi, PlayerData, TechnoRules } from "@chronodivide/game-api";
|
|
2
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
3
|
+
import { SquadExpansion } from "./behaviours/squadExpansion.js";
|
|
4
|
+
import { Squad } from "./squad.js";
|
|
5
|
+
|
|
6
|
+
export type SquadActionNoop = {
|
|
7
|
+
type: "noop";
|
|
8
|
+
};
|
|
9
|
+
export type SquadActionDisband = {
|
|
10
|
+
type: "disband";
|
|
11
|
+
};
|
|
12
|
+
export type SquadActionMergeInto = {
|
|
13
|
+
type: "mergeInto";
|
|
14
|
+
mergeInto: Squad;
|
|
15
|
+
};
|
|
16
|
+
export type SquadActionClaimUnits = {
|
|
17
|
+
type: "claimUnit";
|
|
18
|
+
unitIds: number[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SquadAction = SquadActionNoop | SquadActionDisband | SquadActionMergeInto | SquadActionClaimUnits;
|
|
22
|
+
|
|
23
|
+
export interface SquadBehaviour {
|
|
24
|
+
onAiUpdate(
|
|
25
|
+
gameApi: GameApi,
|
|
26
|
+
playerData: PlayerData,
|
|
27
|
+
squad: Squad,
|
|
28
|
+
threatData: GlobalThreat | undefined,
|
|
29
|
+
): SquadAction;
|
|
30
|
+
|
|
31
|
+
// State the desired composition of the Squad.
|
|
32
|
+
getDesiredComposition(
|
|
33
|
+
gameApi: GameApi,
|
|
34
|
+
playerData: PlayerData,
|
|
35
|
+
squad: Squad,
|
|
36
|
+
threatData: GlobalThreat | undefined,
|
|
37
|
+
): { unitName: string; priority: number; amount: number }[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const allSquadBehaviours: SquadBehaviour[] = [
|
|
41
|
+
//new SquadScouters(),
|
|
42
|
+
new SquadExpansion(),
|
|
43
|
+
];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Meta-controller for forming and controlling squads.
|
|
2
|
+
|
|
3
|
+
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
4
|
+
import { GlobalThreat } from "../threat/threat.js";
|
|
5
|
+
import { Squad, SquadLiveness } from "./squad.js";
|
|
6
|
+
import { SquadAction, SquadActionDisband, SquadActionMergeInto } from "./squadBehaviour.js";
|
|
7
|
+
|
|
8
|
+
export class SquadController {
|
|
9
|
+
constructor(
|
|
10
|
+
private squads: Squad[] = [],
|
|
11
|
+
private unitIdToSquad: Map<number, Squad> = new Map(),
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
public onAiUpdate(gameApi: GameApi, playerData: PlayerData, threatData: GlobalThreat | undefined) {
|
|
15
|
+
// Remove dead squads.
|
|
16
|
+
this.squads = this.squads.filter((squad) => squad.getLiveness() == SquadLiveness.SquadDead);
|
|
17
|
+
this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
18
|
+
|
|
19
|
+
// Check for units in multiple squads, this shouldn't happen.
|
|
20
|
+
this.unitIdToSquad = new Map();
|
|
21
|
+
this.squads.forEach((squad) => {
|
|
22
|
+
squad.getUnitIds().forEach((unitId) => {
|
|
23
|
+
if (this.unitIdToSquad.has(unitId)) {
|
|
24
|
+
console.log(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
25
|
+
} else {
|
|
26
|
+
this.unitIdToSquad.set(unitId, squad);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let squadActions = this.squads.map((squad) => {
|
|
32
|
+
return {
|
|
33
|
+
squad,
|
|
34
|
+
action: squad.onAiUpdate(gameApi, playerData, threatData),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
// Handle disbands and merges.
|
|
38
|
+
const isDisband = (a: SquadAction): a is SquadActionDisband => a.type == "disband";
|
|
39
|
+
const isMerge = (a: SquadAction): a is SquadActionMergeInto => a.type == "mergeInto";
|
|
40
|
+
let disbandedSquads: Set<string> = new Set();
|
|
41
|
+
squadActions
|
|
42
|
+
.filter((a) => isDisband(a.action))
|
|
43
|
+
.forEach((a) => {
|
|
44
|
+
a.squad.getUnitIds().forEach((unitId) => {
|
|
45
|
+
this.unitIdToSquad.delete(unitId);
|
|
46
|
+
});
|
|
47
|
+
a.squad.clearUnits();
|
|
48
|
+
disbandedSquads.add(a.squad.getName());
|
|
49
|
+
});
|
|
50
|
+
squadActions
|
|
51
|
+
.filter((a) => isMerge(a.action))
|
|
52
|
+
.forEach((a) => {
|
|
53
|
+
let mergeInto = a.action as SquadActionMergeInto;
|
|
54
|
+
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
55
|
+
console.log("Merging into a disbanded squad, cancelling.");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
|
|
59
|
+
disbandedSquads.add(a.squad.getName());
|
|
60
|
+
});
|
|
61
|
+
// remove disbanded and merged squads.
|
|
62
|
+
this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
63
|
+
|
|
64
|
+
// Form squads.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A periodically-refreshed cache of known threats to a bot so we can use it in decision making.
|
|
2
|
+
|
|
3
|
+
export class GlobalThreat {
|
|
4
|
+
constructor(
|
|
5
|
+
public certainty: number, // 0.0 - 1.0 based on approximate visibility around the map.
|
|
6
|
+
public totalOffensiveLandThreat: number, // a number that approximates how much land-based firepower our opponents have.
|
|
7
|
+
public totalOffensiveAirThreat: number, // a number that approximates how much airborne firepower our opponents have.
|
|
8
|
+
public totalOffensiveAntiAirThreat: number, // a number that approximates how much anti-air firepower our opponents have.
|
|
9
|
+
public totalDefensiveThreat: number, // a number that approximates how much defensive power our opponents have.
|
|
10
|
+
public totalDefensivePower: number, // a number that approximates how much defensive power we have.
|
|
11
|
+
public totalAvailableAntiGroundFirepower: number, // how much anti-ground power we have
|
|
12
|
+
public totalAvailableAntiAirFirepower: number, // how much anti-air power we have
|
|
13
|
+
public totalAvailableAirPower: number, // how much firepower we have in air units
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { GameApi, MovementZone, ObjectType, PlayerData, UnitData } from "@chronodivide/game-api";
|
|
2
|
+
import { GlobalThreat } from "./threat.js";
|
|
3
|
+
|
|
4
|
+
export function calculateGlobalThreat(game: GameApi, playerData: PlayerData, visibleAreaPercent: number): GlobalThreat {
|
|
5
|
+
let groundUnits = game.getVisibleUnits(
|
|
6
|
+
playerData.name,
|
|
7
|
+
"hostile",
|
|
8
|
+
(r) => r.type == ObjectType.Vehicle || r.type == ObjectType.Infantry,
|
|
9
|
+
);
|
|
10
|
+
let airUnits = game.getVisibleUnits(playerData.name, "hostile", (r) => r.movementZone == MovementZone.Fly);
|
|
11
|
+
let groundDefence = game
|
|
12
|
+
.getVisibleUnits(playerData.name, "hostile", (r) => r.type == ObjectType.Building)
|
|
13
|
+
.filter((unitId) => isAntiGround(game, unitId));
|
|
14
|
+
let antiAirPower = game
|
|
15
|
+
.getVisibleUnits(playerData.name, "hostile", (r) => r.type != ObjectType.Building)
|
|
16
|
+
.filter((unitId) => isAntiAir(game, unitId));
|
|
17
|
+
|
|
18
|
+
let ourAntiGroundUnits = game
|
|
19
|
+
.getVisibleUnits(playerData.name, "self", (r) => r.isSelectableCombatant)
|
|
20
|
+
.filter((unitId) => isAntiGround(game, unitId));
|
|
21
|
+
let ourAntiAirUnits = game
|
|
22
|
+
.getVisibleUnits(playerData.name, "self", (r) => r.isSelectableCombatant)
|
|
23
|
+
.filter((unitId) => isAntiAir(game, unitId));
|
|
24
|
+
let ourGroundDefence = game
|
|
25
|
+
.getVisibleUnits(playerData.name, "self", (r) => r.type == ObjectType.Building)
|
|
26
|
+
.filter((unitId) => isAntiGround(game, unitId));
|
|
27
|
+
let ourAirUnits = game.getVisibleUnits(
|
|
28
|
+
playerData.name,
|
|
29
|
+
"self",
|
|
30
|
+
(r) => r.movementZone == MovementZone.Fly && r.isSelectableCombatant,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
let observedGroundThreat = calculateFirepowerForUnits(game, groundUnits);
|
|
34
|
+
let observedAirThreat = calculateFirepowerForUnits(game, airUnits);
|
|
35
|
+
let observedAntiAirThreat = calculateFirepowerForUnits(game, antiAirPower);
|
|
36
|
+
let observedGroundDefence = calculateFirepowerForUnits(game, groundDefence);
|
|
37
|
+
|
|
38
|
+
let ourAntiGroundPower = calculateFirepowerForUnits(game, ourAntiGroundUnits);
|
|
39
|
+
let ourAntiAirPower = calculateFirepowerForUnits(game, ourAntiAirUnits);
|
|
40
|
+
let ourAirPower = calculateFirepowerForUnits(game, ourAirUnits);
|
|
41
|
+
let ourGroundDefencePower = calculateFirepowerForUnits(game, ourGroundDefence);
|
|
42
|
+
|
|
43
|
+
return new GlobalThreat(
|
|
44
|
+
visibleAreaPercent,
|
|
45
|
+
observedGroundThreat,
|
|
46
|
+
observedAirThreat,
|
|
47
|
+
observedAntiAirThreat,
|
|
48
|
+
observedGroundDefence * 0.25,
|
|
49
|
+
ourGroundDefencePower * 0.25,
|
|
50
|
+
ourAntiGroundPower,
|
|
51
|
+
ourAntiAirPower,
|
|
52
|
+
ourAirPower,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isAntiGround(gameApi: GameApi, unitId: number): boolean {
|
|
57
|
+
let unit = gameApi.getUnitData(unitId);
|
|
58
|
+
if (unit && unit.primaryWeapon) {
|
|
59
|
+
return unit.primaryWeapon.projectileRules.isAntiGround;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isAntiAir(gameApi: GameApi, unitId: number): boolean {
|
|
65
|
+
let unit = gameApi.getUnitData(unitId);
|
|
66
|
+
if (unit && unit.primaryWeapon) {
|
|
67
|
+
return unit.primaryWeapon.projectileRules.isAntiAir;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function calculateFirepowerForUnit(unitData: UnitData): number {
|
|
73
|
+
let threat = 0;
|
|
74
|
+
let hpRatio = unitData.hitPoints / Math.max(1, unitData.maxHitPoints);
|
|
75
|
+
if (unitData.primaryWeapon) {
|
|
76
|
+
threat +=
|
|
77
|
+
(hpRatio *
|
|
78
|
+
((unitData.primaryWeapon.rules.damage + 1) * Math.sqrt(unitData.primaryWeapon.rules.range + 1))) /
|
|
79
|
+
Math.max(unitData.primaryWeapon.cooldownTicks, 1);
|
|
80
|
+
}
|
|
81
|
+
if (unitData.secondaryWeapon) {
|
|
82
|
+
threat +=
|
|
83
|
+
(hpRatio *
|
|
84
|
+
((unitData.secondaryWeapon.rules.damage + 1) * Math.sqrt(unitData.secondaryWeapon.rules.range + 1))) /
|
|
85
|
+
Math.max(unitData.secondaryWeapon.cooldownTicks, 1);
|
|
86
|
+
}
|
|
87
|
+
return Math.min(800, threat);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function calculateFirepowerForUnits(game: GameApi, unitIds: number[]) {
|
|
91
|
+
let threat = 0;
|
|
92
|
+
unitIds.forEach((unitId) => {
|
|
93
|
+
let unitData = game.getUnitData(unitId);
|
|
94
|
+
if (unitData) {
|
|
95
|
+
threat += calculateFirepowerForUnit(unitData);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return threat;
|
|
99
|
+
}
|