@supalosa/chronodivide-bot 0.1.1 → 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 +79 -46
- package/dist/bot/bot.js +33 -185
- package/dist/bot/logic/awareness.js +122 -0
- package/dist/bot/logic/building/basicGroundUnit.js +8 -6
- package/dist/bot/logic/building/building.js +8 -3
- package/dist/bot/logic/building/harvester.js +1 -1
- package/dist/bot/logic/building/queueController.js +4 -21
- package/dist/bot/logic/common/scout.js +10 -0
- package/dist/bot/logic/knowledge.js +1 -0
- package/dist/bot/logic/map/map.js +6 -0
- package/dist/bot/logic/map/sector.js +6 -1
- package/dist/bot/logic/mission/basicMission.js +1 -5
- package/dist/bot/logic/mission/expansionMission.js +22 -4
- package/dist/bot/logic/mission/mission.js +49 -2
- package/dist/bot/logic/mission/missionController.js +67 -34
- package/dist/bot/logic/mission/missionFactories.js +10 -0
- package/dist/bot/logic/mission/missions/attackMission.js +107 -0
- package/dist/bot/logic/mission/missions/defenceMission.js +62 -0
- package/dist/bot/logic/mission/missions/expansionMission.js +24 -0
- package/dist/bot/logic/mission/missions/oneTimeMission.js +26 -0
- package/dist/bot/logic/mission/missions/retreatMission.js +7 -0
- package/dist/bot/logic/mission/missions/scoutingMission.js +38 -0
- package/dist/bot/logic/squad/behaviours/attackSquad.js +82 -0
- package/dist/bot/logic/squad/behaviours/combatSquad.js +99 -0
- package/dist/bot/logic/squad/behaviours/common.js +39 -0
- package/dist/bot/logic/squad/behaviours/defenceSquad.js +48 -0
- package/dist/bot/logic/squad/behaviours/expansionSquad.js +42 -0
- package/dist/bot/logic/squad/behaviours/retreatSquad.js +27 -0
- package/dist/bot/logic/squad/behaviours/scoutingSquad.js +38 -0
- package/dist/bot/logic/squad/behaviours/squadExpansion.js +26 -13
- package/dist/bot/logic/squad/squad.js +68 -15
- package/dist/bot/logic/squad/squadBehaviour.js +5 -5
- package/dist/bot/logic/squad/squadBehaviours.js +6 -0
- package/dist/bot/logic/squad/squadController.js +116 -15
- package/dist/exampleBot.js +37 -6
- package/package.json +29 -24
- package/src/bot/bot.ts +180 -378
- package/src/bot/logic/awareness.ts +220 -0
- package/src/bot/logic/building/ArtilleryUnit.ts +2 -2
- package/src/bot/logic/building/antiGroundStaticDefence.ts +2 -2
- package/src/bot/logic/building/basicAirUnit.ts +2 -2
- package/src/bot/logic/building/basicBuilding.ts +2 -2
- package/src/bot/logic/building/basicGroundUnit.ts +83 -78
- package/src/bot/logic/building/building.ts +127 -120
- package/src/bot/logic/building/harvester.ts +27 -27
- package/src/bot/logic/building/powerPlant.ts +1 -1
- package/src/bot/logic/building/queueController.ts +17 -38
- package/src/bot/logic/building/resourceCollectionBuilding.ts +1 -1
- package/src/bot/logic/common/scout.ts +12 -0
- package/src/bot/logic/map/map.ts +11 -3
- package/src/bot/logic/map/sector.ts +136 -130
- package/src/bot/logic/mission/mission.ts +83 -47
- package/src/bot/logic/mission/missionController.ts +105 -51
- package/src/bot/logic/mission/missionFactories.ts +46 -0
- package/src/bot/logic/mission/missions/attackMission.ts +154 -0
- package/src/bot/logic/mission/missions/defenceMission.ts +104 -0
- package/src/bot/logic/mission/missions/expansionMission.ts +49 -0
- package/src/bot/logic/mission/missions/oneTimeMission.ts +32 -0
- package/src/bot/logic/mission/missions/retreatMission.ts +9 -0
- package/src/bot/logic/mission/missions/scoutingMission.ts +59 -0
- package/src/bot/logic/squad/behaviours/combatSquad.ts +125 -0
- package/src/bot/logic/squad/behaviours/common.ts +39 -0
- package/src/bot/logic/squad/behaviours/expansionSquad.ts +59 -0
- package/src/bot/logic/squad/behaviours/retreatSquad.ts +44 -0
- package/src/bot/logic/squad/behaviours/scoutingSquad.ts +56 -0
- package/src/bot/logic/squad/squad.ts +163 -97
- package/src/bot/logic/squad/squadBehaviour.ts +61 -43
- package/src/bot/logic/squad/squadBehaviours.ts +8 -0
- package/src/bot/logic/squad/squadController.ts +210 -66
- package/src/exampleBot.ts +41 -7
- package/tsconfig.json +1 -1
- package/src/bot/logic/mission/basicMission.ts +0 -42
- package/src/bot/logic/mission/expansionMission.ts +0 -25
- package/src/bot/logic/squad/behaviours/squadExpansion.ts +0 -33
|
@@ -1,43 +1,47 @@
|
|
|
1
1
|
// Meta-controller for forming and controlling squads.
|
|
2
2
|
import { SquadLiveness } from "./squad.js";
|
|
3
|
+
import { getDistanceBetween } from "../map/map.js";
|
|
4
|
+
import _ from "lodash";
|
|
3
5
|
export class SquadController {
|
|
4
|
-
constructor(
|
|
5
|
-
this.
|
|
6
|
-
this.
|
|
6
|
+
constructor(logger) {
|
|
7
|
+
this.logger = logger;
|
|
8
|
+
this.squads = [];
|
|
9
|
+
this.unitIdToSquad = new Map();
|
|
7
10
|
}
|
|
8
|
-
onAiUpdate(gameApi, playerData,
|
|
9
|
-
// Remove dead squads.
|
|
10
|
-
this.squads = this.squads.filter((squad) => squad.getLiveness()
|
|
11
|
+
onAiUpdate(gameApi, actionsApi, playerData, matchAwareness) {
|
|
12
|
+
// Remove dead squads or those where the mission is dead.
|
|
13
|
+
this.squads = this.squads.filter((squad) => squad.getLiveness() !== SquadLiveness.SquadDead);
|
|
11
14
|
this.squads.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
12
15
|
// Check for units in multiple squads, this shouldn't happen.
|
|
13
16
|
this.unitIdToSquad = new Map();
|
|
14
17
|
this.squads.forEach((squad) => {
|
|
15
18
|
squad.getUnitIds().forEach((unitId) => {
|
|
16
19
|
if (this.unitIdToSquad.has(unitId)) {
|
|
17
|
-
|
|
20
|
+
this.logger(`WARNING: unit ${unitId} is in multiple squads, please debug.`);
|
|
18
21
|
}
|
|
19
22
|
else {
|
|
20
23
|
this.unitIdToSquad.set(unitId, squad);
|
|
21
24
|
}
|
|
22
25
|
});
|
|
23
26
|
});
|
|
24
|
-
|
|
27
|
+
const squadActions = this.squads.map((squad) => {
|
|
25
28
|
return {
|
|
26
29
|
squad,
|
|
27
|
-
action: squad.onAiUpdate(gameApi, playerData,
|
|
30
|
+
action: squad.onAiUpdate(gameApi, actionsApi, playerData, matchAwareness),
|
|
28
31
|
};
|
|
29
32
|
});
|
|
30
33
|
// Handle disbands and merges.
|
|
31
|
-
const isDisband = (a) => a.type
|
|
32
|
-
const isMerge = (a) => a.type
|
|
34
|
+
const isDisband = (a) => a.type === "disband";
|
|
35
|
+
const isMerge = (a) => a.type === "mergeInto";
|
|
33
36
|
let disbandedSquads = new Set();
|
|
34
37
|
squadActions
|
|
35
38
|
.filter((a) => isDisband(a.action))
|
|
36
39
|
.forEach((a) => {
|
|
40
|
+
this.logger(`Squad ${a.squad.getName()} disbanding as requested.`);
|
|
41
|
+
a.squad.getMission()?.removeSquad();
|
|
37
42
|
a.squad.getUnitIds().forEach((unitId) => {
|
|
38
43
|
this.unitIdToSquad.delete(unitId);
|
|
39
44
|
});
|
|
40
|
-
a.squad.clearUnits();
|
|
41
45
|
disbandedSquads.add(a.squad.getName());
|
|
42
46
|
});
|
|
43
47
|
squadActions
|
|
@@ -45,14 +49,111 @@ export class SquadController {
|
|
|
45
49
|
.forEach((a) => {
|
|
46
50
|
let mergeInto = a.action;
|
|
47
51
|
if (disbandedSquads.has(mergeInto.mergeInto.getName())) {
|
|
48
|
-
|
|
52
|
+
this.logger(`Squad ${a.squad.getName()} tried to merge into disbanded squad ${mergeInto.mergeInto.getName()}, cancelling.`);
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
a.squad.getUnitIds().forEach((unitId) => mergeInto.mergeInto.addUnit(unitId));
|
|
52
56
|
disbandedSquads.add(a.squad.getName());
|
|
53
57
|
});
|
|
54
58
|
// remove disbanded and merged squads.
|
|
55
|
-
this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
56
|
-
//
|
|
59
|
+
this.squads = this.squads.filter((squad) => !disbandedSquads.has(squad.getName()));
|
|
60
|
+
// Request specific units by ID
|
|
61
|
+
const isRequestSpecific = (a) => a.type === "requestSpecific";
|
|
62
|
+
const unitIdToHighestRequest = squadActions
|
|
63
|
+
.filter((a) => isRequestSpecific(a.action))
|
|
64
|
+
.reduce((prev, a) => {
|
|
65
|
+
const squadWithAction = a;
|
|
66
|
+
const { unitIds } = squadWithAction.action;
|
|
67
|
+
unitIds.forEach((unitId) => {
|
|
68
|
+
if (prev.hasOwnProperty(unitId)) {
|
|
69
|
+
if (prev[unitId].action.priority > prev[unitId].action.priority) {
|
|
70
|
+
prev[unitId] = squadWithAction;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
prev[unitId] = squadWithAction;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return prev;
|
|
78
|
+
}, {});
|
|
79
|
+
Object.entries(unitIdToHighestRequest).forEach(([id, request]) => {
|
|
80
|
+
const unitId = Number.parseInt(id);
|
|
81
|
+
const unit = gameApi.getUnitData(unitId);
|
|
82
|
+
const { squad: requestingSquad } = request;
|
|
83
|
+
const missionName = requestingSquad.getMission()?.getUniqueName();
|
|
84
|
+
if (!unit) {
|
|
85
|
+
this.logger(`mission ${missionName} requested non-existent unit ${unitId}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!this.unitIdToSquad.has(unitId)) {
|
|
89
|
+
this.logger(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
|
|
90
|
+
this.addUnitToSquad(requestingSquad, unit);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Request units by type
|
|
94
|
+
const isRequest = (a) => a.type === "request";
|
|
95
|
+
const unitTypeToHighestRequest = squadActions
|
|
96
|
+
.filter((a) => isRequest(a.action))
|
|
97
|
+
.reduce((prev, a) => {
|
|
98
|
+
const squadWithAction = a;
|
|
99
|
+
const { unitNames } = squadWithAction.action;
|
|
100
|
+
unitNames.forEach((unitName) => {
|
|
101
|
+
if (prev.hasOwnProperty(unitName)) {
|
|
102
|
+
if (prev[unitName].action.priority > prev[unitName].action.priority) {
|
|
103
|
+
prev[unitName] = squadWithAction;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
prev[unitName] = squadWithAction;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return prev;
|
|
111
|
+
}, {});
|
|
112
|
+
// Request combat-capable units in an area
|
|
113
|
+
const isGrab = (a) => a.type === "requestCombatants";
|
|
114
|
+
const grabRequests = squadActions.filter((a) => isGrab(a.action));
|
|
115
|
+
// Find loose units
|
|
116
|
+
const unitIds = gameApi.getVisibleUnits(playerData.name, "self");
|
|
117
|
+
const freeUnits = unitIds
|
|
118
|
+
.map((unitId) => gameApi.getUnitData(unitId))
|
|
119
|
+
.filter((unit) => !!unit && !this.unitIdToSquad.has(unit.id || 0))
|
|
120
|
+
.map((unit) => unit);
|
|
121
|
+
freeUnits.forEach((freeUnit) => {
|
|
122
|
+
if (unitTypeToHighestRequest.hasOwnProperty(freeUnit.name)) {
|
|
123
|
+
const { squad: requestingSquad } = unitTypeToHighestRequest[freeUnit.name];
|
|
124
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()}`);
|
|
125
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
126
|
+
delete unitTypeToHighestRequest[freeUnit.name];
|
|
127
|
+
}
|
|
128
|
+
else if (grabRequests.length > 0) {
|
|
129
|
+
grabRequests.some((request) => {
|
|
130
|
+
const { squad: requestingSquad } = request;
|
|
131
|
+
if (freeUnit.rules.isSelectableCombatant &&
|
|
132
|
+
getDistanceBetween(freeUnit, request.action.point) <= request.action.radius) {
|
|
133
|
+
this.logger(`granting unit ${freeUnit.id}#${freeUnit.name} to squad ${requestingSquad.getName()} via grab at ${request.action.point.x},${request.action.point.y}`);
|
|
134
|
+
this.addUnitToSquad(requestingSquad, freeUnit);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
addUnitToSquad(squad, unit) {
|
|
145
|
+
squad.addUnit(unit.id);
|
|
146
|
+
this.unitIdToSquad.set(unit.id, squad);
|
|
147
|
+
}
|
|
148
|
+
registerSquad(squad) {
|
|
149
|
+
this.squads.push(squad);
|
|
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
|
+
});
|
|
57
158
|
}
|
|
58
159
|
}
|
package/dist/exampleBot.js
CHANGED
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
import { cdapi } from "@chronodivide/game-api";
|
|
2
|
-
import {
|
|
2
|
+
import { SupalosaBot } from "./bot/bot.js";
|
|
3
3
|
async function main() {
|
|
4
|
-
|
|
4
|
+
/*
|
|
5
|
+
Ladder maps:
|
|
6
|
+
CDR2 1v1 2_malibu_cliffs_le.map
|
|
7
|
+
CDR2 1v1 4_country_swing_le_v2.map
|
|
8
|
+
CDR2 1v1 mp01t4.map
|
|
9
|
+
CDR2 1v1 tn04t2.map
|
|
10
|
+
CDR2 1v1 mp10s4.map
|
|
11
|
+
CDR2 1v1 heckcorners.map
|
|
12
|
+
CDR2 1v1 4_montana_dmz_le.map
|
|
13
|
+
CDR2 1v1 barrel.map
|
|
14
|
+
|
|
15
|
+
Other maps:
|
|
16
|
+
mp03t4
|
|
17
|
+
*/
|
|
18
|
+
const mapName = "mp01t4.map";
|
|
5
19
|
// Bot names must be unique in online mode
|
|
6
20
|
const botName = `Joe${String(Date.now()).substr(-6)}`;
|
|
7
21
|
const otherBotName = `Bob${String(Date.now() + 1).substr(-6)}`;
|
|
8
22
|
await cdapi.init(process.env.MIX_DIR || "./");
|
|
9
23
|
console.log("Server URL: " + process.env.SERVER_URL);
|
|
10
24
|
console.log("Client URL: " + process.env.CLIENT_URL);
|
|
11
|
-
|
|
12
|
-
|
|
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 = {
|
|
13
39
|
online: true,
|
|
14
40
|
serverUrl: process.env.SERVER_URL,
|
|
15
41
|
clientUrl: process.env.CLIENT_URL,
|
|
16
|
-
agents: [new
|
|
17
|
-
|
|
42
|
+
agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }],
|
|
43
|
+
};
|
|
44
|
+
const offlineSettings = {
|
|
45
|
+
agents: [new SupalosaBot(botName, "French", false), new SupalosaBot(otherBotName, "French", true)],
|
|
46
|
+
};
|
|
47
|
+
const game = await cdapi.createGame({
|
|
48
|
+
...offlineSettings,
|
|
18
49
|
buildOffAlly: false,
|
|
19
50
|
cratesAppear: false,
|
|
20
51
|
credits: 10000,
|
package/package.json
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@supalosa/chronodivide-bot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Example bot for Chrono Divide",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@supalosa/chronodivide-bot",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Example bot for Chrono Divide",
|
|
5
|
+
"repository": "https://github.com/Supalosa/supalosa-chronodivide-bot",
|
|
6
|
+
"main": "dist/exampleBot.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p .",
|
|
10
|
+
"watch": "tsc -p . -w",
|
|
11
|
+
"start": "node . --es-module-specifier-resolution=node",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@timohausmann/quadtree-ts": "^2.0.0-beta.1",
|
|
17
|
+
"@types/node": "^14.17.32",
|
|
18
|
+
"prettier": "3.0.3",
|
|
19
|
+
"typescript": "^4.3.5"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@chronodivide/game-api": "^0.45.0",
|
|
23
|
+
"@types/lodash": "^4.14.199",
|
|
24
|
+
"@types/luxon": "^3.3.2",
|
|
25
|
+
"lodash": "^4.17.21",
|
|
26
|
+
"luxon": "^3.4.3",
|
|
27
|
+
"priority-queue-typescript": "^1.0.1"
|
|
28
|
+
}
|
|
29
|
+
}
|