@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 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, (msg) => this.logBotStatus(msg));
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, (message) => this.logBotStatus(message));
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
- console.log(`[${this.getHumanTimestamp(this.gameApi)} ${this.name}] ${message}`);
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 = 20;
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)?.then((reason, squad) => {
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
- if (!attacker.canMove) {
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
- if (attacker.canMove && distance <= deployedWeaponRange * 0.8) {
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 (!attacker.canMove && distance > deployedWeaponRange) {
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
- console.log(`Retreat squad ordered ${squad.getUnitIds()} to retreat`);
19
- actionsApi.orderUnits(squad.getUnitIds(), OrderType.Move, this.retreatToPoint.x, this.retreatToPoint.y);
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 ((this.moveOrderSentAt && gameApi.getCurrentTick() > this.moveOrderSentAt + 60) ||
25
- (this.createdAt && gameApi.getCurrentTick() > this.createdAt + 240)) {
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, 100);
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, logger) {
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
  }
@@ -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
- const game = await cdapi.createGame({
26
- // Uncomment the following lines to play in real time versus the bot
27
- /*online: true,
28
- serverUrl: process.env.SERVER_URL!,
29
- clientUrl: process.env.CLIENT_URL!,
30
- agents: [new SupalosaBot(botName, "Americans"), { name: otherBotName, country: "French" }],*/
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
- //agents: [new SupalosaBot(botName, "Americans", false), new SupalosaBot(otherBotName, "Russians", false)],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supalosa/chronodivide-bot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Example bot for Chrono Divide",
5
5
  "repository": "https://github.com/Supalosa/supalosa-chronodivide-bot",
6
6
  "main": "dist/exampleBot.js",
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
- (msg) => this.logBotStatus(msg),
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, (message) =>
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
- console.log(`[${this.getHumanTimestamp(this.gameApi)} ${this.name}] ${message}`);
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
- .addMission(new AttackMission(squadName, 100, matchAwareness.getMainRallyPoint(), attackArea, attackRadius))
129
- ?.then((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
- });
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 = 20;
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)?.then(
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
- if (!attacker.canMove) {
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
- if (attacker.canMove && distance <= deployedWeaponRange * 0.8) {
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 (!attacker.canMove && distance > deployedWeaponRange) {
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
- console.log(`Retreat squad ordered ${squad.getUnitIds()} to retreat`);
32
- actionsApi.orderUnits(squad.getUnitIds(), OrderType.Move, this.retreatToPoint.x, this.retreatToPoint.y);
33
- if (!this.moveOrderSentAt) {
34
- this.moveOrderSentAt = gameApi.getCurrentTick();
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
- (this.moveOrderSentAt && gameApi.getCurrentTick() > this.moveOrderSentAt + 60) ||
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, 100);
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((prev, a) => {
92
- const squadWithAction = a as SquadWithAction<SquadActionRequestSpecificUnits>;
93
- const { unitIds } = squadWithAction.action;
94
- unitIds.forEach((unitId) => {
95
- if (prev.hasOwnProperty(unitId)) {
96
- if (prev[unitId].action.priority > prev[unitId].action.priority) {
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
- } else {
100
- prev[unitId] = squadWithAction;
101
- }
102
- });
103
- return prev;
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(`granting specific unit ${unitId} to squad ${requestingSquad.getName()} in mission ${missionName}`);
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((prev, a) => {
125
- const squadWithAction = a as SquadWithAction<SquadActionRequestUnits>;
126
- const { unitNames } = squadWithAction.action;
127
- unitNames.forEach((unitName) => {
128
- if (prev.hasOwnProperty(unitName)) {
129
- if (prev[unitName].action.priority > prev[unitName].action.priority) {
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
- } else {
133
- prev[unitName] = squadWithAction;
134
- }
135
- });
136
- return prev;
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
- const game = await cdapi.createGame({
30
- // Uncomment the following lines to play in real time versus the bot
31
- /*online: true,
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
- //agents: [new SupalosaBot(botName, "Americans", false), new SupalosaBot(otherBotName, "Russians", false)],
52
+ };
53
+
54
+ const game = await cdapi.createGame({
55
+ ...offlineSettings,
37
56
  buildOffAlly: false,
38
57
  cratesAppear: false,
39
58
  credits: 10000,