@supalosa/chronodivide-bot 0.2.0 → 0.2.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/.prettierrc +5 -5
- package/README.md +11 -3
- package/dist/bot/bot.js +15 -7
- package/dist/bot/bot.js.map +1 -0
- package/dist/bot/logic/awareness.js +12 -2
- package/dist/bot/logic/awareness.js.map +1 -0
- package/dist/bot/logic/awarenessImpl.js +132 -0
- package/dist/bot/logic/awarenessImpl.js.map +1 -0
- package/dist/bot/logic/building/ArtilleryUnit.js +1 -0
- package/dist/bot/logic/building/ArtilleryUnit.js.map +1 -0
- package/dist/bot/logic/building/antiGroundStaticDefence.js +1 -0
- package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -0
- package/dist/bot/logic/building/basicAirUnit.js +1 -0
- package/dist/bot/logic/building/basicAirUnit.js.map +1 -0
- package/dist/bot/logic/building/basicBuilding.js +1 -0
- package/dist/bot/logic/building/basicBuilding.js.map +1 -0
- package/dist/bot/logic/building/basicGroundUnit.js +1 -0
- package/dist/bot/logic/building/basicGroundUnit.js.map +1 -0
- package/dist/bot/logic/building/building.js +57 -11
- package/dist/bot/logic/building/building.js.map +1 -0
- package/dist/bot/logic/building/harvester.js +1 -0
- package/dist/bot/logic/building/harvester.js.map +1 -0
- package/dist/bot/logic/building/powerPlant.js +1 -0
- package/dist/bot/logic/building/powerPlant.js.map +1 -0
- package/dist/bot/logic/building/queueController.js +1 -0
- package/dist/bot/logic/building/queueController.js.map +1 -0
- package/dist/bot/logic/building/resourceCollectionBuilding.js +1 -0
- package/dist/bot/logic/building/resourceCollectionBuilding.js.map +1 -0
- package/dist/bot/logic/common/scout.js +100 -0
- package/dist/bot/logic/common/scout.js.map +1 -0
- package/dist/bot/logic/common/utils.js +2 -0
- package/dist/bot/logic/common/utils.js.map +1 -0
- package/dist/bot/logic/map/map.js +9 -25
- package/dist/bot/logic/map/map.js.map +1 -0
- package/dist/bot/logic/map/sector.js +33 -1
- package/dist/bot/logic/map/sector.js.map +1 -0
- package/dist/bot/logic/mission/mission.js +3 -1
- package/dist/bot/logic/mission/mission.js.map +1 -0
- package/dist/bot/logic/mission/missionController.js +5 -4
- package/dist/bot/logic/mission/missionController.js.map +1 -0
- package/dist/bot/logic/mission/missionFactories.js +1 -0
- package/dist/bot/logic/mission/missionFactories.js.map +1 -0
- package/dist/bot/logic/mission/missions/attackMission.js +7 -8
- package/dist/bot/logic/mission/missions/attackMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +12 -7
- package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/expansionMission.js +5 -4
- package/dist/bot/logic/mission/missions/expansionMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/oneTimeMission.js +3 -2
- package/dist/bot/logic/mission/missions/oneTimeMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +3 -2
- package/dist/bot/logic/mission/missions/retreatMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/scoutingMission.js +8 -9
- package/dist/bot/logic/mission/missions/scoutingMission.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/attackSquad.js +63 -56
- package/dist/bot/logic/squad/behaviours/combatSquad.js +6 -3
- package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/common.js +7 -4
- package/dist/bot/logic/squad/behaviours/common.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +15 -2
- package/dist/bot/logic/squad/behaviours/expansionSquad.js +1 -0
- package/dist/bot/logic/squad/behaviours/expansionSquad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/retreatSquad.js +6 -10
- package/dist/bot/logic/squad/behaviours/retreatSquad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +66 -18
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js.map +1 -0
- package/dist/bot/logic/squad/squad.js +3 -3
- package/dist/bot/logic/squad/squad.js.map +1 -0
- package/dist/bot/logic/squad/squadBehaviour.js +1 -0
- package/dist/bot/logic/squad/squadBehaviour.js.map +1 -0
- package/dist/bot/logic/squad/squadBehaviours.js +1 -0
- package/dist/bot/logic/squad/squadBehaviours.js.map +1 -0
- package/dist/bot/logic/squad/squadController.js +73 -23
- package/dist/bot/logic/squad/squadController.js.map +1 -0
- package/dist/bot/logic/threat/threat.js +1 -0
- package/dist/bot/logic/threat/threat.js.map +1 -0
- package/dist/bot/logic/threat/threatCalculator.js +1 -0
- package/dist/bot/logic/threat/threatCalculator.js.map +1 -0
- package/dist/exampleBot.js +24 -7
- package/dist/exampleBot.js.map +1 -0
- package/package.json +9 -6
- package/src/bot/bot.ts +17 -11
- package/src/bot/logic/awareness.ts +22 -5
- package/src/bot/logic/building/ArtilleryUnit.ts +43 -43
- package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -60
- package/src/bot/logic/building/basicAirUnit.ts +68 -68
- package/src/bot/logic/building/basicBuilding.ts +47 -47
- package/src/bot/logic/building/building.ts +72 -12
- package/src/bot/logic/building/powerPlant.ts +32 -32
- package/src/bot/logic/building/resourceCollectionBuilding.ts +56 -56
- package/src/bot/logic/common/scout.ts +127 -1
- package/src/bot/logic/common/utils.ts +1 -0
- package/src/bot/logic/map/map.ts +70 -84
- package/src/bot/logic/map/sector.ts +46 -4
- package/src/bot/logic/mission/mission.ts +2 -2
- package/src/bot/logic/mission/missionController.ts +7 -6
- package/src/bot/logic/mission/missionFactories.ts +3 -0
- package/src/bot/logic/mission/missions/attackMission.ts +19 -13
- package/src/bot/logic/mission/missions/defenceMission.ts +35 -15
- package/src/bot/logic/mission/missions/expansionMission.ts +6 -4
- package/src/bot/logic/mission/missions/oneTimeMission.ts +3 -2
- package/src/bot/logic/mission/missions/retreatMission.ts +3 -2
- package/src/bot/logic/mission/missions/scoutingMission.ts +9 -6
- package/src/bot/logic/squad/behaviours/combatSquad.ts +6 -2
- package/src/bot/logic/squad/behaviours/common.ts +6 -4
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +10 -12
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +81 -23
- package/src/bot/logic/squad/squad.ts +3 -2
- package/src/bot/logic/squad/squadBehaviour.ts +3 -1
- package/src/bot/logic/squad/squadController.ts +136 -69
- package/src/bot/logic/threat/threat.ts +15 -15
- package/src/bot/logic/threat/threatCalculator.ts +99 -99
- package/src/exampleBot.ts +25 -6
- package/tsconfig.json +73 -73
- package/dist/bot/logic/building/massedAntiGroundUnit.js +0 -20
- package/dist/bot/logic/building/queues.js +0 -19
- package/dist/bot/logic/knowledge.js +0 -1
- package/dist/bot/logic/mission/basicMission.js +0 -26
- package/dist/bot/logic/mission/expansionMission.js +0 -32
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +0 -31
- package/dist/bot/logic/squad/behaviours/squadScouters.js +0 -8
|
@@ -6,6 +6,7 @@ import { Squad } from "../../squad/squad.js";
|
|
|
6
6
|
import { MissionFactory } from "../missionFactories.js";
|
|
7
7
|
import { SquadBehaviour } from "../../squad/squadBehaviour.js";
|
|
8
8
|
import { MatchAwareness } from "../../awareness.js";
|
|
9
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A mission that gets dispatched once, and once the squad decides to disband, the mission is disbanded.
|
|
@@ -13,8 +14,8 @@ import { MatchAwareness } from "../../awareness.js";
|
|
|
13
14
|
export abstract class OneTimeMission<T = undefined> extends Mission<T> {
|
|
14
15
|
private hadSquad = false;
|
|
15
16
|
|
|
16
|
-
constructor(uniqueName: string, priority: number, private behaviourFactory: () => SquadBehaviour) {
|
|
17
|
-
super(uniqueName, priority);
|
|
17
|
+
constructor(uniqueName: string, priority: number, private behaviourFactory: () => SquadBehaviour, logger: DebugLogger) {
|
|
18
|
+
super(uniqueName, priority, logger);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
onAiUpdate(gameApi: GameApi, playerData: PlayerData, matchAwareness: MatchAwareness): MissionAction {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Point2D } from "@chronodivide/game-api";
|
|
2
2
|
import { OneTimeMission } from "./oneTimeMission.js";
|
|
3
3
|
import { RetreatSquad } from "../../squad/behaviours/retreatSquad.js";
|
|
4
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
4
5
|
|
|
5
6
|
export class RetreatMission extends OneTimeMission {
|
|
6
|
-
constructor(uniqueName: string, priority: number, retreatToPoint: Point2D, unitIds: number[]) {
|
|
7
|
-
super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint));
|
|
7
|
+
constructor(uniqueName: string, priority: number, retreatToPoint: Point2D, unitIds: number[], logger: DebugLogger) {
|
|
8
|
+
super(uniqueName, priority, () => new RetreatSquad(unitIds, retreatToPoint), logger);
|
|
8
9
|
}
|
|
9
10
|
}
|
|
@@ -7,13 +7,15 @@ import { Mission } from "../mission.js";
|
|
|
7
7
|
import { AttackMission } from "./attackMission.js";
|
|
8
8
|
import { MissionController } from "../missionController.js";
|
|
9
9
|
import { getUnseenStartingLocations } from "../../common/scout.js";
|
|
10
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* A mission that tries to scout around the map with a cheap, fast unit (usually attack dogs)
|
|
13
14
|
*/
|
|
14
15
|
export class ScoutingMission extends OneTimeMission {
|
|
15
|
-
constructor(uniqueName: string, priority: number
|
|
16
|
-
|
|
16
|
+
constructor(uniqueName: string, priority: number,
|
|
17
|
+
logger: DebugLogger) {
|
|
18
|
+
super(uniqueName, priority, () => new ScoutingSquad(), logger);
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -31,15 +33,15 @@ export class ScoutingMissionFactory implements MissionFactory {
|
|
|
31
33
|
playerData: PlayerData,
|
|
32
34
|
matchAwareness: MatchAwareness,
|
|
33
35
|
missionController: MissionController,
|
|
36
|
+
logger: DebugLogger
|
|
34
37
|
): void {
|
|
35
38
|
if (gameApi.getCurrentTick() < this.lastScoutAt + SCOUT_COOLDOWN_TICKS) {
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
|
-
|
|
39
|
-
if (candidatePoints.length === 0) {
|
|
41
|
+
if (!matchAwareness.getScoutingManager().hasScoutTargets()) {
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
|
-
if (!missionController.addMission(new ScoutingMission("globalScout", 100))) {
|
|
44
|
+
if (!missionController.addMission(new ScoutingMission("globalScout", 100, logger))) {
|
|
43
45
|
this.lastScoutAt = gameApi.getCurrentTick();
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -51,9 +53,10 @@ export class ScoutingMissionFactory implements MissionFactory {
|
|
|
51
53
|
failedMission: Mission,
|
|
52
54
|
failureReason: undefined,
|
|
53
55
|
missionController: MissionController,
|
|
56
|
+
logger: DebugLogger
|
|
54
57
|
): void {
|
|
55
58
|
if (failedMission instanceof AttackMission) {
|
|
56
|
-
missionController.addMission(new ScoutingMission("globalScout", 100));
|
|
59
|
+
missionController.addMission(new ScoutingMission("globalScout", 100, logger));
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -5,6 +5,7 @@ import { SquadAction, SquadBehaviour, grabCombatants, noop } from "../squadBehav
|
|
|
5
5
|
import { MatchAwareness } from "../../awareness.js";
|
|
6
6
|
import { getDistanceBetweenPoints } from "../../map/map.js";
|
|
7
7
|
import { manageAttackMicro, manageMoveMicro } from "./common.js";
|
|
8
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
8
9
|
|
|
9
10
|
const TARGET_UPDATE_INTERVAL_TICKS = 10;
|
|
10
11
|
const GRAB_INTERVAL_TICKS = 10;
|
|
@@ -52,8 +53,9 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
52
53
|
playerData: PlayerData,
|
|
53
54
|
squad: Squad,
|
|
54
55
|
matchAwareness: MatchAwareness,
|
|
56
|
+
logger: DebugLogger,
|
|
55
57
|
): SquadAction {
|
|
56
|
-
if (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS) {
|
|
58
|
+
if (squad.getUnitIds().length > 0 && (!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)) {
|
|
57
59
|
this.lastCommand = gameApi.getCurrentTick();
|
|
58
60
|
const centerOfMass = squad.getCenterOfMass();
|
|
59
61
|
const maxDistance = squad.getMaxDistanceToCenterOfMass();
|
|
@@ -81,6 +83,7 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
81
83
|
manageMoveMicro(actionsApi, unit, centerOfMass);
|
|
82
84
|
});
|
|
83
85
|
} else {
|
|
86
|
+
logger(`CombatSquad ${squad.getName()} switching back to attack mode (${maxDistance})`)
|
|
84
87
|
this.state = SquadState.Attacking;
|
|
85
88
|
}
|
|
86
89
|
} else {
|
|
@@ -92,7 +95,8 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
92
95
|
gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
|
|
93
96
|
maxDistance > requiredGatherRadius
|
|
94
97
|
) {
|
|
95
|
-
// Switch back to gather mode
|
|
98
|
+
// Switch back to gather mode
|
|
99
|
+
logger(`CombatSquad ${squad.getName()} switching back to gather (${maxDistance})`)
|
|
96
100
|
this.state = SquadState.Gathering;
|
|
97
101
|
return noop();
|
|
98
102
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ActionsApi, ObjectType, OrderType, Point2D, UnitData } from "@chronodivide/game-api";
|
|
1
|
+
import { ActionsApi, AttackState, ObjectType, OrderType, Point2D, StanceType, UnitData } from "@chronodivide/game-api";
|
|
2
2
|
import { getDistanceBetweenUnits } from "../../map/map.js";
|
|
3
3
|
|
|
4
4
|
// Micro methods
|
|
5
5
|
export function manageMoveMicro(actionsApi: ActionsApi, attacker: UnitData, attackPoint: Point2D) {
|
|
6
6
|
if (attacker.name === "E1") {
|
|
7
|
-
|
|
7
|
+
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
8
|
+
if (isDeployed) {
|
|
8
9
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
9
10
|
}
|
|
10
11
|
}
|
|
@@ -16,10 +17,11 @@ export function manageAttackMicro(actionsApi: ActionsApi, attacker: UnitData, ta
|
|
|
16
17
|
if (attacker.name === "E1") {
|
|
17
18
|
// Para (deployed weapon) range is 5.
|
|
18
19
|
const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
|
|
19
|
-
|
|
20
|
+
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
21
|
+
if (!isDeployed && (distance <= deployedWeaponRange || attacker.attackState === AttackState.JustFired)) {
|
|
20
22
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
21
23
|
return;
|
|
22
|
-
} else if (
|
|
24
|
+
} else if (isDeployed && distance > deployedWeaponRange) {
|
|
23
25
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
@@ -7,8 +7,6 @@ import { MatchAwareness } from "../../awareness.js";
|
|
|
7
7
|
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
8
8
|
|
|
9
9
|
export class RetreatSquad implements SquadBehaviour {
|
|
10
|
-
private hasRequestedUnits: boolean = false;
|
|
11
|
-
private moveOrderSentAt: number | null = null;
|
|
12
10
|
private createdAt: number | null = null;
|
|
13
11
|
|
|
14
12
|
constructor(
|
|
@@ -28,19 +26,19 @@ export class RetreatSquad implements SquadBehaviour {
|
|
|
28
26
|
}
|
|
29
27
|
if (squad.getUnitIds().length > 0) {
|
|
30
28
|
// Only send the order once we have managed to claim some units.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
|
|
29
|
+
actionsApi.orderUnits(
|
|
30
|
+
squad.getUnitIds(),
|
|
31
|
+
OrderType.AttackMove,
|
|
32
|
+
this.retreatToPoint.x,
|
|
33
|
+
this.retreatToPoint.y,
|
|
34
|
+
);
|
|
35
|
+
return disband();
|
|
36
36
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
(this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240)
|
|
40
|
-
) {
|
|
37
|
+
if (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240) {
|
|
38
|
+
// Disband automatically after 240 ticks in case we couldn't actually claim any units.
|
|
41
39
|
return disband();
|
|
42
40
|
} else {
|
|
43
|
-
return requestSpecificUnits(this.unitIds,
|
|
41
|
+
return requestSpecificUnits(this.unitIds, 1000);
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
}
|
|
@@ -4,14 +4,30 @@ import { Squad } from "../squad.js";
|
|
|
4
4
|
import { SquadAction, SquadBehaviour, disband, noop, requestUnits } from "../squadBehaviour.js";
|
|
5
5
|
import { MatchAwareness } from "../../awareness.js";
|
|
6
6
|
import { getUnseenStartingLocations } from "../../common/scout.js";
|
|
7
|
+
import { match } from "assert";
|
|
8
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
9
|
+
import _ from "lodash";
|
|
10
|
+
import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
|
|
7
11
|
|
|
8
12
|
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
9
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;
|
|
20
|
+
|
|
10
21
|
export class ScoutingSquad implements SquadBehaviour {
|
|
11
|
-
private
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
private scoutTarget: Point2D | null = null;
|
|
23
|
+
private attemptsOnCurrentTarget: number = 0;
|
|
24
|
+
private scoutTargetRefreshedAt: number = 0;
|
|
25
|
+
private lastMoveCommandTick: number = 0;
|
|
26
|
+
|
|
27
|
+
// Minimum distance from a scout to the target.
|
|
28
|
+
private scoutMinDistance?: number;
|
|
29
|
+
|
|
30
|
+
private hadUnit: boolean = false;
|
|
15
31
|
|
|
16
32
|
public onAiUpdate(
|
|
17
33
|
gameApi: GameApi,
|
|
@@ -19,6 +35,7 @@ export class ScoutingSquad implements SquadBehaviour {
|
|
|
19
35
|
playerData: PlayerData,
|
|
20
36
|
squad: Squad,
|
|
21
37
|
matchAwareness: MatchAwareness,
|
|
38
|
+
logger: DebugLogger,
|
|
22
39
|
): SquadAction {
|
|
23
40
|
const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
|
|
24
41
|
const scouts = squad.getUnitsOfTypes(gameApi, ...scoutNames);
|
|
@@ -28,29 +45,70 @@ export class ScoutingSquad implements SquadBehaviour {
|
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
if (scouts.length === 0) {
|
|
31
|
-
|
|
48
|
+
// Count the number of times the scout dies trying to uncover the current scoutTarget.
|
|
49
|
+
if (this.scoutTarget && this.hadUnit) {
|
|
50
|
+
this.attemptsOnCurrentTarget++;
|
|
51
|
+
this.hadUnit = false;
|
|
52
|
+
}
|
|
32
53
|
return requestUnits(scoutNames, 100);
|
|
33
|
-
} else if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
} else if (this.scoutTarget) {
|
|
55
|
+
this.hadUnit = true;
|
|
56
|
+
if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {
|
|
57
|
+
logger(
|
|
58
|
+
`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,
|
|
59
|
+
);
|
|
60
|
+
this.setScoutTarget(null, 0);
|
|
61
|
+
return noop();
|
|
62
|
+
}
|
|
63
|
+
if (gameApi.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {
|
|
64
|
+
logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`);
|
|
65
|
+
this.setScoutTarget(null, 0);
|
|
66
|
+
return noop();
|
|
67
|
+
}
|
|
68
|
+
const targetTile = gameApi.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);
|
|
69
|
+
if (!targetTile) {
|
|
70
|
+
throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);
|
|
71
|
+
}
|
|
72
|
+
if (gameApi.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {
|
|
73
|
+
this.lastMoveCommandTick = gameApi.getCurrentTick();
|
|
74
|
+
scouts.forEach((unit) => {
|
|
75
|
+
if (this.scoutTarget) {
|
|
76
|
+
actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);
|
|
44
77
|
}
|
|
78
|
+
});
|
|
79
|
+
// Check that a scout is actually moving closer to the target.
|
|
80
|
+
const newMinDistance = _.min(
|
|
81
|
+
scouts.map((unit) =>
|
|
82
|
+
getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, this.scoutTarget!),
|
|
83
|
+
),
|
|
84
|
+
)!;
|
|
85
|
+
if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {
|
|
86
|
+
logger(
|
|
87
|
+
`Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,
|
|
88
|
+
);
|
|
89
|
+
this.scoutTargetRefreshedAt = gameApi.getCurrentTick();
|
|
90
|
+
this.scoutMinDistance = newMinDistance;
|
|
45
91
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
}
|
|
93
|
+
if (gameApi.mapApi.isVisibleTile(targetTile, playerData.name)) {
|
|
94
|
+
logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`);
|
|
95
|
+
this.setScoutTarget(null, gameApi.getCurrentTick());
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const candidatePoint = matchAwareness.getScoutingManager().getNewScoutTarget()?.asPoint2D();
|
|
99
|
+
if (!candidatePoint) {
|
|
100
|
+
logger(`No more scouting targets available, disbanding.`);
|
|
101
|
+
return disband();
|
|
102
|
+
}
|
|
103
|
+
this.setScoutTarget(candidatePoint, gameApi.getCurrentTick());
|
|
53
104
|
}
|
|
54
105
|
return noop();
|
|
55
106
|
}
|
|
107
|
+
|
|
108
|
+
setScoutTarget(point: Point2D | null, currentTick: number) {
|
|
109
|
+
this.attemptsOnCurrentTarget = 0;
|
|
110
|
+
this.scoutTargetRefreshedAt = currentTick;
|
|
111
|
+
this.scoutTarget = point;
|
|
112
|
+
this.scoutMinDistance = undefined;
|
|
113
|
+
}
|
|
56
114
|
}
|
|
@@ -5,6 +5,7 @@ import { SquadAction, SquadBehaviour, disband } from "./squadBehaviour.js";
|
|
|
5
5
|
import { MatchAwareness } from "../awareness.js";
|
|
6
6
|
import { getDistanceBetweenPoints } from "../map/map.js";
|
|
7
7
|
import _ from "lodash";
|
|
8
|
+
import { DebugLogger } from "../common/utils.js";
|
|
8
9
|
|
|
9
10
|
export enum SquadLiveness {
|
|
10
11
|
SquadDead,
|
|
@@ -76,6 +77,7 @@ export class Squad {
|
|
|
76
77
|
actionsApi: ActionsApi,
|
|
77
78
|
playerData: PlayerData,
|
|
78
79
|
matchAwareness: MatchAwareness,
|
|
80
|
+
logger: DebugLogger
|
|
79
81
|
): SquadAction {
|
|
80
82
|
this.updateLiveness(gameApi);
|
|
81
83
|
const movableUnitTiles = this.unitIds
|
|
@@ -100,8 +102,7 @@ export class Squad {
|
|
|
100
102
|
} else if (!this.mission) {
|
|
101
103
|
return disband();
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
return outcome;
|
|
105
|
+
return this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness, logger);
|
|
105
106
|
}
|
|
106
107
|
public getMission(): Mission | null {
|
|
107
108
|
return this.mission;
|
|
@@ -2,6 +2,7 @@ import { ActionsApi, GameApi, PlayerData, Point2D } from "@chronodivide/game-api
|
|
|
2
2
|
import { GlobalThreat } from "../threat/threat.js";
|
|
3
3
|
import { Squad } from "./squad.js";
|
|
4
4
|
import { MatchAwareness } from "../awareness.js";
|
|
5
|
+
import { DebugLogger } from "../common/utils.js";
|
|
5
6
|
|
|
6
7
|
export type SquadActionNoop = {
|
|
7
8
|
type: "noop";
|
|
@@ -56,6 +57,7 @@ export interface SquadBehaviour {
|
|
|
56
57
|
actionsApi: ActionsApi,
|
|
57
58
|
playerData: PlayerData,
|
|
58
59
|
squad: Squad,
|
|
59
|
-
matchAwareness: MatchAwareness
|
|
60
|
+
matchAwareness: MatchAwareness,
|
|
61
|
+
logger: DebugLogger
|
|
60
62
|
): SquadAction;
|
|
61
63
|
}
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
} from "./squadBehaviour.js";
|
|
14
14
|
import { MatchAwareness } from "../awareness.js";
|
|
15
15
|
import { getDistanceBetween } from "../map/map.js";
|
|
16
|
+
import _ from "lodash";
|
|
17
|
+
import { DebugLogger } from "../common/utils.js";
|
|
16
18
|
|
|
17
19
|
type SquadWithAction<T> = {
|
|
18
20
|
squad: Squad;
|
|
@@ -23,14 +25,13 @@ export class SquadController {
|
|
|
23
25
|
private squads: Squad[] = [];
|
|
24
26
|
private unitIdToSquad: Map<number, Squad> = new Map();
|
|
25
27
|
|
|
26
|
-
constructor() {}
|
|
28
|
+
constructor(private logger: (message: string, sayInGame?: boolean) => void) {}
|
|
27
29
|
|
|
28
30
|
public onAiUpdate(
|
|
29
31
|
gameApi: GameApi,
|
|
30
32
|
actionsApi: ActionsApi,
|
|
31
33
|
playerData: PlayerData,
|
|
32
34
|
matchAwareness: MatchAwareness,
|
|
33
|
-
logger: (message: string) => void
|
|
34
35
|
) {
|
|
35
36
|
// Remove dead squads or those where the mission is dead.
|
|
36
37
|
this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
|
|
@@ -41,7 +42,7 @@ export class SquadController {
|
|
|
41
42
|
this.squads.forEach((squad) => {
|
|
42
43
|
squad.getUnitIds().forEach((unitId) => {
|
|
43
44
|
if (this.unitIdToSquad.has(unitId)) {
|
|
44
|
-
logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
45
|
+
this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
45
46
|
} else {
|
|
46
47
|
this.unitIdToSquad.set(unitId, squad);
|
|
47
48
|
}
|
|
@@ -51,7 +52,7 @@ export class SquadController {
|
|
|
51
52
|
const squadActions: SquadWithAction<SquadAction>[] = this.squads.map((squad) => {
|
|
52
53
|
return {
|
|
53
54
|
squad,
|
|
54
|
-
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
|
|
55
|
+
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, this.logger),
|
|
55
56
|
};
|
|
56
57
|
});
|
|
57
58
|
// Handle disbands and merges.
|
|
@@ -61,7 +62,7 @@ export class SquadController {
|
|
|
61
62
|
squadActions
|
|
62
63
|
.filter((a) => isDisband(a.action))
|
|
63
64
|
.forEach((a) => {
|
|
64
|
-
logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
65
|
+
this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
65
66
|
a.squad.getMission()?.removeSquad();
|
|
66
67
|
a.squad.getUnitIds().forEach((unitId) => {
|
|
67
68
|
this.unitIdToSquad.delete(unitId);
|
|
@@ -73,8 +74,8 @@ export class SquadController {
|
|
|
73
74
|
.forEach((a) => {
|
|
74
75
|
let mergeInto = a.action as SquadActionMergeInto;
|
|
75
76
|
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
76
|
-
logger(
|
|
77
|
-
`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling
|
|
77
|
+
this.logger(
|
|
78
|
+
`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`,
|
|
78
79
|
);
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
@@ -88,58 +89,88 @@ export class SquadController {
|
|
|
88
89
|
const isRequestSpecific = (a: SquadAction) => a.type === "requestSpecific";
|
|
89
90
|
const unitIdToHighestRequest = squadActions
|
|
90
91
|
.filter((a) => isRequestSpecific(a.action))
|
|
91
|
-
.reduce(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (prev
|
|
92
|
+
.reduce(
|
|
93
|
+
(prev, a) => {
|
|
94
|
+
const squadWithAction = a as SquadWithAction<SquadActionRequestSpecificUnits>;
|
|
95
|
+
const { unitIds } = squadWithAction.action;
|
|
96
|
+
unitIds.forEach((unitId) => {
|
|
97
|
+
if (prev.hasOwnProperty(unitId)) {
|
|
98
|
+
if (prev[unitId].action.priority > prev[unitId].action.priority) {
|
|
99
|
+
prev[unitId] = squadWithAction;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
97
102
|
prev[unitId] = squadWithAction;
|
|
98
103
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
104
|
+
});
|
|
105
|
+
return prev;
|
|
106
|
+
},
|
|
107
|
+
{} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Map of Squad ID to Unit Type to Count.
|
|
111
|
+
const newSquadAssignments = Object.entries(unitIdToHighestRequest)
|
|
112
|
+
.flatMap(([id, request]) => {
|
|
113
|
+
const unitId = Number.parseInt(id);
|
|
114
|
+
const unit = gameApi.getUnitData(unitId);
|
|
115
|
+
const { squad: requestingSquad } = request;
|
|
116
|
+
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
117
|
+
if (!unit) {
|
|
118
|
+
this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
if (!this.unitIdToSquad.has(unitId)) {
|
|
122
|
+
this.addUnitToSquad(requestingSquad, unit);
|
|
123
|
+
return [{ unitName: unit?.name, squad: requestingSquad.getName() }];
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
})
|
|
127
|
+
.reduce(
|
|
128
|
+
(acc, curr) => {
|
|
129
|
+
if (!acc[curr.squad]) {
|
|
130
|
+
acc[curr.squad] = {};
|
|
101
131
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.addUnitToSquad(requestingSquad, unit);
|
|
117
|
-
}
|
|
132
|
+
if (!acc[curr.squad][curr.unitName]) {
|
|
133
|
+
acc[curr.squad][curr.unitName] = 0;
|
|
134
|
+
}
|
|
135
|
+
acc[curr.squad][curr.unitName] = acc[curr.squad][curr.unitName] + 1;
|
|
136
|
+
return acc;
|
|
137
|
+
},
|
|
138
|
+
{} as Record<string, Record<string, number>>,
|
|
139
|
+
);
|
|
140
|
+
Object.entries(newSquadAssignments).forEach(([squad, assignments]) => {
|
|
141
|
+
this.logger(
|
|
142
|
+
`Squad ${squad} received: ${Object.entries(assignments)
|
|
143
|
+
.map(([unitType, count]) => unitType + " x " + count)
|
|
144
|
+
.join(", ")}`,
|
|
145
|
+
);
|
|
118
146
|
});
|
|
119
147
|
|
|
120
148
|
// Request units by type
|
|
121
149
|
const isRequest = (a: SquadAction) => a.type === "request";
|
|
122
150
|
const unitTypeToHighestRequest = squadActions
|
|
123
151
|
.filter((a) => isRequest(a.action))
|
|
124
|
-
.reduce(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (prev
|
|
152
|
+
.reduce(
|
|
153
|
+
(prev, a) => {
|
|
154
|
+
const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
|
|
155
|
+
const { unitNames } = squadWithAction.action;
|
|
156
|
+
unitNames.forEach((unitName) => {
|
|
157
|
+
if (prev.hasOwnProperty(unitName)) {
|
|
158
|
+
if (prev[unitName].action.priority > prev[unitName].action.priority) {
|
|
159
|
+
prev[unitName] = squadWithAction;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
130
162
|
prev[unitName] = squadWithAction;
|
|
131
163
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
}, {} as Record<string, SquadWithAction<SquadActionRequestUnits>>);
|
|
164
|
+
});
|
|
165
|
+
return prev;
|
|
166
|
+
},
|
|
167
|
+
{} as Record<string, SquadWithAction<SquadActionRequestUnits>>,
|
|
168
|
+
);
|
|
138
169
|
|
|
139
170
|
// Request combat-capable units in an area
|
|
140
171
|
const isGrab = (a: SquadAction) => a.type === "requestCombatants";
|
|
141
172
|
const grabRequests = squadActions.filter((a) =>
|
|
142
|
-
isGrab(a.action)
|
|
173
|
+
isGrab(a.action),
|
|
143
174
|
) as SquadWithAction<SquadActionGrabFreeCombatants>[];
|
|
144
175
|
|
|
145
176
|
// Find loose units
|
|
@@ -149,33 +180,57 @@ export class SquadController {
|
|
|
149
180
|
.filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
|
|
150
181
|
.map((unit) => unit!);
|
|
151
182
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
freeUnit.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
request.action.point.y
|
|
170
|
-
}`
|
|
183
|
+
type AssignmentWithType = { unitName: string; squad: string; method: "type" | "grab" };
|
|
184
|
+
// [squadName][unitName]['type' | 'grab']
|
|
185
|
+
const newAssignmentsByType = freeUnits
|
|
186
|
+
.flatMap((freeUnit) => {
|
|
187
|
+
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
188
|
+
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
189
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
190
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
191
|
+
delete unitTypeToHighestRequest[freeUnit.name];
|
|
192
|
+
return [
|
|
193
|
+
{ unitName: freeUnit.name, squad: requestingSquad.getName(), method: "type" },
|
|
194
|
+
] as AssignmentWithType[];
|
|
195
|
+
} else if (grabRequests.length > 0) {
|
|
196
|
+
const grantedSquad = grabRequests.find((request) => {
|
|
197
|
+
return (
|
|
198
|
+
freeUnit.rules.isSelectableCombatant &&
|
|
199
|
+
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
|
|
171
200
|
);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return
|
|
201
|
+
});
|
|
202
|
+
if (grantedSquad) {
|
|
203
|
+
this.addUnitToSquad(grantedSquad.squad, freeUnit);
|
|
204
|
+
return [
|
|
205
|
+
{ unitName: freeUnit.name, squad: grantedSquad.squad.getName(), method: "grab" },
|
|
206
|
+
] as AssignmentWithType[];
|
|
176
207
|
}
|
|
177
|
-
}
|
|
178
|
-
|
|
208
|
+
}
|
|
209
|
+
return [];
|
|
210
|
+
})
|
|
211
|
+
.reduce(
|
|
212
|
+
(acc, curr) => {
|
|
213
|
+
if (!acc[curr.squad]) {
|
|
214
|
+
acc[curr.squad] = {};
|
|
215
|
+
}
|
|
216
|
+
if (!acc[curr.squad][curr.unitName]) {
|
|
217
|
+
acc[curr.squad][curr.unitName] = { grab: 0, type: 0 };
|
|
218
|
+
}
|
|
219
|
+
acc[curr.squad][curr.unitName][curr.method] = acc[curr.squad][curr.unitName][curr.method] + 1;
|
|
220
|
+
return acc;
|
|
221
|
+
},
|
|
222
|
+
{} as Record<string, Record<string, Record<"type" | "grab", number>>>,
|
|
223
|
+
);
|
|
224
|
+
Object.entries(newAssignmentsByType).forEach(([squad, assignments]) => {
|
|
225
|
+
this.logger(
|
|
226
|
+
`Squad ${squad} received: ${Object.entries(assignments)
|
|
227
|
+
.flatMap(([unitType, methodToCount]) =>
|
|
228
|
+
Object.entries(methodToCount)
|
|
229
|
+
.filter(([, count]) => count > 0)
|
|
230
|
+
.map(([method, count]) => unitType + " x " + count + " (by " + method + ")"),
|
|
231
|
+
)
|
|
232
|
+
.join(", ")}`,
|
|
233
|
+
);
|
|
179
234
|
});
|
|
180
235
|
}
|
|
181
236
|
|
|
@@ -187,4 +242,16 @@ export class SquadController {
|
|
|
187
242
|
public registerSquad(squad: Squad) {
|
|
188
243
|
this.squads.push(squad);
|
|
189
244
|
}
|
|
245
|
+
|
|
246
|
+
public debugSquads(gameApi: GameApi) {
|
|
247
|
+
const unitsInSquad = (unitIds: number[]) => _.countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
|
|
248
|
+
|
|
249
|
+
this.squads.forEach((squad) => {
|
|
250
|
+
this.logger(
|
|
251
|
+
`Squad ${squad.getName()}: ${Object.entries(unitsInSquad(squad.getUnitIds()))
|
|
252
|
+
.map(([unitName, count]) => `${unitName} x ${count}`)
|
|
253
|
+
.join(", ")}`,
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
190
257
|
}
|