@supalosa/chronodivide-bot 0.4.0 → 0.5.2
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/.env.template +5 -0
- package/README.md +54 -47
- package/dist/bot/bot.js +14 -35
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/logic/awareness.js +13 -8
- package/dist/bot/logic/awareness.js.map +1 -1
- package/dist/bot/logic/building/ArtilleryUnit.js +2 -29
- package/dist/bot/logic/building/antiAirStaticDefence.js +43 -0
- package/dist/bot/logic/building/antiAirStaticDefence.js.map +1 -0
- package/dist/bot/logic/building/antiGroundStaticDefence.js +10 -20
- package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -1
- package/dist/bot/logic/building/artilleryUnit.js.map +1 -1
- package/dist/bot/logic/building/basicAirUnit.js +2 -23
- package/dist/bot/logic/building/basicAirUnit.js.map +1 -1
- package/dist/bot/logic/building/basicBuilding.js +3 -2
- package/dist/bot/logic/building/basicBuilding.js.map +1 -1
- package/dist/bot/logic/building/basicGroundUnit.js +2 -43
- package/dist/bot/logic/building/basicGroundUnit.js.map +1 -1
- package/dist/bot/logic/building/buildingRules.js +15 -9
- package/dist/bot/logic/building/buildingRules.js.map +1 -1
- package/dist/bot/logic/building/common.js +19 -0
- package/dist/bot/logic/building/common.js.map +1 -0
- package/dist/bot/logic/building/harvester.js +2 -1
- package/dist/bot/logic/building/harvester.js.map +1 -1
- package/dist/bot/logic/building/queueController.js +69 -42
- package/dist/bot/logic/building/queueController.js.map +1 -1
- package/dist/bot/logic/common/utils.js +21 -0
- package/dist/bot/logic/common/utils.js.map +1 -1
- package/dist/bot/logic/composition/alliedCompositions.js +13 -0
- package/dist/bot/logic/composition/alliedCompositions.js.map +1 -0
- package/dist/bot/logic/composition/common.js +2 -0
- package/dist/bot/logic/composition/common.js.map +1 -0
- package/dist/bot/logic/composition/sovietCompositions.js +13 -0
- package/dist/bot/logic/composition/sovietCompositions.js.map +1 -0
- package/dist/bot/logic/mission/actionBatcher.js +92 -0
- package/dist/bot/logic/mission/actionBatcher.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/combatSquad.js +124 -0
- package/dist/bot/logic/mission/behaviours/combatSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/common.js +56 -0
- package/dist/bot/logic/mission/behaviours/common.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/engineerSquad.js +39 -0
- package/dist/bot/logic/mission/behaviours/engineerSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/expansionSquad.js +46 -0
- package/dist/bot/logic/mission/behaviours/expansionSquad.js.map +1 -0
- package/dist/bot/logic/mission/behaviours/retreatSquad.js +31 -0
- package/dist/bot/logic/mission/behaviours/retreatSquad.js.map +1 -0
- package/{src/bot/logic/squad/behaviours/scoutingSquad.ts → dist/bot/logic/mission/behaviours/scoutingSquad.js} +27 -51
- package/dist/bot/logic/mission/behaviours/scoutingSquad.js.map +1 -0
- package/dist/bot/logic/mission/mission.js +91 -19
- package/dist/bot/logic/mission/mission.js.map +1 -1
- package/dist/bot/logic/mission/missionController.js +262 -21
- package/dist/bot/logic/mission/missionController.js.map +1 -1
- package/dist/bot/logic/mission/missions/attackMission.js +113 -39
- package/dist/bot/logic/mission/missions/attackMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/basicMission.js +13 -0
- package/dist/bot/logic/mission/missions/basicMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +43 -28
- package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/engineerMission.js +37 -7
- package/dist/bot/logic/mission/missions/engineerMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/expansionMission.js +42 -6
- package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/missionBehaviour.js +2 -0
- package/dist/bot/logic/mission/missions/missionBehaviour.js.map +1 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +31 -5
- package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/scoutingMission.js +103 -6
- package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -1
- package/dist/bot/logic/mission/missions/squads/combatSquad.js +116 -0
- package/dist/bot/logic/mission/missions/squads/combatSquad.js.map +1 -0
- package/dist/bot/logic/mission/missions/squads/common.js +58 -0
- package/dist/bot/logic/mission/missions/squads/common.js.map +1 -0
- package/dist/bot/logic/mission/missions/squads/squad.js +2 -0
- package/dist/bot/logic/mission/missions/squads/squad.js.map +1 -0
- package/dist/bot/logic/squad/squadController.js +6 -2
- package/dist/bot/logic/squad/squadController.js.map +1 -1
- package/dist/bot/logic/threat/threatCalculator.js +9 -9
- package/dist/bot/logic/threat/threatCalculator.js.map +1 -1
- package/dist/exampleBot.js +50 -18
- package/dist/exampleBot.js.map +1 -1
- package/package.json +5 -4
- package/src/bot/bot.ts +19 -51
- package/src/bot/logic/awareness.ts +34 -22
- package/src/bot/logic/building/antiAirStaticDefence.ts +64 -0
- package/src/bot/logic/building/antiGroundStaticDefence.ts +7 -20
- package/src/bot/logic/building/artilleryUnit.ts +2 -28
- package/src/bot/logic/building/basicAirUnit.ts +2 -33
- package/src/bot/logic/building/basicBuilding.ts +8 -6
- package/src/bot/logic/building/basicGroundUnit.ts +2 -46
- package/src/bot/logic/building/buildingRules.ts +15 -9
- package/src/bot/logic/building/common.ts +23 -0
- package/src/bot/logic/building/harvester.ts +2 -1
- package/src/bot/logic/building/queueController.ts +98 -43
- package/src/bot/logic/common/utils.ts +28 -0
- package/src/bot/logic/composition/alliedCompositions.ts +22 -0
- package/src/bot/logic/composition/common.ts +3 -0
- package/src/bot/logic/composition/sovietCompositions.ts +21 -0
- package/src/bot/logic/{squad/behaviours → mission}/actionBatcher.ts +66 -7
- package/src/bot/logic/mission/mission.ts +186 -37
- package/src/bot/logic/mission/missionController.ts +340 -31
- package/src/bot/logic/mission/missionFactories.ts +3 -3
- package/src/bot/logic/mission/missions/attackMission.ts +181 -44
- package/src/bot/logic/mission/missions/defenceMission.ts +72 -45
- package/src/bot/logic/mission/missions/engineerMission.ts +67 -15
- package/src/bot/logic/mission/missions/expansionMission.ts +67 -14
- package/src/bot/logic/mission/missions/retreatMission.ts +50 -6
- package/src/bot/logic/mission/missions/scoutingMission.ts +138 -14
- package/src/bot/logic/{squad/behaviours → mission/missions/squads}/combatSquad.ts +56 -33
- package/src/bot/logic/{squad/behaviours → mission/missions/squads}/common.ts +11 -17
- package/src/bot/logic/mission/missions/squads/squad.ts +19 -0
- package/src/bot/logic/threat/threatCalculator.ts +10 -10
- package/src/exampleBot.ts +56 -24
- package/.prettierrc +0 -5
- package/TODO.md +0 -15
- package/rules.ini +0 -23126
- package/src/bot/logic/mission/missions/oneTimeMission.ts +0 -33
- package/src/bot/logic/squad/behaviours/engineerSquad.ts +0 -58
- package/src/bot/logic/squad/behaviours/expansionSquad.ts +0 -64
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +0 -50
- package/src/bot/logic/squad/squad.ts +0 -165
- package/src/bot/logic/squad/squadBehaviour.ts +0 -66
- package/src/bot/logic/squad/squadBehaviours.ts +0 -8
- package/src/bot/logic/squad/squadController.ts +0 -271
|
@@ -1,20 +1,74 @@
|
|
|
1
|
-
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
-
import {
|
|
3
|
-
import { Mission } from "../mission.js";
|
|
4
|
-
import { ExpansionSquad } from "../../squad/behaviours/expansionSquad.js";
|
|
1
|
+
import { ActionsApi, GameApi, OrderType, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { Mission, MissionAction, disbandMission, noop, requestSpecificUnits, requestUnits } from "../mission.js";
|
|
5
3
|
import { MissionFactory } from "../missionFactories.js";
|
|
6
|
-
import { OneTimeMission } from "./oneTimeMission.js";
|
|
7
4
|
import { MatchAwareness } from "../../awareness.js";
|
|
8
5
|
import { MissionController } from "../missionController.js";
|
|
9
6
|
import { DebugLogger } from "../../common/utils.js";
|
|
7
|
+
import { ActionBatcher } from "../actionBatcher.js";
|
|
8
|
+
|
|
9
|
+
const DEPLOY_COOLDOWN_TICKS = 30;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* A mission that tries to create an MCV (if it doesn't exist) and deploy it somewhere it can be deployed.
|
|
13
13
|
*/
|
|
14
|
-
export class ExpansionMission extends
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export class ExpansionMission extends Mission {
|
|
15
|
+
private hasAttemptedDeployWith: {
|
|
16
|
+
unitId: number;
|
|
17
|
+
gameTick: number;
|
|
18
|
+
} | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
uniqueName: string,
|
|
22
|
+
private priority: number,
|
|
23
|
+
private selectedMcv: number | null,
|
|
24
|
+
logger: DebugLogger,
|
|
25
|
+
) {
|
|
26
|
+
super(uniqueName, logger);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public _onAiUpdate(
|
|
30
|
+
gameApi: GameApi,
|
|
31
|
+
actionsApi: ActionsApi,
|
|
32
|
+
playerData: PlayerData,
|
|
33
|
+
matchAwareness: MatchAwareness,
|
|
34
|
+
actionBatcher: ActionBatcher,
|
|
35
|
+
): MissionAction {
|
|
36
|
+
const mcvTypes = ["AMCV", "SMCV"];
|
|
37
|
+
const mcvs = this.getUnitsOfTypes(gameApi, ...mcvTypes);
|
|
38
|
+
if (mcvs.length === 0) {
|
|
39
|
+
// Perhaps we deployed already (or the unit was destroyed), end the mission.
|
|
40
|
+
if (this.hasAttemptedDeployWith !== null) {
|
|
41
|
+
return disbandMission();
|
|
42
|
+
}
|
|
43
|
+
// We need an mcv!
|
|
44
|
+
if (this.selectedMcv) {
|
|
45
|
+
return requestSpecificUnits([this.selectedMcv], this.priority);
|
|
46
|
+
} else {
|
|
47
|
+
return requestUnits(mcvTypes, this.priority);
|
|
48
|
+
}
|
|
49
|
+
} else if (
|
|
50
|
+
!this.hasAttemptedDeployWith ||
|
|
51
|
+
gameApi.getCurrentTick() > this.hasAttemptedDeployWith.gameTick + DEPLOY_COOLDOWN_TICKS
|
|
52
|
+
) {
|
|
53
|
+
actionsApi.orderUnits(
|
|
54
|
+
mcvs.map((mcv) => mcv.id),
|
|
55
|
+
OrderType.DeploySelected,
|
|
56
|
+
);
|
|
57
|
+
// Add a cooldown to deploy attempts.
|
|
58
|
+
this.hasAttemptedDeployWith = {
|
|
59
|
+
unitId: mcvs[0].id,
|
|
60
|
+
gameTick: gameApi.getCurrentTick(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return noop();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getGlobalDebugText(): string | undefined {
|
|
67
|
+
return `Expand with MCV ${this.selectedMcv}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getPriority() {
|
|
71
|
+
return this.priority;
|
|
18
72
|
}
|
|
19
73
|
}
|
|
20
74
|
|
|
@@ -28,11 +82,11 @@ export class ExpansionMissionFactory implements MissionFactory {
|
|
|
28
82
|
playerData: PlayerData,
|
|
29
83
|
matchAwareness: MatchAwareness,
|
|
30
84
|
missionController: MissionController,
|
|
31
|
-
logger: DebugLogger
|
|
85
|
+
logger: DebugLogger,
|
|
32
86
|
): void {
|
|
33
87
|
// At this point, only expand if we have a loose MCV.
|
|
34
88
|
const mcvs = gameApi.getVisibleUnits(playerData.name, "self", (r) =>
|
|
35
|
-
gameApi.getGeneralRules().baseUnit.includes(r.name)
|
|
89
|
+
gameApi.getGeneralRules().baseUnit.includes(r.name),
|
|
36
90
|
);
|
|
37
91
|
mcvs.forEach((mcv) => {
|
|
38
92
|
missionController.addMission(new ExpansionMission("expand-with-" + mcv, 100, mcv, logger));
|
|
@@ -43,9 +97,8 @@ export class ExpansionMissionFactory implements MissionFactory {
|
|
|
43
97
|
gameApi: GameApi,
|
|
44
98
|
playerData: PlayerData,
|
|
45
99
|
matchAwareness: MatchAwareness,
|
|
46
|
-
failedMission: Mission
|
|
100
|
+
failedMission: Mission<any>,
|
|
47
101
|
failureReason: undefined,
|
|
48
102
|
missionController: MissionController,
|
|
49
|
-
): void {
|
|
50
|
-
}
|
|
103
|
+
): void {}
|
|
51
104
|
}
|
|
@@ -1,10 +1,54 @@
|
|
|
1
|
-
import { OneTimeMission } from "./oneTimeMission.js";
|
|
2
|
-
import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
|
|
3
1
|
import { DebugLogger } from "../../common/utils.js";
|
|
4
|
-
import { Vector2 } from "@chronodivide/game-api";
|
|
2
|
+
import { ActionsApi, GameApi, OrderType, PlayerData, Vector2 } from "@chronodivide/game-api";
|
|
3
|
+
import { Mission, MissionAction, disbandMission, requestSpecificUnits } from "../mission.js";
|
|
4
|
+
import { ActionBatcher } from "../actionBatcher.js";
|
|
5
|
+
import { MatchAwareness } from "../../awareness.js";
|
|
5
6
|
|
|
6
|
-
export class RetreatMission extends
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export class RetreatMission extends Mission {
|
|
8
|
+
private createdAt: number | null = null;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
uniqueName: string,
|
|
12
|
+
private retreatToPoint: Vector2,
|
|
13
|
+
private withUnitIds: number[],
|
|
14
|
+
logger: DebugLogger,
|
|
15
|
+
) {
|
|
16
|
+
super(uniqueName, logger);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public _onAiUpdate(
|
|
20
|
+
gameApi: GameApi,
|
|
21
|
+
actionsApi: ActionsApi,
|
|
22
|
+
playerData: PlayerData,
|
|
23
|
+
matchAwareness: MatchAwareness,
|
|
24
|
+
actionBatcher: ActionBatcher,
|
|
25
|
+
): MissionAction {
|
|
26
|
+
if (!this.createdAt) {
|
|
27
|
+
this.createdAt = gameApi.getCurrentTick();
|
|
28
|
+
}
|
|
29
|
+
if (this.getUnitIds().length > 0) {
|
|
30
|
+
// Only send the order once we have managed to claim some units.
|
|
31
|
+
actionsApi.orderUnits(
|
|
32
|
+
this.getUnitIds(),
|
|
33
|
+
OrderType.AttackMove,
|
|
34
|
+
this.retreatToPoint.x,
|
|
35
|
+
this.retreatToPoint.y,
|
|
36
|
+
);
|
|
37
|
+
return disbandMission();
|
|
38
|
+
}
|
|
39
|
+
if (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240) {
|
|
40
|
+
// Disband automatically after 240 ticks in case we couldn't actually claim any units.
|
|
41
|
+
return disbandMission();
|
|
42
|
+
} else {
|
|
43
|
+
return requestSpecificUnits(this.withUnitIds, 1000);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public getGlobalDebugText(): string | undefined {
|
|
48
|
+
return `retreat with ${this.withUnitIds.length} units`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getPriority() {
|
|
52
|
+
return 100;
|
|
9
53
|
}
|
|
10
54
|
}
|
|
@@ -1,21 +1,138 @@
|
|
|
1
|
-
import { GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
-
import { ScoutingSquad } from "../../squad/behaviours/scoutingSquad.js";
|
|
1
|
+
import { ActionsApi, GameApi, OrderType, PlayerData, Vector2 } from "@chronodivide/game-api";
|
|
3
2
|
import { MissionFactory } from "../missionFactories.js";
|
|
4
|
-
import { OneTimeMission } from "./oneTimeMission.js";
|
|
5
3
|
import { MatchAwareness } from "../../awareness.js";
|
|
6
|
-
import { Mission } from "../mission.js";
|
|
4
|
+
import { Mission, MissionAction, disbandMission, noop, requestUnits } from "../mission.js";
|
|
7
5
|
import { AttackMission } from "./attackMission.js";
|
|
8
6
|
import { MissionController } from "../missionController.js";
|
|
9
|
-
import { getUnseenStartingLocations } from "../../common/scout.js";
|
|
10
7
|
import { DebugLogger } from "../../common/utils.js";
|
|
8
|
+
import { ActionBatcher } from "../actionBatcher.js";
|
|
9
|
+
import { getDistanceBetweenTileAndPoint } from "../../map/map.js";
|
|
10
|
+
import { PrioritisedScoutTarget } from "../../common/scout.js";
|
|
11
|
+
|
|
12
|
+
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
13
|
+
|
|
14
|
+
// Max units to spend on a particular scout target.
|
|
15
|
+
const MAX_ATTEMPTS_PER_TARGET = 5;
|
|
16
|
+
|
|
17
|
+
// Maximum ticks to spend trying to scout a target *without making progress towards it*.
|
|
18
|
+
// Every time a unit gets closer to the target, the timer refreshes.
|
|
19
|
+
const MAX_TICKS_PER_TARGET = 600;
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)
|
|
14
23
|
*/
|
|
15
|
-
export class ScoutingMission extends
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
export class ScoutingMission extends Mission {
|
|
25
|
+
private scoutTarget: Vector2 | null = null;
|
|
26
|
+
private attemptsOnCurrentTarget: number = 0;
|
|
27
|
+
private scoutTargetRefreshedAt: number = 0;
|
|
28
|
+
private lastMoveCommandTick: number = 0;
|
|
29
|
+
private scoutTargetIsPermanent: boolean = false;
|
|
30
|
+
|
|
31
|
+
// Minimum distance from a scout to the target.
|
|
32
|
+
private scoutMinDistance?: number;
|
|
33
|
+
|
|
34
|
+
private hadUnit: boolean = false;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
uniqueName: string,
|
|
38
|
+
private priority: number,
|
|
39
|
+
logger: DebugLogger,
|
|
40
|
+
) {
|
|
41
|
+
super(uniqueName, logger);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public _onAiUpdate(
|
|
45
|
+
gameApi: GameApi,
|
|
46
|
+
actionsApi: ActionsApi,
|
|
47
|
+
playerData: PlayerData,
|
|
48
|
+
matchAwareness: MatchAwareness,
|
|
49
|
+
actionBatcher: ActionBatcher,
|
|
50
|
+
): MissionAction {
|
|
51
|
+
const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
|
|
52
|
+
const scouts = this.getUnitsOfTypes(gameApi, ...scoutNames);
|
|
53
|
+
|
|
54
|
+
if ((matchAwareness.getSectorCache().getOverallVisibility() || 0) > 0.9) {
|
|
55
|
+
return disbandMission();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (scouts.length === 0) {
|
|
59
|
+
// Count the number of times the scout dies trying to uncover the current scoutTarget.
|
|
60
|
+
if (this.scoutTarget && this.hadUnit) {
|
|
61
|
+
this.attemptsOnCurrentTarget++;
|
|
62
|
+
this.hadUnit = false;
|
|
63
|
+
}
|
|
64
|
+
return requestUnits(scoutNames, this.priority);
|
|
65
|
+
} else if (this.scoutTarget) {
|
|
66
|
+
this.hadUnit = true;
|
|
67
|
+
if (!this.scoutTargetIsPermanent) {
|
|
68
|
+
if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {
|
|
69
|
+
this.logger(
|
|
70
|
+
`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,
|
|
71
|
+
);
|
|
72
|
+
this.setScoutTarget(null, 0);
|
|
73
|
+
return noop();
|
|
74
|
+
}
|
|
75
|
+
if (gameApi.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {
|
|
76
|
+
this.logger(
|
|
77
|
+
`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`,
|
|
78
|
+
);
|
|
79
|
+
this.setScoutTarget(null, 0);
|
|
80
|
+
return noop();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const targetTile = gameApi.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);
|
|
84
|
+
if (!targetTile) {
|
|
85
|
+
throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);
|
|
86
|
+
}
|
|
87
|
+
if (gameApi.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {
|
|
88
|
+
this.lastMoveCommandTick = gameApi.getCurrentTick();
|
|
89
|
+
scouts.forEach((unit) => {
|
|
90
|
+
if (this.scoutTarget) {
|
|
91
|
+
actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// Check that a scout is actually moving closer to the target.
|
|
95
|
+
const distances = scouts.map((unit) => getDistanceBetweenTileAndPoint(unit.tile, this.scoutTarget!));
|
|
96
|
+
const newMinDistance = Math.min(...distances);
|
|
97
|
+
if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {
|
|
98
|
+
this.logger(
|
|
99
|
+
`Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,
|
|
100
|
+
);
|
|
101
|
+
this.scoutTargetRefreshedAt = gameApi.getCurrentTick();
|
|
102
|
+
this.scoutMinDistance = newMinDistance;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (gameApi.mapApi.isVisibleTile(targetTile, playerData.name)) {
|
|
106
|
+
this.logger(
|
|
107
|
+
`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`,
|
|
108
|
+
);
|
|
109
|
+
this.setScoutTarget(null, gameApi.getCurrentTick());
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const nextScoutTarget = matchAwareness.getScoutingManager().getNewScoutTarget();
|
|
113
|
+
if (!nextScoutTarget) {
|
|
114
|
+
this.logger(`No more scouting targets available, disbanding.`);
|
|
115
|
+
return disbandMission();
|
|
116
|
+
}
|
|
117
|
+
this.setScoutTarget(nextScoutTarget, gameApi.getCurrentTick());
|
|
118
|
+
}
|
|
119
|
+
return noop();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setScoutTarget(target: PrioritisedScoutTarget | null, currentTick: number) {
|
|
123
|
+
this.attemptsOnCurrentTarget = 0;
|
|
124
|
+
this.scoutTargetRefreshedAt = currentTick;
|
|
125
|
+
this.scoutTarget = target?.asVector2() ?? null;
|
|
126
|
+
this.scoutMinDistance = undefined;
|
|
127
|
+
this.scoutTargetIsPermanent = target?.isPermanent ?? false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public getGlobalDebugText(): string | undefined {
|
|
131
|
+
return "scouting";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public getPriority() {
|
|
135
|
+
return this.priority;
|
|
19
136
|
}
|
|
20
137
|
}
|
|
21
138
|
|
|
@@ -33,7 +150,7 @@ export class ScoutingMissionFactory implements MissionFactory {
|
|
|
33
150
|
playerData: PlayerData,
|
|
34
151
|
matchAwareness: MatchAwareness,
|
|
35
152
|
missionController: MissionController,
|
|
36
|
-
logger: DebugLogger
|
|
153
|
+
logger: DebugLogger,
|
|
37
154
|
): void {
|
|
38
155
|
if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
|
|
39
156
|
return;
|
|
@@ -41,7 +158,7 @@ export class ScoutingMissionFactory implements MissionFactory {
|
|
|
41
158
|
if (!matchAwareness.getScoutingManager().hasScoutTargets()) {
|
|
42
159
|
return;
|
|
43
160
|
}
|
|
44
|
-
if (!missionController.addMission(new ScoutingMission("globalScout",
|
|
161
|
+
if (!missionController.addMission(new ScoutingMission("globalScout", 10, logger))) {
|
|
45
162
|
this.lastScoutAt = gameApi.getCurrentTick();
|
|
46
163
|
}
|
|
47
164
|
}
|
|
@@ -50,13 +167,20 @@ export class ScoutingMissionFactory implements MissionFactory {
|
|
|
50
167
|
gameApi: GameApi,
|
|
51
168
|
playerData: PlayerData,
|
|
52
169
|
matchAwareness: MatchAwareness,
|
|
53
|
-
failedMission: Mission
|
|
170
|
+
failedMission: Mission<any>,
|
|
54
171
|
failureReason: undefined,
|
|
55
172
|
missionController: MissionController,
|
|
56
|
-
logger: DebugLogger
|
|
173
|
+
logger: DebugLogger,
|
|
57
174
|
): void {
|
|
175
|
+
if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!matchAwareness.getScoutingManager().hasScoutTargets()) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
58
181
|
if (failedMission instanceof AttackMission) {
|
|
59
|
-
missionController.addMission(new ScoutingMission("globalScout",
|
|
182
|
+
missionController.addMission(new ScoutingMission("globalScout", 10, logger));
|
|
183
|
+
this.lastScoutAt = gameApi.getCurrentTick();
|
|
60
184
|
}
|
|
61
185
|
}
|
|
62
186
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
ActionsApi,
|
|
3
|
+
AttackState,
|
|
4
|
+
GameApi,
|
|
5
|
+
GameMath,
|
|
6
|
+
MovementZone,
|
|
7
|
+
PlayerData,
|
|
8
|
+
UnitData,
|
|
9
|
+
Vector2,
|
|
10
|
+
} from "@chronodivide/game-api";
|
|
11
|
+
import { MatchAwareness } from "../../../awareness.js";
|
|
5
12
|
import { getAttackWeight, manageAttackMicro, manageMoveMicro } from "./common.js";
|
|
6
|
-
import { DebugLogger, maxBy } from "
|
|
7
|
-
import { ActionBatcher } from "
|
|
13
|
+
import { DebugLogger, isOwnedByNeutral, maxBy, minBy } from "../../../common/utils.js";
|
|
14
|
+
import { ActionBatcher, BatchableAction } from "../../actionBatcher.js";
|
|
15
|
+
import { Squad } from "./squad.js";
|
|
16
|
+
import { Mission, MissionAction, grabCombatants, noop } from "../../mission.js";
|
|
8
17
|
|
|
9
18
|
const TARGET_UPDATE_INTERVAL_TICKS = 10;
|
|
10
|
-
const GRAB_INTERVAL_TICKS = 10;
|
|
11
|
-
|
|
12
|
-
const GRAB_RADIUS = 20;
|
|
13
19
|
|
|
14
20
|
// Units must be in a certain radius of the center of mass before attacking.
|
|
15
21
|
// This scales for number of units in the squad though.
|
|
@@ -20,18 +26,21 @@ const MAX_GATHER_RADIUS = 15;
|
|
|
20
26
|
|
|
21
27
|
const GATHER_RATIO = 10;
|
|
22
28
|
|
|
29
|
+
const ATTACK_SCAN_AREA = 15;
|
|
30
|
+
|
|
23
31
|
enum SquadState {
|
|
24
32
|
Gathering,
|
|
25
33
|
Attacking,
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
export class CombatSquad implements
|
|
29
|
-
private lastGrab: number | null = null;
|
|
36
|
+
export class CombatSquad implements Squad {
|
|
30
37
|
private lastCommand: number | null = null;
|
|
31
38
|
private state = SquadState.Gathering;
|
|
32
39
|
|
|
33
40
|
private debugLastTarget: string | undefined;
|
|
34
41
|
|
|
42
|
+
private lastOrderGiven: { [unitId: number]: BatchableAction } = {};
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
*
|
|
37
46
|
* @param rallyArea the initial location to grab combatants
|
|
@@ -57,21 +66,21 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
57
66
|
actionsApi: ActionsApi,
|
|
58
67
|
actionBatcher: ActionBatcher,
|
|
59
68
|
playerData: PlayerData,
|
|
60
|
-
|
|
69
|
+
mission: Mission<any>,
|
|
61
70
|
matchAwareness: MatchAwareness,
|
|
62
71
|
logger: DebugLogger,
|
|
63
|
-
):
|
|
72
|
+
): MissionAction {
|
|
64
73
|
if (
|
|
65
|
-
|
|
74
|
+
mission.getUnitIds().length > 0 &&
|
|
66
75
|
(!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)
|
|
67
76
|
) {
|
|
68
77
|
this.lastCommand = gameApi.getCurrentTick();
|
|
69
|
-
const centerOfMass =
|
|
70
|
-
const maxDistance =
|
|
71
|
-
const units =
|
|
78
|
+
const centerOfMass = mission.getCenterOfMass();
|
|
79
|
+
const maxDistance = mission.getMaxDistanceToCenterOfMass();
|
|
80
|
+
const units = mission.getUnitsMatching(gameApi, (r) => r.rules.isSelectableCombatant);
|
|
72
81
|
|
|
73
82
|
// Only use ground units for center of mass.
|
|
74
|
-
const groundUnits =
|
|
83
|
+
const groundUnits = mission.getUnitsMatching(
|
|
75
84
|
gameApi,
|
|
76
85
|
(r) =>
|
|
77
86
|
r.rules.isSelectableCombatant &&
|
|
@@ -89,10 +98,10 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
89
98
|
maxDistance > requiredGatherRadius
|
|
90
99
|
) {
|
|
91
100
|
units.forEach((unit) => {
|
|
92
|
-
|
|
101
|
+
this.submitActionIfNew(actionBatcher, manageMoveMicro(unit, centerOfMass));
|
|
93
102
|
});
|
|
94
103
|
} else {
|
|
95
|
-
logger(`CombatSquad ${
|
|
104
|
+
logger(`CombatSquad ${mission.getUniqueName()} switching back to attack mode (${maxDistance})`);
|
|
96
105
|
this.state = SquadState.Attacking;
|
|
97
106
|
}
|
|
98
107
|
} else {
|
|
@@ -105,33 +114,47 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
105
114
|
maxDistance > requiredGatherRadius
|
|
106
115
|
) {
|
|
107
116
|
// Switch back to gather mode
|
|
108
|
-
logger(`CombatSquad ${
|
|
117
|
+
logger(`CombatSquad ${mission.getUniqueName()} switching back to gather (${maxDistance})`);
|
|
109
118
|
this.state = SquadState.Gathering;
|
|
110
119
|
return noop();
|
|
111
120
|
}
|
|
121
|
+
// The unit with the shortest range chooses the target. Otherwise, a base range of 5 is chosen.
|
|
122
|
+
const getRangeForUnit = (unit: UnitData) =>
|
|
123
|
+
unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
|
|
124
|
+
const attackLeader = minBy(units, getRangeForUnit);
|
|
125
|
+
if (!attackLeader) {
|
|
126
|
+
return noop();
|
|
127
|
+
}
|
|
128
|
+
// Find units within double the range of the leader.
|
|
129
|
+
const nearbyHostiles = matchAwareness
|
|
130
|
+
.getHostilesNearPoint(attackLeader.tile.rx, attackLeader.tile.ry, ATTACK_SCAN_AREA)
|
|
131
|
+
.map(({ unitId }) => gameApi.getUnitData(unitId))
|
|
132
|
+
.filter((unit) => !isOwnedByNeutral(unit)) as UnitData[];
|
|
133
|
+
|
|
112
134
|
for (const unit of units) {
|
|
113
|
-
const { rx: x, ry: y } = unit.tile;
|
|
114
|
-
const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
|
|
115
|
-
const nearbyHostiles = matchAwareness
|
|
116
|
-
.getHostilesNearPoint(x, y, range * 2)
|
|
117
|
-
.map(({ unitId }) => gameApi.getUnitData(unitId)) as UnitData[];
|
|
118
135
|
const bestUnit = maxBy(nearbyHostiles, (target) => getAttackWeight(unit, target));
|
|
119
136
|
if (bestUnit) {
|
|
120
|
-
|
|
137
|
+
this.submitActionIfNew(actionBatcher, manageAttackMicro(unit, bestUnit));
|
|
121
138
|
this.debugLastTarget = `Unit ${bestUnit.id.toString()}`;
|
|
122
139
|
} else {
|
|
123
|
-
|
|
140
|
+
this.submitActionIfNew(actionBatcher, manageMoveMicro(unit, targetPoint));
|
|
124
141
|
this.debugLastTarget = `@${targetPoint.x},${targetPoint.y}`;
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
}
|
|
146
|
+
return noop();
|
|
147
|
+
}
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Sends an action to the acitonBatcher if and only if the action is different from the last action we submitted to it.
|
|
151
|
+
* Prevents spamming redundant orders, which affects performance and can also ccause the unit to sit around doing nothing.
|
|
152
|
+
*/
|
|
153
|
+
private submitActionIfNew(actionBatcher: ActionBatcher, action: BatchableAction) {
|
|
154
|
+
const lastAction = this.lastOrderGiven[action.unitId];
|
|
155
|
+
if (!lastAction || !lastAction.isSameAs(action)) {
|
|
156
|
+
actionBatcher.push(action);
|
|
157
|
+
this.lastOrderGiven[action.unitId] = action;
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
}
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
UnitData,
|
|
8
|
-
Vector2,
|
|
9
|
-
ZoneType,
|
|
10
|
-
} from "@chronodivide/game-api";
|
|
11
|
-
import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
|
|
12
|
-
import { BatchableAction } from "./actionBatcher.js";
|
|
1
|
+
import { AttackState, ObjectType, OrderType, StanceType, UnitData, Vector2, ZoneType } from "@chronodivide/game-api";
|
|
2
|
+
import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../../map/map.js";
|
|
3
|
+
import { BatchableAction } from "../../actionBatcher.js";
|
|
4
|
+
|
|
5
|
+
const NONCE_GI_DEPLOY = 0;
|
|
6
|
+
const NONCE_GI_UNDEPLOY = 1;
|
|
13
7
|
|
|
14
8
|
// Micro methods
|
|
15
9
|
export function manageMoveMicro(attacker: UnitData, attackPoint: Vector2): BatchableAction {
|
|
16
10
|
if (attacker.name === "E1") {
|
|
17
11
|
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
18
12
|
if (isDeployed) {
|
|
19
|
-
return
|
|
13
|
+
return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_UNDEPLOY);
|
|
20
14
|
}
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
return
|
|
17
|
+
return BatchableAction.toPoint(attacker.id, OrderType.AttackMove, attackPoint);
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
export function manageAttackMicro(attacker: UnitData, target: UnitData): BatchableAction {
|
|
@@ -30,9 +24,9 @@ export function manageAttackMicro(attacker: UnitData, target: UnitData): Batchab
|
|
|
30
24
|
const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
|
|
31
25
|
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
32
26
|
if (!isDeployed && (distance <= deployedWeaponRange || attacker.attackState === AttackState.JustFired)) {
|
|
33
|
-
return
|
|
27
|
+
return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_DEPLOY);
|
|
34
28
|
} else if (isDeployed && distance > deployedWeaponRange) {
|
|
35
|
-
return
|
|
29
|
+
return BatchableAction.noTarget(attacker.id, OrderType.DeploySelected, NONCE_GI_UNDEPLOY);
|
|
36
30
|
}
|
|
37
31
|
}
|
|
38
32
|
let targetData = target;
|
|
@@ -44,7 +38,7 @@ export function manageAttackMicro(attacker: UnitData, target: UnitData): Batchab
|
|
|
44
38
|
// Special case for mirage tank/spy as otherwise they just sit next to it.
|
|
45
39
|
orderType = OrderType.Attack;
|
|
46
40
|
}
|
|
47
|
-
return
|
|
41
|
+
return BatchableAction.toTargetId(attacker.id, orderType, target.id);
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ActionsApi, GameApi, PlayerData } from "@chronodivide/game-api";
|
|
2
|
+
import { ActionBatcher } from "../../actionBatcher";
|
|
3
|
+
import { Mission, MissionAction } from "../../mission";
|
|
4
|
+
import { MatchAwareness } from "../../../awareness";
|
|
5
|
+
import { DebugLogger } from "../../../common/utils";
|
|
6
|
+
|
|
7
|
+
export interface Squad {
|
|
8
|
+
onAiUpdate(
|
|
9
|
+
gameApi: GameApi,
|
|
10
|
+
actionsApi: ActionsApi,
|
|
11
|
+
actionBatcher: ActionBatcher,
|
|
12
|
+
playerData: PlayerData,
|
|
13
|
+
mission: Mission<any>,
|
|
14
|
+
matchAwareness: MatchAwareness,
|
|
15
|
+
logger: DebugLogger,
|
|
16
|
+
): MissionAction;
|
|
17
|
+
|
|
18
|
+
getGlobalDebugText(): string | undefined;
|
|
19
|
+
}
|
|
@@ -4,25 +4,25 @@ import { GlobalThreat } from "./threat.js";
|
|
|
4
4
|
export function calculateGlobalThreat(game: GameApi, playerData: PlayerData, visibleAreaPercent: number): GlobalThreat {
|
|
5
5
|
let groundUnits = game.getVisibleUnits(
|
|
6
6
|
playerData.name,
|
|
7
|
-
"
|
|
7
|
+
"enemy",
|
|
8
8
|
(r) => r.type == ObjectType.Vehicle || r.type == ObjectType.Infantry,
|
|
9
9
|
);
|
|
10
|
-
let airUnits = game.getVisibleUnits(playerData.name, "
|
|
10
|
+
let airUnits = game.getVisibleUnits(playerData.name, "enemy", (r) => r.movementZone == MovementZone.Fly);
|
|
11
11
|
let groundDefence = game
|
|
12
|
-
.getVisibleUnits(playerData.name, "
|
|
12
|
+
.getVisibleUnits(playerData.name, "enemy", (r) => r.type == ObjectType.Building)
|
|
13
13
|
.filter((unitId) => isAntiGround(game, unitId));
|
|
14
14
|
let antiAirPower = game
|
|
15
|
-
.getVisibleUnits(playerData.name, "
|
|
15
|
+
.getVisibleUnits(playerData.name, "enemy", (r) => r.type != ObjectType.Building)
|
|
16
16
|
.filter((unitId) => isAntiAir(game, unitId));
|
|
17
17
|
|
|
18
18
|
let ourAntiGroundUnits = game
|
|
19
19
|
.getVisibleUnits(playerData.name, "self", (r) => r.isSelectableCombatant)
|
|
20
20
|
.filter((unitId) => isAntiGround(game, unitId));
|
|
21
21
|
let ourAntiAirUnits = game
|
|
22
|
-
.getVisibleUnits(playerData.name, "self", (r) => r.isSelectableCombatant)
|
|
22
|
+
.getVisibleUnits(playerData.name, "self", (r) => r.isSelectableCombatant || r.type === ObjectType.Building)
|
|
23
23
|
.filter((unitId) => isAntiAir(game, unitId));
|
|
24
24
|
let ourGroundDefence = game
|
|
25
|
-
.getVisibleUnits(playerData.name, "self", (r) => r.type
|
|
25
|
+
.getVisibleUnits(playerData.name, "self", (r) => r.type === ObjectType.Building)
|
|
26
26
|
.filter((unitId) => isAntiGround(game, unitId));
|
|
27
27
|
let ourAirUnits = game.getVisibleUnits(
|
|
28
28
|
playerData.name,
|
|
@@ -45,8 +45,8 @@ export function calculateGlobalThreat(game: GameApi, playerData: PlayerData, vis
|
|
|
45
45
|
observedGroundThreat,
|
|
46
46
|
observedAirThreat,
|
|
47
47
|
observedAntiAirThreat,
|
|
48
|
-
observedGroundDefence
|
|
49
|
-
ourGroundDefencePower
|
|
48
|
+
observedGroundDefence,
|
|
49
|
+
ourGroundDefencePower,
|
|
50
50
|
ourAntiGroundPower,
|
|
51
51
|
ourAntiAirPower,
|
|
52
52
|
ourAirPower,
|
|
@@ -76,14 +76,14 @@ function calculateFirepowerForUnit(unitData: UnitData): number {
|
|
|
76
76
|
threat +=
|
|
77
77
|
(hpRatio *
|
|
78
78
|
((unitData.primaryWeapon.rules.damage + 1) * GameMath.sqrt(unitData.primaryWeapon.rules.range + 1))) /
|
|
79
|
-
Math.max(unitData.primaryWeapon.
|
|
79
|
+
Math.max(unitData.primaryWeapon.rules.rof, 1);
|
|
80
80
|
}
|
|
81
81
|
if (unitData.secondaryWeapon) {
|
|
82
82
|
threat +=
|
|
83
83
|
(hpRatio *
|
|
84
84
|
((unitData.secondaryWeapon.rules.damage + 1) *
|
|
85
85
|
GameMath.sqrt(unitData.secondaryWeapon.rules.range + 1))) /
|
|
86
|
-
Math.max(unitData.secondaryWeapon.
|
|
86
|
+
Math.max(unitData.secondaryWeapon.rules.rof, 1);
|
|
87
87
|
}
|
|
88
88
|
return Math.min(800, threat);
|
|
89
89
|
}
|