@supalosa/chronodivide-bot 0.2.1 → 0.2.2-a
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/README.md +3 -3
- package/dist/bot/bot.js +7 -3
- 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/building/ArtilleryUnit.js +29 -8
- package/dist/bot/logic/building/antiGroundStaticDefence.js +3 -2
- package/dist/bot/logic/building/antiGroundStaticDefence.js.map +1 -0
- package/dist/bot/logic/building/artilleryUnit.js.map +1 -0
- package/dist/bot/logic/building/basicAirUnit.js +2 -1
- package/dist/bot/logic/building/basicAirUnit.js.map +1 -0
- package/dist/bot/logic/building/basicBuilding.js +2 -1
- package/dist/bot/logic/building/basicBuilding.js.map +1 -0
- package/dist/bot/logic/building/basicGroundUnit.js +2 -1
- package/dist/bot/logic/building/basicGroundUnit.js.map +1 -0
- package/dist/bot/logic/building/buildingRules.js +168 -0
- package/dist/bot/logic/building/buildingRules.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 +2 -1
- package/dist/bot/logic/building/powerPlant.js.map +1 -0
- package/dist/bot/logic/building/queueController.js +2 -1
- package/dist/bot/logic/building/queueController.js.map +1 -0
- package/dist/bot/logic/building/resourceCollectionBuilding.js +2 -1
- 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 +14 -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 +3 -2
- package/dist/bot/logic/mission/missionController.js.map +1 -0
- package/dist/bot/logic/mission/missionFactories.js +3 -0
- package/dist/bot/logic/mission/missionFactories.js.map +1 -0
- package/dist/bot/logic/mission/missions/attackMission.js +8 -7
- package/dist/bot/logic/mission/missions/attackMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +11 -6
- package/dist/bot/logic/mission/missions/defenceMission.js.map +1 -0
- package/dist/bot/logic/mission/missions/engineerMission.js +34 -0
- package/dist/bot/logic/mission/missions/engineerMission.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/combatSquad.js +19 -17
- package/dist/bot/logic/squad/behaviours/combatSquad.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/common.js +20 -2
- package/dist/bot/logic/squad/behaviours/common.js.map +1 -0
- package/dist/bot/logic/squad/behaviours/engineerSquad.js +36 -0
- package/dist/bot/logic/squad/behaviours/engineerSquad.js.map +1 -0
- 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 +1 -0
- 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 +4 -5
- 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 +58 -18
- 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 +7 -6
- package/dist/exampleBot.js.map +1 -0
- package/package.json +15 -7
- package/src/bot/bot.ts +10 -7
- package/src/bot/logic/awareness.ts +21 -4
- package/src/bot/logic/building/antiGroundStaticDefence.ts +60 -60
- package/src/bot/logic/building/artilleryUnit.ts +68 -0
- package/src/bot/logic/building/basicAirUnit.ts +68 -68
- package/src/bot/logic/building/basicBuilding.ts +47 -47
- package/src/bot/logic/building/basicGroundUnit.ts +1 -1
- package/src/bot/logic/building/buildingRules.ts +233 -0
- package/src/bot/logic/building/powerPlant.ts +32 -32
- package/src/bot/logic/building/queueController.ts +1 -1
- 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 +17 -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 +2 -3
- package/src/bot/logic/mission/missionFactories.ts +5 -0
- package/src/bot/logic/mission/missions/attackMission.ts +25 -20
- package/src/bot/logic/mission/missions/defenceMission.ts +34 -14
- package/src/bot/logic/mission/missions/engineerMission.ts +61 -0
- 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 +21 -17
- package/src/bot/logic/squad/behaviours/common.ts +33 -2
- package/src/bot/logic/squad/behaviours/engineerSquad.ts +53 -0
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +79 -26
- package/src/bot/logic/squad/squad.ts +4 -4
- package/src/bot/logic/squad/squadBehaviour.ts +3 -1
- package/src/bot/logic/squad/squadController.ts +89 -44
- package/src/exampleBot.ts +6 -6
- package/tsconfig.json +73 -73
- package/src/bot/logic/building/ArtilleryUnit.ts +0 -43
- package/src/bot/logic/building/building.ts +0 -127
|
@@ -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
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { ActionsApi, GameApi, MovementZone, PlayerData, Point2D } from "@chronodivide/game-api";
|
|
1
|
+
import maxBy from "lodash.maxby";
|
|
2
|
+
import { ActionsApi, GameApi, MovementZone, PlayerData, Point2D, UnitData } from "@chronodivide/game-api";
|
|
3
3
|
import { Squad } from "../squad.js";
|
|
4
4
|
import { SquadAction, SquadBehaviour, grabCombatants, noop } from "../squadBehaviour.js";
|
|
5
5
|
import { MatchAwareness } from "../../awareness.js";
|
|
6
6
|
import { getDistanceBetweenPoints } from "../../map/map.js";
|
|
7
|
-
import { manageAttackMicro, manageMoveMicro } from "./common.js";
|
|
7
|
+
import { getAttackWeight, 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,12 @@ 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 (
|
|
58
|
+
if (
|
|
59
|
+
squad.getUnitIds().length > 0 &&
|
|
60
|
+
(!this.lastCommand || gameApi.getCurrentTick() > this.lastCommand + TARGET_UPDATE_INTERVAL_TICKS)
|
|
61
|
+
) {
|
|
57
62
|
this.lastCommand = gameApi.getCurrentTick();
|
|
58
63
|
const centerOfMass = squad.getCenterOfMass();
|
|
59
64
|
const maxDistance = squad.getMaxDistanceToCenterOfMass();
|
|
@@ -81,6 +86,7 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
81
86
|
manageMoveMicro(actionsApi, unit, centerOfMass);
|
|
82
87
|
});
|
|
83
88
|
} else {
|
|
89
|
+
logger(`CombatSquad ${squad.getName()} switching back to attack mode (${maxDistance})`);
|
|
84
90
|
this.state = SquadState.Attacking;
|
|
85
91
|
}
|
|
86
92
|
} else {
|
|
@@ -93,23 +99,21 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
93
99
|
maxDistance > requiredGatherRadius
|
|
94
100
|
) {
|
|
95
101
|
// Switch back to gather mode
|
|
102
|
+
logger(`CombatSquad ${squad.getName()} switching back to gather (${maxDistance})`);
|
|
96
103
|
this.state = SquadState.Gathering;
|
|
97
104
|
return noop();
|
|
98
105
|
}
|
|
99
106
|
for (const unit of units) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
manageMoveMicro(actionsApi, unit, targetPoint);
|
|
112
|
-
}
|
|
107
|
+
const { rx: x, ry: y } = unit.tile;
|
|
108
|
+
const range = unit.primaryWeapon?.maxRange ?? unit.secondaryWeapon?.maxRange ?? 5;
|
|
109
|
+
const nearbyHostiles = matchAwareness
|
|
110
|
+
.getHostilesNearPoint(x, y, range * 2)
|
|
111
|
+
.map(({ unitId }) => gameApi.getUnitData(unitId)) as UnitData[];
|
|
112
|
+
const bestUnit = maxBy(nearbyHostiles, (target) => getAttackWeight(unit, target));
|
|
113
|
+
if (bestUnit) {
|
|
114
|
+
manageAttackMicro(actionsApi, unit, bestUnit);
|
|
115
|
+
} else {
|
|
116
|
+
manageMoveMicro(actionsApi, unit, targetPoint);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ActionsApi,
|
|
3
|
+
AttackState,
|
|
4
|
+
ObjectType,
|
|
5
|
+
OrderType,
|
|
6
|
+
Point2D,
|
|
7
|
+
StanceType,
|
|
8
|
+
UnitData,
|
|
9
|
+
ZoneType,
|
|
10
|
+
} from "@chronodivide/game-api";
|
|
11
|
+
import { getDistanceBetweenPoints, getDistanceBetweenUnits } from "../../map/map.js";
|
|
12
|
+
import { Zone } from "luxon";
|
|
3
13
|
|
|
4
14
|
// Micro methods
|
|
5
15
|
export function manageMoveMicro(actionsApi: ActionsApi, attacker: UnitData, attackPoint: Point2D) {
|
|
@@ -37,3 +47,24 @@ export function manageAttackMicro(actionsApi: ActionsApi, attacker: UnitData, ta
|
|
|
37
47
|
}
|
|
38
48
|
actionsApi.orderUnits([attacker.id], orderType, target.id);
|
|
39
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param attacker
|
|
54
|
+
* @param target
|
|
55
|
+
* @returns A number describing the weight of the given target for the attacker, or null if it should not attack it.
|
|
56
|
+
*/
|
|
57
|
+
export function getAttackWeight(attacker: UnitData, target: UnitData): number | null {
|
|
58
|
+
const { rx: x, ry: y } = attacker.tile;
|
|
59
|
+
const { rx: hX, ry: hY } = target.tile;
|
|
60
|
+
|
|
61
|
+
if (!attacker.primaryWeapon?.projectileRules.isAntiAir && target.zone === ZoneType.Air) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!attacker.primaryWeapon?.projectileRules.isAntiGround && target.zone === ZoneType.Ground) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 1000000 - getDistanceBetweenPoints({ x, y }, { x: hX, y: hY });
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ActionsApi, GameApi, OrderType, PlayerData, SideType } from "@chronodivide/game-api";
|
|
2
|
+
import { Squad } from "../squad.js";
|
|
3
|
+
import { SquadAction, SquadBehaviour, disband, noop, requestSpecificUnits, requestUnits } from "../squadBehaviour.js";
|
|
4
|
+
import { MatchAwareness } from "../../awareness.js";
|
|
5
|
+
|
|
6
|
+
const CAPTURE_COOLDOWN_TICKS = 30;
|
|
7
|
+
|
|
8
|
+
// Capture squad
|
|
9
|
+
export class EngineerSquad implements SquadBehaviour {
|
|
10
|
+
private hasAttemptedCaptureWith: {
|
|
11
|
+
unitId: number;
|
|
12
|
+
gameTick: number;
|
|
13
|
+
} | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param captureTarget ID of the target to try and capture/send engineer into.
|
|
17
|
+
*/
|
|
18
|
+
constructor(private captureTarget: number) {
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public onAiUpdate(
|
|
22
|
+
gameApi: GameApi,
|
|
23
|
+
actionsApi: ActionsApi,
|
|
24
|
+
playerData: PlayerData,
|
|
25
|
+
squad: Squad,
|
|
26
|
+
matchAwareness: MatchAwareness
|
|
27
|
+
): SquadAction {
|
|
28
|
+
const engineerTypes = ["ENGINEER", "SENGINEER"];
|
|
29
|
+
const engineers = squad.getUnitsOfTypes(gameApi, ...engineerTypes);
|
|
30
|
+
if (engineers.length === 0) {
|
|
31
|
+
// Perhaps we deployed already (or the unit was destroyed), end the mission.
|
|
32
|
+
if (this.hasAttemptedCaptureWith !== null) {
|
|
33
|
+
return disband();
|
|
34
|
+
}
|
|
35
|
+
return requestUnits(engineerTypes, 100);
|
|
36
|
+
} else if (
|
|
37
|
+
!this.hasAttemptedCaptureWith ||
|
|
38
|
+
gameApi.getCurrentTick() > this.hasAttemptedCaptureWith.gameTick + CAPTURE_COOLDOWN_TICKS
|
|
39
|
+
) {
|
|
40
|
+
actionsApi.orderUnits(
|
|
41
|
+
engineers.map((engineer) => engineer.id),
|
|
42
|
+
OrderType.Capture,
|
|
43
|
+
this.captureTarget
|
|
44
|
+
);
|
|
45
|
+
// Add a cooldown to deploy attempts.
|
|
46
|
+
this.hasAttemptedCaptureWith = {
|
|
47
|
+
unitId: engineers[0].id,
|
|
48
|
+
gameTick: gameApi.getCurrentTick(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return noop();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import { ActionsApi, GameApi, OrderType, PlayerData, Point2D
|
|
2
|
-
import { GlobalThreat } from "../../threat/threat.js";
|
|
1
|
+
import { ActionsApi, GameApi, OrderType, PlayerData, Point2D } from "@chronodivide/game-api";
|
|
3
2
|
import { Squad } from "../squad.js";
|
|
4
3
|
import { SquadAction, SquadBehaviour, disband, noop, requestUnits } from "../squadBehaviour.js";
|
|
5
4
|
import { MatchAwareness } from "../../awareness.js";
|
|
6
|
-
import {
|
|
5
|
+
import { DebugLogger } from "../../common/utils.js";
|
|
6
|
+
import { getDistanceBetweenPoints } from "../../map/map.js";
|
|
7
7
|
|
|
8
8
|
const SCOUT_MOVE_COOLDOWN_TICKS = 30;
|
|
9
9
|
|
|
10
|
+
// Max units to spend on a particular scout target.
|
|
11
|
+
const MAX_ATTEMPTS_PER_TARGET = 5;
|
|
12
|
+
|
|
13
|
+
// Maximum ticks to spend trying to scout a target *without making progress towards it*.
|
|
14
|
+
// Every time a unit gets closer to the target, the timer refreshes.
|
|
15
|
+
const MAX_TICKS_PER_TARGET = 600;
|
|
16
|
+
|
|
10
17
|
export class ScoutingSquad implements SquadBehaviour {
|
|
11
|
-
private
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
private scoutTarget: Point2D | null = null;
|
|
19
|
+
private attemptsOnCurrentTarget: number = 0;
|
|
20
|
+
private scoutTargetRefreshedAt: number = 0;
|
|
21
|
+
private lastMoveCommandTick: number = 0;
|
|
22
|
+
|
|
23
|
+
// Minimum distance from a scout to the target.
|
|
24
|
+
private scoutMinDistance?: number;
|
|
25
|
+
|
|
26
|
+
private hadUnit: boolean = false;
|
|
15
27
|
|
|
16
28
|
public onAiUpdate(
|
|
17
29
|
gameApi: GameApi,
|
|
@@ -19,6 +31,7 @@ export class ScoutingSquad implements SquadBehaviour {
|
|
|
19
31
|
playerData: PlayerData,
|
|
20
32
|
squad: Squad,
|
|
21
33
|
matchAwareness: MatchAwareness,
|
|
34
|
+
logger: DebugLogger,
|
|
22
35
|
): SquadAction {
|
|
23
36
|
const scoutNames = ["ADOG", "DOG", "E1", "E2", "FV", "HTK"];
|
|
24
37
|
const scouts = squad.getUnitsOfTypes(gameApi, ...scoutNames);
|
|
@@ -28,29 +41,69 @@ export class ScoutingSquad implements SquadBehaviour {
|
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
if (scouts.length === 0) {
|
|
31
|
-
|
|
44
|
+
// Count the number of times the scout dies trying to uncover the current scoutTarget.
|
|
45
|
+
if (this.scoutTarget && this.hadUnit) {
|
|
46
|
+
this.attemptsOnCurrentTarget++;
|
|
47
|
+
this.hadUnit = false;
|
|
48
|
+
}
|
|
32
49
|
return requestUnits(scoutNames, 100);
|
|
33
|
-
} else if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
} else if (this.scoutTarget) {
|
|
51
|
+
this.hadUnit = true;
|
|
52
|
+
if (this.attemptsOnCurrentTarget > MAX_ATTEMPTS_PER_TARGET) {
|
|
53
|
+
logger(
|
|
54
|
+
`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too many attempts, moving to next`,
|
|
55
|
+
);
|
|
56
|
+
this.setScoutTarget(null, 0);
|
|
57
|
+
return noop();
|
|
58
|
+
}
|
|
59
|
+
if (gameApi.getCurrentTick() > this.scoutTargetRefreshedAt + MAX_TICKS_PER_TARGET) {
|
|
60
|
+
logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} took too long, moving to next`);
|
|
61
|
+
this.setScoutTarget(null, 0);
|
|
62
|
+
return noop();
|
|
63
|
+
}
|
|
64
|
+
const targetTile = gameApi.mapApi.getTile(this.scoutTarget.x, this.scoutTarget.y);
|
|
65
|
+
if (!targetTile) {
|
|
66
|
+
throw new Error(`target tile ${this.scoutTarget.x},${this.scoutTarget.y} does not exist`);
|
|
67
|
+
}
|
|
68
|
+
if (gameApi.getCurrentTick() > this.lastMoveCommandTick + SCOUT_MOVE_COOLDOWN_TICKS) {
|
|
69
|
+
this.lastMoveCommandTick = gameApi.getCurrentTick();
|
|
70
|
+
scouts.forEach((unit) => {
|
|
71
|
+
if (this.scoutTarget) {
|
|
72
|
+
actionsApi.orderUnits([unit.id], OrderType.AttackMove, this.scoutTarget.x, this.scoutTarget.y);
|
|
44
73
|
}
|
|
74
|
+
});
|
|
75
|
+
// Check that a scout is actually moving closer to the target.
|
|
76
|
+
const distances = scouts.map((unit) =>
|
|
77
|
+
getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, this.scoutTarget!),
|
|
78
|
+
);
|
|
79
|
+
const newMinDistance = Math.min(...distances);
|
|
80
|
+
if (!this.scoutMinDistance || newMinDistance < this.scoutMinDistance) {
|
|
81
|
+
logger(
|
|
82
|
+
`Scout timeout refreshed because unit moved closer to point (${newMinDistance} < ${this.scoutMinDistance})`,
|
|
83
|
+
);
|
|
84
|
+
this.scoutTargetRefreshedAt = gameApi.getCurrentTick();
|
|
85
|
+
this.scoutMinDistance = newMinDistance;
|
|
45
86
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
}
|
|
88
|
+
if (gameApi.mapApi.isVisibleTile(targetTile, playerData.name)) {
|
|
89
|
+
logger(`Scout target ${this.scoutTarget.x},${this.scoutTarget.y} successfully scouted, moving to next`);
|
|
90
|
+
this.setScoutTarget(null, gameApi.getCurrentTick());
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const candidatePoint = matchAwareness.getScoutingManager().getNewScoutTarget()?.asPoint2D();
|
|
94
|
+
if (!candidatePoint) {
|
|
95
|
+
logger(`No more scouting targets available, disbanding.`);
|
|
96
|
+
return disband();
|
|
97
|
+
}
|
|
98
|
+
this.setScoutTarget(candidatePoint, gameApi.getCurrentTick());
|
|
53
99
|
}
|
|
54
100
|
return noop();
|
|
55
101
|
}
|
|
102
|
+
|
|
103
|
+
setScoutTarget(point: Point2D | null, currentTick: number) {
|
|
104
|
+
this.attemptsOnCurrentTarget = 0;
|
|
105
|
+
this.scoutTargetRefreshedAt = currentTick;
|
|
106
|
+
this.scoutTarget = point;
|
|
107
|
+
this.scoutMinDistance = undefined;
|
|
108
|
+
}
|
|
56
109
|
}
|
|
@@ -4,7 +4,7 @@ import { GlobalThreat } from "../threat/threat.js";
|
|
|
4
4
|
import { SquadAction, SquadBehaviour, disband } from "./squadBehaviour.js";
|
|
5
5
|
import { MatchAwareness } from "../awareness.js";
|
|
6
6
|
import { getDistanceBetweenPoints } from "../map/map.js";
|
|
7
|
-
import
|
|
7
|
+
import { DebugLogger } from "../common/utils.js";
|
|
8
8
|
|
|
9
9
|
export enum SquadLiveness {
|
|
10
10
|
SquadDead,
|
|
@@ -41,7 +41,7 @@ const calculateCenterOfMass: (unitTiles: Tile[]) => {
|
|
|
41
41
|
|
|
42
42
|
// max distance of units to the center of mass
|
|
43
43
|
const distances = unitTiles.map((tile) => getDistanceBetweenPoints({ x: tile.rx, y: tile.ry }, centerOfMass));
|
|
44
|
-
const maxDistance =
|
|
44
|
+
const maxDistance = Math.max(...distances);
|
|
45
45
|
return { centerOfMass, maxDistance };
|
|
46
46
|
};
|
|
47
47
|
|
|
@@ -76,6 +76,7 @@ export class Squad {
|
|
|
76
76
|
actionsApi: ActionsApi,
|
|
77
77
|
playerData: PlayerData,
|
|
78
78
|
matchAwareness: MatchAwareness,
|
|
79
|
+
logger: DebugLogger,
|
|
79
80
|
): SquadAction {
|
|
80
81
|
this.updateLiveness(gameApi);
|
|
81
82
|
const movableUnitTiles = this.unitIds
|
|
@@ -100,8 +101,7 @@ export class Squad {
|
|
|
100
101
|
} else if (!this.mission) {
|
|
101
102
|
return disband();
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
-
return outcome;
|
|
104
|
+
return this.behaviour.onAiUpdate(gameApi, actionsApi, playerData, this, matchAwareness, logger);
|
|
105
105
|
}
|
|
106
106
|
public getMission(): Mission | null {
|
|
107
107
|
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
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Meta-controller for forming and controlling squads.
|
|
2
2
|
|
|
3
3
|
import { ActionsApi, GameApi, PlayerData, UnitData } from "@chronodivide/game-api";
|
|
4
|
-
import { GlobalThreat } from "../threat/threat.js";
|
|
5
4
|
import { Squad, SquadLiveness } from "./squad.js";
|
|
6
5
|
import {
|
|
7
6
|
SquadAction,
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
} from "./squadBehaviour.js";
|
|
14
13
|
import { MatchAwareness } from "../awareness.js";
|
|
15
14
|
import { getDistanceBetween } from "../map/map.js";
|
|
16
|
-
import
|
|
15
|
+
import countBy from "lodash.countby";
|
|
17
16
|
|
|
18
17
|
type SquadWithAction<T> = {
|
|
19
18
|
squad: Squad;
|
|
@@ -51,7 +50,7 @@ export class SquadController {
|
|
|
51
50
|
const squadActions: SquadWithAction<SquadAction>[] = this.squads.map((squad) => {
|
|
52
51
|
return {
|
|
53
52
|
squad,
|
|
54
|
-
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
|
|
53
|
+
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness, this.logger),
|
|
55
54
|
};
|
|
56
55
|
});
|
|
57
56
|
// Handle disbands and merges.
|
|
@@ -105,21 +104,43 @@ export class SquadController {
|
|
|
105
104
|
},
|
|
106
105
|
{} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
|
|
107
106
|
);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
|
|
108
|
+
// Map of Squad ID to Unit Type to Count.
|
|
109
|
+
const newSquadAssignments = Object.entries(unitIdToHighestRequest)
|
|
110
|
+
.flatMap(([id, request]) => {
|
|
111
|
+
const unitId = Number.parseInt(id);
|
|
112
|
+
const unit = gameApi.getUnitData(unitId);
|
|
113
|
+
const { squad: requestingSquad } = request;
|
|
114
|
+
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
115
|
+
if (!unit) {
|
|
116
|
+
this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
if (!this.unitIdToSquad.has(unitId)) {
|
|
120
|
+
this.addUnitToSquad(requestingSquad, unit);
|
|
121
|
+
return [{ unitName: unit?.name, squad: requestingSquad.getName() }];
|
|
122
|
+
}
|
|
123
|
+
return [];
|
|
124
|
+
})
|
|
125
|
+
.reduce(
|
|
126
|
+
(acc, curr) => {
|
|
127
|
+
if (!acc[curr.squad]) {
|
|
128
|
+
acc[curr.squad] = {};
|
|
129
|
+
}
|
|
130
|
+
if (!acc[curr.squad][curr.unitName]) {
|
|
131
|
+
acc[curr.squad][curr.unitName] = 0;
|
|
132
|
+
}
|
|
133
|
+
acc[curr.squad][curr.unitName] = acc[curr.squad][curr.unitName] + 1;
|
|
134
|
+
return acc;
|
|
135
|
+
},
|
|
136
|
+
{} as Record<string, Record<string, number>>,
|
|
137
|
+
);
|
|
138
|
+
Object.entries(newSquadAssignments).forEach(([squad, assignments]) => {
|
|
139
|
+
this.logger(
|
|
140
|
+
`Squad ${squad} received: ${Object.entries(assignments)
|
|
141
|
+
.map(([unitType, count]) => unitType + " x " + count)
|
|
142
|
+
.join(", ")}`,
|
|
143
|
+
);
|
|
123
144
|
});
|
|
124
145
|
|
|
125
146
|
// Request units by type
|
|
@@ -157,33 +178,57 @@ export class SquadController {
|
|
|
157
178
|
.filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
|
|
158
179
|
.map((unit) => unit!);
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
freeUnit.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
request.action.point.y
|
|
178
|
-
}`,
|
|
181
|
+
type AssignmentWithType = { unitName: string; squad: string; method: "type" | "grab" };
|
|
182
|
+
// [squadName][unitName]['type' | 'grab']
|
|
183
|
+
const newAssignmentsByType = freeUnits
|
|
184
|
+
.flatMap((freeUnit) => {
|
|
185
|
+
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
186
|
+
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
187
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
188
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
189
|
+
delete unitTypeToHighestRequest[freeUnit.name];
|
|
190
|
+
return [
|
|
191
|
+
{ unitName: freeUnit.name, squad: requestingSquad.getName(), method: "type" },
|
|
192
|
+
] as AssignmentWithType[];
|
|
193
|
+
} else if (grabRequests.length > 0) {
|
|
194
|
+
const grantedSquad = grabRequests.find((request) => {
|
|
195
|
+
return (
|
|
196
|
+
freeUnit.rules.isSelectableCombatant &&
|
|
197
|
+
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
|
|
179
198
|
);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
199
|
+
});
|
|
200
|
+
if (grantedSquad) {
|
|
201
|
+
this.addUnitToSquad(grantedSquad.squad, freeUnit);
|
|
202
|
+
return [
|
|
203
|
+
{ unitName: freeUnit.name, squad: grantedSquad.squad.getName(), method: "grab" },
|
|
204
|
+
] as AssignmentWithType[];
|
|
184
205
|
}
|
|
185
|
-
}
|
|
186
|
-
|
|
206
|
+
}
|
|
207
|
+
return [];
|
|
208
|
+
})
|
|
209
|
+
.reduce(
|
|
210
|
+
(acc, curr) => {
|
|
211
|
+
if (!acc[curr.squad]) {
|
|
212
|
+
acc[curr.squad] = {};
|
|
213
|
+
}
|
|
214
|
+
if (!acc[curr.squad][curr.unitName]) {
|
|
215
|
+
acc[curr.squad][curr.unitName] = { grab: 0, type: 0 };
|
|
216
|
+
}
|
|
217
|
+
acc[curr.squad][curr.unitName][curr.method] = acc[curr.squad][curr.unitName][curr.method] + 1;
|
|
218
|
+
return acc;
|
|
219
|
+
},
|
|
220
|
+
{} as Record<string, Record<string, Record<"type" | "grab", number>>>,
|
|
221
|
+
);
|
|
222
|
+
Object.entries(newAssignmentsByType).forEach(([squad, assignments]) => {
|
|
223
|
+
this.logger(
|
|
224
|
+
`Squad ${squad} received: ${Object.entries(assignments)
|
|
225
|
+
.flatMap(([unitType, methodToCount]) =>
|
|
226
|
+
Object.entries(methodToCount)
|
|
227
|
+
.filter(([, count]) => count > 0)
|
|
228
|
+
.map(([method, count]) => unitType + " x " + count + " (by " + method + ")"),
|
|
229
|
+
)
|
|
230
|
+
.join(", ")}`,
|
|
231
|
+
);
|
|
187
232
|
});
|
|
188
233
|
}
|
|
189
234
|
|
|
@@ -197,7 +242,7 @@ export class SquadController {
|
|
|
197
242
|
}
|
|
198
243
|
|
|
199
244
|
public debugSquads(gameApi: GameApi) {
|
|
200
|
-
const unitsInSquad = (unitIds: number[]) =>
|
|
245
|
+
const unitsInSquad = (unitIds: number[]) => countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
|
|
201
246
|
|
|
202
247
|
this.squads.forEach((squad) => {
|
|
203
248
|
this.logger(
|
package/src/exampleBot.ts
CHANGED
|
@@ -6,17 +6,17 @@ async function main() {
|
|
|
6
6
|
Ladder maps:
|
|
7
7
|
CDR2 1v1 2_malibu_cliffs_le.map
|
|
8
8
|
CDR2 1v1 4_country_swing_le_v2.map
|
|
9
|
-
CDR2 1v1 mp01t4.map
|
|
10
|
-
CDR2 1v1 tn04t2.map
|
|
11
|
-
CDR2 1v1 mp10s4.map
|
|
9
|
+
CDR2 1v1 mp01t4.map, large map, oil derricks
|
|
10
|
+
CDR2 1v1 tn04t2.map, small map
|
|
11
|
+
CDR2 1v1 mp10s4.map <- depth charge, naval map (not supported)
|
|
12
12
|
CDR2 1v1 heckcorners.map
|
|
13
13
|
CDR2 1v1 4_montana_dmz_le.map
|
|
14
14
|
CDR2 1v1 barrel.map
|
|
15
15
|
|
|
16
16
|
Other maps:
|
|
17
|
-
mp03t4
|
|
17
|
+
mp03t4 large map, no oil derricks
|
|
18
18
|
*/
|
|
19
|
-
const mapName = "
|
|
19
|
+
const mapName = "tn04t2.map";
|
|
20
20
|
// Bot names must be unique in online mode
|
|
21
21
|
const botName = `Joe${String(Date.now()).substr(-6)}`;
|
|
22
22
|
const otherBotName = `Bob${String(Date.now() + 1).substr(-6)}`;
|
|
@@ -48,7 +48,7 @@ async function main() {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const offlineSettings = {
|
|
51
|
-
agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "
|
|
51
|
+
agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "Russians", true)],
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
const game = await cdapi.createGame({
|