@supalosa/chronodivide-bot 0.2.0 → 0.2.1
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 +8 -0
- package/dist/bot/bot.js +11 -7
- package/dist/bot/logic/building/building.js +2 -0
- package/dist/bot/logic/mission/missionController.js +2 -2
- package/dist/bot/logic/mission/missions/attackMission.js +2 -4
- package/dist/bot/logic/mission/missions/defenceMission.js +2 -2
- package/dist/bot/logic/squad/behaviours/combatSquad.js +1 -1
- package/dist/bot/logic/squad/behaviours/common.js +6 -4
- package/dist/bot/logic/squad/behaviours/retreatSquad.js +5 -10
- package/dist/bot/logic/squad/squadController.js +19 -9
- package/dist/exampleBot.js +23 -7
- package/package.json +1 -1
- package/src/bot/bot.ts +11 -9
- package/src/bot/logic/awareness.ts +1 -1
- package/src/bot/logic/building/building.ts +2 -0
- package/src/bot/logic/mission/missionController.ts +5 -3
- package/src/bot/logic/mission/missions/attackMission.ts +14 -12
- package/src/bot/logic/mission/missions/defenceMission.ts +2 -2
- package/src/bot/logic/squad/behaviours/combatSquad.ts +1 -1
- 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/squadController.ts +56 -36
- package/src/exampleBot.ts +25 -6
package/README.md
CHANGED
|
@@ -40,6 +40,14 @@ Contact the developer of Chrono Divide for details if you are seriously interest
|
|
|
40
40
|
npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" NODE_OPTIONS="--inspect" npm start
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Publishing
|
|
44
|
+
|
|
45
|
+
Have the npmjs token in ~/.npmrc or somewhere appropriate.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npm publish
|
|
49
|
+
```
|
|
50
|
+
|
|
43
51
|
# ignore me
|
|
44
52
|
|
|
45
53
|
export GAMEPATH="G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II"
|
package/dist/bot/bot.js
CHANGED
|
@@ -14,8 +14,8 @@ export class SupalosaBot extends Bot {
|
|
|
14
14
|
super(name, country);
|
|
15
15
|
this.tickOfLastAttackOrder = 0;
|
|
16
16
|
this.matchAwareness = null;
|
|
17
|
-
this.missionController = new MissionController((message) => this.logBotStatus(message));
|
|
18
|
-
this.squadController = new SquadController();
|
|
17
|
+
this.missionController = new MissionController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
18
|
+
this.squadController = new SquadController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
19
19
|
this.queueController = new QueueController();
|
|
20
20
|
this.enableLogging = enableLogging;
|
|
21
21
|
}
|
|
@@ -25,7 +25,7 @@ export class SupalosaBot extends Bot {
|
|
|
25
25
|
const botRate = botApm / 60;
|
|
26
26
|
this.tickRatio = Math.ceil(gameRate / botRate);
|
|
27
27
|
this.knownMapBounds = determineMapBounds(game.mapApi);
|
|
28
|
-
this.matchAwareness = new MatchAwarenessImpl(null, new SectorCache(game.mapApi, this.knownMapBounds), game.getPlayerData(this.name).startLocation, (
|
|
28
|
+
this.matchAwareness = new MatchAwarenessImpl(null, new SectorCache(game.mapApi, this.knownMapBounds), game.getPlayerData(this.name).startLocation, (message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
29
29
|
this.logBotStatus(`Map bounds: ${this.knownMapBounds.x}, ${this.knownMapBounds.y}`);
|
|
30
30
|
}
|
|
31
31
|
onGameTick(game) {
|
|
@@ -59,18 +59,22 @@ export class SupalosaBot extends Bot {
|
|
|
59
59
|
}
|
|
60
60
|
// Squad logic every 3 ticks
|
|
61
61
|
if (this.gameApi.getCurrentTick() % 3 === 0) {
|
|
62
|
-
this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness
|
|
62
|
+
this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
getHumanTimestamp(game) {
|
|
67
67
|
return Duration.fromMillis((game.getCurrentTick() / NATURAL_TICK_RATE) * 1000).toFormat("hh:mm:ss");
|
|
68
68
|
}
|
|
69
|
-
logBotStatus(message) {
|
|
69
|
+
logBotStatus(message, sayInGame = false) {
|
|
70
70
|
if (!this.enableLogging) {
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
const timestamp = this.getHumanTimestamp(this.gameApi);
|
|
74
|
+
console.log(`[${timestamp} ${this.name}] ${message}`);
|
|
75
|
+
if (sayInGame) {
|
|
76
|
+
this.actionsApi.sayAll(`${timestamp}: ${message}`);
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
logDebugState(game) {
|
|
76
80
|
if (!this.enableLogging) {
|
|
@@ -93,9 +97,9 @@ export class SupalosaBot extends Bot {
|
|
|
93
97
|
this.logBotStatus(`----- Cash: ${myPlayer.credits} ----- | Queues: ${queueState}`);
|
|
94
98
|
const harvesters = game.getVisibleUnits(this.name, "self", (r) => r.harvester).length;
|
|
95
99
|
this.logBotStatus(`Harvesters: ${harvesters}`);
|
|
100
|
+
this.squadController.debugSquads(this.gameApi);
|
|
96
101
|
this.logBotStatus(`----- End -----`);
|
|
97
102
|
this.missionController.logDebugOutput();
|
|
98
|
-
this.actionsApi.sayAll(`Cash: ${myPlayer.credits}`);
|
|
99
103
|
}
|
|
100
104
|
onGameEvent(ev) {
|
|
101
105
|
switch (ev.type) {
|
|
@@ -45,6 +45,7 @@ export const BUILDING_NAME_TO_RULES = new Map([
|
|
|
45
45
|
["GADEPT", new BasicBuilding(1, 1, 10000)],
|
|
46
46
|
["GAAIRC", new BasicBuilding(8, 1, 6000)],
|
|
47
47
|
["GATECH", new BasicBuilding(20, 1, 4000)],
|
|
48
|
+
["GAYARD", new BasicBuilding(0, 0, 0)],
|
|
48
49
|
["GAPILL", new AntiGroundStaticDefence(5, 1, 5)],
|
|
49
50
|
["ATESLA", new AntiGroundStaticDefence(5, 1, 10)],
|
|
50
51
|
["GAWALL", new AntiGroundStaticDefence(0, 0, 0)],
|
|
@@ -67,6 +68,7 @@ export const BUILDING_NAME_TO_RULES = new Map([
|
|
|
67
68
|
["NADEPT", new BasicBuilding(1, 1, 10000)],
|
|
68
69
|
["NARADR", new BasicBuilding(8, 1, 4000)],
|
|
69
70
|
["NANRCT", new PowerPlant()],
|
|
71
|
+
["NAYARD", new BasicBuilding(0, 0, 0)],
|
|
70
72
|
["NATECH", new BasicBuilding(20, 1, 4000)],
|
|
71
73
|
["NALASR", new AntiGroundStaticDefence(5, 1, 5)],
|
|
72
74
|
["TESLA", new AntiGroundStaticDefence(5, 1, 10)],
|
|
@@ -30,11 +30,11 @@ export class MissionController {
|
|
|
30
30
|
this.missions
|
|
31
31
|
.filter((missions) => disbandedMissions.has(missions.getUniqueName()))
|
|
32
32
|
.forEach((disbandedMission) => {
|
|
33
|
-
this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}`);
|
|
34
33
|
const reason = disbandedMissions.get(disbandedMission.getUniqueName());
|
|
34
|
+
this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}, reason: ${reason}, hasSquad: ${!!disbandedMission.getSquad}`);
|
|
35
35
|
disbandedMissionsArray.push({ mission: disbandedMission, reason });
|
|
36
|
-
disbandedMission.getSquad()?.setMission(null);
|
|
37
36
|
disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));
|
|
37
|
+
disbandedMission.getSquad()?.setMission(null);
|
|
38
38
|
});
|
|
39
39
|
this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
|
|
40
40
|
// Register new squads
|
|
@@ -96,11 +96,9 @@ export class AttackMissionFactory {
|
|
|
96
96
|
}
|
|
97
97
|
// TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
|
|
98
98
|
const squadName = "globalAttack";
|
|
99
|
-
const tryAttack = missionController
|
|
100
|
-
.addMission(new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius))
|
|
101
|
-
?.then((reason, squad) => {
|
|
99
|
+
const tryAttack = missionController.addMission(new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius).then((reason, squad) => {
|
|
102
100
|
missionController.addMission(new RetreatMission("retreat-from-" + squadName + gameApi.getCurrentTick(), 100, matchAwareness.getMainRallyPoint(), squad?.getUnitIds() ?? []));
|
|
103
|
-
});
|
|
101
|
+
}));
|
|
104
102
|
if (tryAttack) {
|
|
105
103
|
this.lastAttackAt = gameApi.getCurrentTick();
|
|
106
104
|
}
|
|
@@ -35,7 +35,7 @@ export class DefenceMission extends Mission {
|
|
|
35
35
|
}
|
|
36
36
|
const DEFENCE_CHECK_TICKS = 30;
|
|
37
37
|
// Starting radius around the player's base to trigger defense.
|
|
38
|
-
const DEFENCE_STARTING_RADIUS =
|
|
38
|
+
const DEFENCE_STARTING_RADIUS = 10;
|
|
39
39
|
// Every game tick, we increase the defendable area by this amount.
|
|
40
40
|
const DEFENCE_RADIUS_INCREASE_PER_GAME_TICK = 0.005;
|
|
41
41
|
export class DefenceMissionFactory {
|
|
@@ -53,7 +53,7 @@ export class DefenceMissionFactory {
|
|
|
53
53
|
const defendableRadius = DEFENCE_STARTING_RADIUS + DEFENCE_RADIUS_INCREASE_PER_GAME_TICK * gameApi.getCurrentTick();
|
|
54
54
|
const enemiesNearSpawn = matchAwareness.getHostilesNearPoint2d(playerData.startLocation, defendableRadius);
|
|
55
55
|
if (enemiesNearSpawn.length > 0) {
|
|
56
|
-
missionController.addMission(new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2)
|
|
56
|
+
missionController.addMission(new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2).then((reason, squad) => {
|
|
57
57
|
missionController.addMission(new RetreatMission("retreat-from-globalDefence" + gameApi.getCurrentTick(), 100, matchAwareness.getMainRallyPoint(), squad?.getUnitIds() ?? []));
|
|
58
58
|
}));
|
|
59
59
|
}
|
|
@@ -67,7 +67,7 @@ export class CombatSquad {
|
|
|
67
67
|
maxDistance &&
|
|
68
68
|
gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
|
|
69
69
|
maxDistance > requiredGatherRadius) {
|
|
70
|
-
// Switch back to gather mode
|
|
70
|
+
// Switch back to gather mode
|
|
71
71
|
this.state = SquadState.Gathering;
|
|
72
72
|
return noop();
|
|
73
73
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ObjectType, OrderType } from "@chronodivide/game-api";
|
|
1
|
+
import { AttackState, ObjectType, OrderType, StanceType } from "@chronodivide/game-api";
|
|
2
2
|
import { getDistanceBetweenUnits } from "../../map/map.js";
|
|
3
3
|
// Micro methods
|
|
4
4
|
export function manageMoveMicro(actionsApi, attacker, attackPoint) {
|
|
5
5
|
if (attacker.name === "E1") {
|
|
6
|
-
|
|
6
|
+
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
7
|
+
if (isDeployed) {
|
|
7
8
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
8
9
|
}
|
|
9
10
|
}
|
|
@@ -14,11 +15,12 @@ export function manageAttackMicro(actionsApi, attacker, target) {
|
|
|
14
15
|
if (attacker.name === "E1") {
|
|
15
16
|
// Para (deployed weapon) range is 5.
|
|
16
17
|
const deployedWeaponRange = attacker.secondaryWeapon?.maxRange || 5;
|
|
17
|
-
|
|
18
|
+
const isDeployed = attacker.stance === StanceType.Deployed;
|
|
19
|
+
if (!isDeployed && (distance <= deployedWeaponRange || attacker.attackState === AttackState.JustFired)) {
|
|
18
20
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
|
-
else if (
|
|
23
|
+
else if (isDeployed && distance > deployedWeaponRange) {
|
|
22
24
|
actionsApi.orderUnits([attacker.id], OrderType.DeploySelected);
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
@@ -5,8 +5,6 @@ export class RetreatSquad {
|
|
|
5
5
|
constructor(unitIds, retreatToPoint) {
|
|
6
6
|
this.unitIds = unitIds;
|
|
7
7
|
this.retreatToPoint = retreatToPoint;
|
|
8
|
-
this.hasRequestedUnits = false;
|
|
9
|
-
this.moveOrderSentAt = null;
|
|
10
8
|
this.createdAt = null;
|
|
11
9
|
}
|
|
12
10
|
onAiUpdate(gameApi, actionsApi, playerData, squad, matchAwareness) {
|
|
@@ -15,18 +13,15 @@ export class RetreatSquad {
|
|
|
15
13
|
}
|
|
16
14
|
if (squad.getUnitIds().length > 0) {
|
|
17
15
|
// Only send the order once we have managed to claim some units.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!this.moveOrderSentAt) {
|
|
21
|
-
this.moveOrderSentAt = gameApi.getCurrentTick();
|
|
22
|
-
}
|
|
16
|
+
actionsApi.orderUnits(squad.getUnitIds(), OrderType.AttackMove, this.retreatToPoint.x, this.retreatToPoint.y);
|
|
17
|
+
return disband();
|
|
23
18
|
}
|
|
24
|
-
if (
|
|
25
|
-
|
|
19
|
+
if (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240) {
|
|
20
|
+
// Disband automatically after 240 ticks in case we couldn't actually claim any units.
|
|
26
21
|
return disband();
|
|
27
22
|
}
|
|
28
23
|
else {
|
|
29
|
-
return requestSpecificUnits(this.unitIds,
|
|
24
|
+
return requestSpecificUnits(this.unitIds, 1000);
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// Meta-controller for forming and controlling squads.
|
|
2
2
|
import { SquadLiveness } from "./squad.js";
|
|
3
3
|
import { getDistanceBetween } from "../map/map.js";
|
|
4
|
+
import _ from "lodash";
|
|
4
5
|
export class SquadController {
|
|
5
|
-
constructor() {
|
|
6
|
+
constructor(logger) {
|
|
7
|
+
this.logger = logger;
|
|
6
8
|
this.squads = [];
|
|
7
9
|
this.unitIdToSquad = new Map();
|
|
8
10
|
}
|
|
9
|
-
onAiUpdate(gameApi, actionsApi, playerData, matchAwareness
|
|
11
|
+
onAiUpdate(gameApi, actionsApi, playerData, matchAwareness) {
|
|
10
12
|
// Remove dead squads or those where the mission is dead.
|
|
11
13
|
this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
|
|
12
14
|
this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
@@ -15,7 +17,7 @@ export class SquadController {
|
|
|
15
17
|
this.squads.forEach((squad) => {
|
|
16
18
|
squad.getUnitIds().forEach((unitId) => {
|
|
17
19
|
if (this.unitIdToSquad.has(unitId)) {
|
|
18
|
-
logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
20
|
+
this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
19
21
|
}
|
|
20
22
|
else {
|
|
21
23
|
this.unitIdToSquad.set(unitId, squad);
|
|
@@ -35,7 +37,7 @@ export class SquadController {
|
|
|
35
37
|
squadActions
|
|
36
38
|
.filter((a) => isDisband(a.action))
|
|
37
39
|
.forEach((a) => {
|
|
38
|
-
logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
40
|
+
this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
39
41
|
a.squad.getMission()?.removeSquad();
|
|
40
42
|
a.squad.getUnitIds().forEach((unitId) => {
|
|
41
43
|
this.unitIdToSquad.delete(unitId);
|
|
@@ -47,7 +49,7 @@ export class SquadController {
|
|
|
47
49
|
.forEach((a) => {
|
|
48
50
|
let mergeInto = a.action;
|
|
49
51
|
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
50
|
-
logger(`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`);
|
|
52
|
+
this.logger(`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`);
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
|
|
@@ -80,11 +82,11 @@ export class SquadController {
|
|
|
80
82
|
const { squad: requestingSquad } = request;
|
|
81
83
|
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
82
84
|
if (!unit) {
|
|
83
|
-
logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
85
|
+
this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
if (!this.unitIdToSquad.has(unitId)) {
|
|
87
|
-
logger(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
|
|
89
|
+
this.logger(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
|
|
88
90
|
this.addUnitToSquad(requestingSquad, unit);
|
|
89
91
|
}
|
|
90
92
|
});
|
|
@@ -119,7 +121,7 @@ export class SquadController {
|
|
|
119
121
|
freeUnits.forEach((freeUnit) => {
|
|
120
122
|
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
121
123
|
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
122
|
-
logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
124
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
123
125
|
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
124
126
|
delete unitTypeToHighestRequest[freeUnit.name];
|
|
125
127
|
}
|
|
@@ -128,7 +130,7 @@ export class SquadController {
|
|
|
128
130
|
const { squad: requestingSquad } = request;
|
|
129
131
|
if (freeUnit.rules.isSelectableCombatant &&
|
|
130
132
|
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius) {
|
|
131
|
-
logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${request.action.point.y}`);
|
|
133
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${request.action.point.y}`);
|
|
132
134
|
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
133
135
|
return true;
|
|
134
136
|
}
|
|
@@ -146,4 +148,12 @@ export class SquadController {
|
|
|
146
148
|
registerSquad(squad) {
|
|
147
149
|
this.squads.push(squad);
|
|
148
150
|
}
|
|
151
|
+
debugSquads(gameApi) {
|
|
152
|
+
const unitsInSquad = (unitIds) => _.countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
|
|
153
|
+
this.squads.forEach((squad) => {
|
|
154
|
+
this.logger(`Squad ${squad.getName()}: ${Object.entries(unitsInSquad(squad.getUnitIds()))
|
|
155
|
+
.map(([unitName, count]) => `${unitName} x ${count}`)
|
|
156
|
+
.join(", ")}`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
149
159
|
}
|
package/dist/exampleBot.js
CHANGED
|
@@ -22,14 +22,30 @@ async function main() {
|
|
|
22
22
|
await cdapi.init(process.env.MIX_DIR || "./");
|
|
23
23
|
console.log("Server URL: " + process.env.SERVER_URL);
|
|
24
24
|
console.log("Client URL: " + process.env.CLIENT_URL);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/*
|
|
26
|
+
Countries:
|
|
27
|
+
0=Americans
|
|
28
|
+
1=Alliance -> Korea
|
|
29
|
+
2=French
|
|
30
|
+
3=Germans
|
|
31
|
+
4=British
|
|
32
|
+
|
|
33
|
+
5=Africans -> Libya
|
|
34
|
+
6=Arabs -> Iraq
|
|
35
|
+
7=Confederation -> Cuba
|
|
36
|
+
8=Russians
|
|
37
|
+
*/
|
|
38
|
+
const onlineSettings = {
|
|
39
|
+
online: true,
|
|
40
|
+
serverUrl: process.env.SERVER_URL,
|
|
41
|
+
clientUrl: process.env.CLIENT_URL,
|
|
42
|
+
agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }],
|
|
43
|
+
};
|
|
44
|
+
const offlineSettings = {
|
|
31
45
|
agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "French", true)],
|
|
32
|
-
|
|
46
|
+
};
|
|
47
|
+
const game = await cdapi.createGame({
|
|
48
|
+
...offlineSettings,
|
|
33
49
|
buildOffAlly: false,
|
|
34
50
|
cratesAppear: false,
|
|
35
51
|
credits: 10000,
|
package/package.json
CHANGED
package/src/bot/bot.ts
CHANGED
|
@@ -37,8 +37,8 @@ export class SupalosaBot extends Bot {
|
|
|
37
37
|
|
|
38
38
|
constructor(name: string, country: string, enableLogging = true) {
|
|
39
39
|
super(name, country);
|
|
40
|
-
this.missionController = new MissionController((message) => this.logBotStatus(message));
|
|
41
|
-
this.squadController = new SquadController();
|
|
40
|
+
this.missionController = new MissionController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
41
|
+
this.squadController = new SquadController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
42
42
|
this.queueController = new QueueController();
|
|
43
43
|
this.enableLogging = enableLogging;
|
|
44
44
|
}
|
|
@@ -55,7 +55,7 @@ export class SupalosaBot extends Bot {
|
|
|
55
55
|
null,
|
|
56
56
|
new SectorCache(game.mapApi, this.knownMapBounds),
|
|
57
57
|
game.getPlayerData(this.name).startLocation,
|
|
58
|
-
(
|
|
58
|
+
(message, sayInGame) => this.logBotStatus(message, sayInGame),
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
this.logBotStatus(`Map bounds: ${this.knownMapBounds.x}, ${this.knownMapBounds.y}`);
|
|
@@ -116,9 +116,7 @@ export class SupalosaBot extends Bot {
|
|
|
116
116
|
|
|
117
117
|
// Squad logic every 3 ticks
|
|
118
118
|
if (this.gameApi.getCurrentTick() % 3 === 0) {
|
|
119
|
-
this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness
|
|
120
|
-
this.logBotStatus(message),
|
|
121
|
-
);
|
|
119
|
+
this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness);
|
|
122
120
|
}
|
|
123
121
|
}
|
|
124
122
|
}
|
|
@@ -127,11 +125,15 @@ export class SupalosaBot extends Bot {
|
|
|
127
125
|
return Duration.fromMillis((game.getCurrentTick() / NATURAL_TICK_RATE) * 1000).toFormat("hh:mm:ss");
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
private logBotStatus(message: string) {
|
|
128
|
+
private logBotStatus(message: string, sayInGame: boolean = false) {
|
|
131
129
|
if (!this.enableLogging) {
|
|
132
130
|
return;
|
|
133
131
|
}
|
|
134
|
-
|
|
132
|
+
const timestamp = this.getHumanTimestamp(this.gameApi);
|
|
133
|
+
console.log(`[${timestamp} ${this.name}] ${message}`);
|
|
134
|
+
if (sayInGame) {
|
|
135
|
+
this.actionsApi.sayAll(`${timestamp}: ${message}`);
|
|
136
|
+
}
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
private logDebugState(game: GameApi) {
|
|
@@ -157,9 +159,9 @@ export class SupalosaBot extends Bot {
|
|
|
157
159
|
this.logBotStatus(`----- Cash: ${myPlayer.credits} ----- | Queues: ${queueState}`);
|
|
158
160
|
const harvesters = game.getVisibleUnits(this.name, "self", (r) => r.harvester).length;
|
|
159
161
|
this.logBotStatus(`Harvesters: ${harvesters}`);
|
|
162
|
+
this.squadController.debugSquads(this.gameApi);
|
|
160
163
|
this.logBotStatus(`----- End -----`);
|
|
161
164
|
this.missionController.logDebugOutput();
|
|
162
|
-
this.actionsApi.sayAll(`Cash: ${myPlayer.credits}`);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
override onGameEvent(ev: ApiEvent) {
|
|
@@ -69,7 +69,7 @@ export class MatchAwarenessImpl implements MatchAwareness {
|
|
|
69
69
|
private threatCache: GlobalThreat | null,
|
|
70
70
|
private sectorCache: SectorCache,
|
|
71
71
|
private mainRallyPoint: Point2D,
|
|
72
|
-
private logger: (message: string) => void,
|
|
72
|
+
private logger: (message: string, sayInGame?: boolean) => void,
|
|
73
73
|
) {
|
|
74
74
|
const { x: width, y: height } = sectorCache.getMapBounds();
|
|
75
75
|
this.hostileQuadTree = new Quadtree({ width, height });
|
|
@@ -84,6 +84,7 @@ export const BUILDING_NAME_TO_RULES = new Map<string, AiBuildingRules>([
|
|
|
84
84
|
["GAAIRC", new BasicBuilding(8, 1, 6000)], // Airforce Command
|
|
85
85
|
|
|
86
86
|
["GATECH", new BasicBuilding(20, 1, 4000)], // Allied Battle Lab
|
|
87
|
+
["GAYARD", new BasicBuilding(0, 0, 0)], // Naval Yard, disabled
|
|
87
88
|
|
|
88
89
|
["GAPILL", new AntiGroundStaticDefence(5, 1, 5)], // Pillbox
|
|
89
90
|
["ATESLA", new AntiGroundStaticDefence(5, 1, 10)], // Prism Cannon
|
|
@@ -109,6 +110,7 @@ export const BUILDING_NAME_TO_RULES = new Map<string, AiBuildingRules>([
|
|
|
109
110
|
["NADEPT", new BasicBuilding(1, 1, 10000)], // Repair Depot
|
|
110
111
|
["NARADR", new BasicBuilding(8, 1, 4000)], // Radar
|
|
111
112
|
["NANRCT", new PowerPlant()], // Nuclear Reactor
|
|
113
|
+
["NAYARD", new BasicBuilding(0, 0, 0)], // Naval Yard, disabled
|
|
112
114
|
|
|
113
115
|
["NATECH", new BasicBuilding(20, 1, 4000)], // Soviet Battle Lab
|
|
114
116
|
|
|
@@ -13,7 +13,7 @@ export class MissionController {
|
|
|
13
13
|
|
|
14
14
|
private forceDisbandedMissions: string[] = [];
|
|
15
15
|
|
|
16
|
-
constructor(private logger: (message: string) => void) {
|
|
16
|
+
constructor(private logger: (message: string, sayInGame?: boolean) => void) {
|
|
17
17
|
this.missionFactories = createMissionFactories();
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -48,11 +48,13 @@ export class MissionController {
|
|
|
48
48
|
this.missions
|
|
49
49
|
.filter((missions) => disbandedMissions.has(missions.getUniqueName()))
|
|
50
50
|
.forEach((disbandedMission) => {
|
|
51
|
-
this.logger(`mission disbanded: ${disbandedMission.getUniqueName()}`);
|
|
52
51
|
const reason = disbandedMissions.get(disbandedMission.getUniqueName());
|
|
52
|
+
this.logger(
|
|
53
|
+
`mission disbanded: ${disbandedMission.getUniqueName()}, reason: ${reason}, hasSquad: ${!!disbandedMission.getSquad}`,
|
|
54
|
+
);
|
|
53
55
|
disbandedMissionsArray.push({ mission: disbandedMission, reason });
|
|
54
|
-
disbandedMission.getSquad()?.setMission(null);
|
|
55
56
|
disbandedMission.endMission(disbandedMissions.get(disbandedMission.getUniqueName()));
|
|
57
|
+
disbandedMission.getSquad()?.setMission(null);
|
|
56
58
|
});
|
|
57
59
|
this.missions = this.missions.filter((missions) => !disbandedMissions.has(missions.getUniqueName()));
|
|
58
60
|
|
|
@@ -124,18 +124,20 @@ export class AttackMissionFactory implements MissionFactory {
|
|
|
124
124
|
// TODO: not using a fixed value here. But performance slows to a crawl when this is unique.
|
|
125
125
|
const squadName = "globalAttack";
|
|
126
126
|
|
|
127
|
-
const tryAttack = missionController
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
const tryAttack = missionController.addMission(
|
|
128
|
+
new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius).then(
|
|
129
|
+
(reason, squad) => {
|
|
130
|
+
missionController.addMission(
|
|
131
|
+
new RetreatMission(
|
|
132
|
+
"retreat-from-" + squadName + gameApi.getCurrentTick(),
|
|
133
|
+
100,
|
|
134
|
+
matchAwareness.getMainRallyPoint(),
|
|
135
|
+
squad?.getUnitIds() ?? [],
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
);
|
|
139
141
|
if (tryAttack) {
|
|
140
142
|
this.lastAttackAt = gameApi.getCurrentTick();
|
|
141
143
|
}
|
|
@@ -47,7 +47,7 @@ export class DefenceMission extends Mission<DefenceFailReason> {
|
|
|
47
47
|
const DEFENCE_CHECK_TICKS = 30;
|
|
48
48
|
|
|
49
49
|
// Starting radius around the player's base to trigger defense.
|
|
50
|
-
const DEFENCE_STARTING_RADIUS =
|
|
50
|
+
const DEFENCE_STARTING_RADIUS = 10;
|
|
51
51
|
// Every game tick, we increase the defendable area by this amount.
|
|
52
52
|
const DEFENCE_RADIUS_INCREASE_PER_GAME_TICK = 0.005;
|
|
53
53
|
|
|
@@ -77,7 +77,7 @@ export class DefenceMissionFactory implements MissionFactory {
|
|
|
77
77
|
|
|
78
78
|
if (enemiesNearSpawn.length > 0) {
|
|
79
79
|
missionController.addMission(
|
|
80
|
-
new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2)
|
|
80
|
+
new DefenceMission("globalDefence", 1000, playerData.startLocation, defendableRadius * 1.2).then(
|
|
81
81
|
(reason, squad) => {
|
|
82
82
|
missionController.addMission(
|
|
83
83
|
new RetreatMission(
|
|
@@ -92,7 +92,7 @@ export class CombatSquad implements SquadBehaviour {
|
|
|
92
92
|
gameApi.mapApi.getTile(centerOfMass.x, centerOfMass.y) !== undefined &&
|
|
93
93
|
maxDistance > requiredGatherRadius
|
|
94
94
|
) {
|
|
95
|
-
// Switch back to gather mode
|
|
95
|
+
// Switch back to gather mode
|
|
96
96
|
this.state = SquadState.Gathering;
|
|
97
97
|
return noop();
|
|
98
98
|
}
|
|
@@ -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
|
}
|
|
@@ -13,6 +13,7 @@ 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";
|
|
16
17
|
|
|
17
18
|
type SquadWithAction<T> = {
|
|
18
19
|
squad: Squad;
|
|
@@ -23,14 +24,13 @@ export class SquadController {
|
|
|
23
24
|
private squads: Squad[] = [];
|
|
24
25
|
private unitIdToSquad: Map<number, Squad> = new Map();
|
|
25
26
|
|
|
26
|
-
constructor() {}
|
|
27
|
+
constructor(private logger: (message: string, sayInGame?: boolean) => void) {}
|
|
27
28
|
|
|
28
29
|
public onAiUpdate(
|
|
29
30
|
gameApi: GameApi,
|
|
30
31
|
actionsApi: ActionsApi,
|
|
31
32
|
playerData: PlayerData,
|
|
32
33
|
matchAwareness: MatchAwareness,
|
|
33
|
-
logger: (message: string) => void
|
|
34
34
|
) {
|
|
35
35
|
// Remove dead squads or those where the mission is dead.
|
|
36
36
|
this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
|
|
@@ -41,7 +41,7 @@ export class SquadController {
|
|
|
41
41
|
this.squads.forEach((squad) => {
|
|
42
42
|
squad.getUnitIds().forEach((unitId) => {
|
|
43
43
|
if (this.unitIdToSquad.has(unitId)) {
|
|
44
|
-
logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
44
|
+
this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
45
45
|
} else {
|
|
46
46
|
this.unitIdToSquad.set(unitId, squad);
|
|
47
47
|
}
|
|
@@ -61,7 +61,7 @@ export class SquadController {
|
|
|
61
61
|
squadActions
|
|
62
62
|
.filter((a) => isDisband(a.action))
|
|
63
63
|
.forEach((a) => {
|
|
64
|
-
logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
64
|
+
this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
65
65
|
a.squad.getMission()?.removeSquad();
|
|
66
66
|
a.squad.getUnitIds().forEach((unitId) => {
|
|
67
67
|
this.unitIdToSquad.delete(unitId);
|
|
@@ -73,8 +73,8 @@ export class SquadController {
|
|
|
73
73
|
.forEach((a) => {
|
|
74
74
|
let mergeInto = a.action as SquadActionMergeInto;
|
|
75
75
|
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
76
|
-
logger(
|
|
77
|
-
`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling
|
|
76
|
+
this.logger(
|
|
77
|
+
`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`,
|
|
78
78
|
);
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
@@ -88,31 +88,36 @@ export class SquadController {
|
|
|
88
88
|
const isRequestSpecific = (a: SquadAction) => a.type === "requestSpecific";
|
|
89
89
|
const unitIdToHighestRequest = squadActions
|
|
90
90
|
.filter((a) => isRequestSpecific(a.action))
|
|
91
|
-
.reduce(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (prev
|
|
91
|
+
.reduce(
|
|
92
|
+
(prev, a) => {
|
|
93
|
+
const squadWithAction = a as SquadWithAction<SquadActionRequestSpecificUnits>;
|
|
94
|
+
const { unitIds } = squadWithAction.action;
|
|
95
|
+
unitIds.forEach((unitId) => {
|
|
96
|
+
if (prev.hasOwnProperty(unitId)) {
|
|
97
|
+
if (prev[unitId].action.priority > prev[unitId].action.priority) {
|
|
98
|
+
prev[unitId] = squadWithAction;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
97
101
|
prev[unitId] = squadWithAction;
|
|
98
102
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
}, {} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>);
|
|
103
|
+
});
|
|
104
|
+
return prev;
|
|
105
|
+
},
|
|
106
|
+
{} as Record<number, SquadWithAction<SquadActionRequestSpecificUnits>>,
|
|
107
|
+
);
|
|
105
108
|
Object.entries(unitIdToHighestRequest).forEach(([id, request]) => {
|
|
106
109
|
const unitId = Number.parseInt(id);
|
|
107
110
|
const unit = gameApi.getUnitData(unitId);
|
|
108
111
|
const { squad: requestingSquad } = request;
|
|
109
112
|
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
110
113
|
if (!unit) {
|
|
111
|
-
logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
114
|
+
this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
114
117
|
if (!this.unitIdToSquad.has(unitId)) {
|
|
115
|
-
logger(
|
|
118
|
+
this.logger(
|
|
119
|
+
`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`,
|
|
120
|
+
);
|
|
116
121
|
this.addUnitToSquad(requestingSquad, unit);
|
|
117
122
|
}
|
|
118
123
|
});
|
|
@@ -121,25 +126,28 @@ export class SquadController {
|
|
|
121
126
|
const isRequest = (a: SquadAction) => a.type === "request";
|
|
122
127
|
const unitTypeToHighestRequest = squadActions
|
|
123
128
|
.filter((a) => isRequest(a.action))
|
|
124
|
-
.reduce(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (prev
|
|
129
|
+
.reduce(
|
|
130
|
+
(prev, a) => {
|
|
131
|
+
const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
|
|
132
|
+
const { unitNames } = squadWithAction.action;
|
|
133
|
+
unitNames.forEach((unitName) => {
|
|
134
|
+
if (prev.hasOwnProperty(unitName)) {
|
|
135
|
+
if (prev[unitName].action.priority > prev[unitName].action.priority) {
|
|
136
|
+
prev[unitName] = squadWithAction;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
130
139
|
prev[unitName] = squadWithAction;
|
|
131
140
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
}, {} as Record<string, SquadWithAction<SquadActionRequestUnits>>);
|
|
141
|
+
});
|
|
142
|
+
return prev;
|
|
143
|
+
},
|
|
144
|
+
{} as Record<string, SquadWithAction<SquadActionRequestUnits>>,
|
|
145
|
+
);
|
|
138
146
|
|
|
139
147
|
// Request combat-capable units in an area
|
|
140
148
|
const isGrab = (a: SquadAction) => a.type === "requestCombatants";
|
|
141
149
|
const grabRequests = squadActions.filter((a) =>
|
|
142
|
-
isGrab(a.action)
|
|
150
|
+
isGrab(a.action),
|
|
143
151
|
) as SquadWithAction<SquadActionGrabFreeCombatants>[];
|
|
144
152
|
|
|
145
153
|
// Find loose units
|
|
@@ -152,7 +160,7 @@ export class SquadController {
|
|
|
152
160
|
freeUnits.forEach((freeUnit) => {
|
|
153
161
|
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
154
162
|
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
155
|
-
logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
163
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
156
164
|
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
157
165
|
delete unitTypeToHighestRequest[freeUnit.name];
|
|
158
166
|
} else if (grabRequests.length > 0) {
|
|
@@ -162,12 +170,12 @@ export class SquadController {
|
|
|
162
170
|
freeUnit.rules.isSelectableCombatant &&
|
|
163
171
|
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius
|
|
164
172
|
) {
|
|
165
|
-
logger(
|
|
173
|
+
this.logger(
|
|
166
174
|
`granting unit ${freeUnit.id}#${
|
|
167
175
|
freeUnit.name
|
|
168
176
|
} to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${
|
|
169
177
|
request.action.point.y
|
|
170
|
-
}
|
|
178
|
+
}`,
|
|
171
179
|
);
|
|
172
180
|
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
173
181
|
return true;
|
|
@@ -187,4 +195,16 @@ export class SquadController {
|
|
|
187
195
|
public registerSquad(squad: Squad) {
|
|
188
196
|
this.squads.push(squad);
|
|
189
197
|
}
|
|
198
|
+
|
|
199
|
+
public debugSquads(gameApi: GameApi) {
|
|
200
|
+
const unitsInSquad = (unitIds: number[]) => _.countBy(unitIds, (unitId) => gameApi.getUnitData(unitId)?.name);
|
|
201
|
+
|
|
202
|
+
this.squads.forEach((squad) => {
|
|
203
|
+
this.logger(
|
|
204
|
+
`Squad ${squad.getName()}: ${Object.entries(unitsInSquad(squad.getUnitIds()))
|
|
205
|
+
.map(([unitName, count]) => `${unitName} x ${count}`)
|
|
206
|
+
.join(", ")}`,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
190
210
|
}
|
package/src/exampleBot.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cdapi } from "@chronodivide/game-api";
|
|
1
|
+
import { Agent, Bot, cdapi } from "@chronodivide/game-api";
|
|
2
2
|
import { SupalosaBot } from "./bot/bot.js";
|
|
3
3
|
|
|
4
4
|
async function main() {
|
|
@@ -26,14 +26,33 @@ async function main() {
|
|
|
26
26
|
console.log("Server URL: " + process.env.SERVER_URL!);
|
|
27
27
|
console.log("Client URL: " + process.env.CLIENT_URL!);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/*
|
|
30
|
+
Countries:
|
|
31
|
+
0=Americans
|
|
32
|
+
1=Alliance -> Korea
|
|
33
|
+
2=French
|
|
34
|
+
3=Germans
|
|
35
|
+
4=British
|
|
36
|
+
|
|
37
|
+
5=Africans -> Libya
|
|
38
|
+
6=Arabs -> Iraq
|
|
39
|
+
7=Confederation -> Cuba
|
|
40
|
+
8=Russians
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const onlineSettings = {
|
|
44
|
+
online: true as true,
|
|
32
45
|
serverUrl: process.env.SERVER_URL!,
|
|
33
46
|
clientUrl: process.env.CLIENT_URL!,
|
|
34
|
-
agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }]
|
|
47
|
+
agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }] as [Bot, ...Agent[]],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const offlineSettings = {
|
|
35
51
|
agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "French", true)],
|
|
36
|
-
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const game = await cdapi.createGame({
|
|
55
|
+
...offlineSettings,
|
|
37
56
|
buildOffAlly: false,
|
|
38
57
|
cratesAppear: false,
|
|
39
58
|
credits: 10000,
|