@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
package/README.md
CHANGED
|
@@ -1,46 +1,79 @@
|
|
|
1
|
-
# Supalosa's Chrono
|
|
2
|
-
|
|
3
|
-
[Chrono Divide](https://chronodivide.com/) is a ground-up rebuild of Red Alert 2 in the browser. It is feature-complete and allows for online skirmish play against other players.
|
|
4
|
-
It also provides [an API to build bots](https://discord.com/channels/771701199812558848/842700851520339988), as there is no built-in AI yet.
|
|
5
|
-
|
|
6
|
-
This repository is one such implementation of a bot.
|
|
7
|
-
|
|
8
|
-
## Development State
|
|
9
|
-
|
|
10
|
-
Development on this is paused as I'm currently focusing on a Starcraft 2 bot instead.
|
|
11
|
-
The bot only plays Allied, and is not particularly good at the game. Feel free to use its code as a training dummy against your own bot, or extend it if you'd like. Caveat: I'm not a professional AI dev, this was my first foray into this, nor am I particularly experienced with TypeScript or JS.
|
|
12
|
-
|
|
13
|
-
## Future plans (on hold)
|
|
14
|
-
|
|
15
|
-
I was working on three things at once before I put this on hold:
|
|
16
|
-
|
|
17
|
-
- Task System - Something to not only follow actual build orders, but manage attacks, harass/attack the enemy, perform scouting, expand to other bases etc.
|
|
18
|
-
- Squad System - Ability to independently control more than one mass of units (i.e. squads), for example a Harass Squad directed by a Harass Task.
|
|
19
|
-
- Map Control System - Ability to analyse the state of the map and decide whether to fight for control over areas. Currently we already divide the map into square regions with individual threat calculations, but don't really do much with that information.
|
|
20
|
-
|
|
21
|
-
A lot of these concepts are being built into my Starcraft 2 bot, [Supabot](https://github.com/Supalosa/supabot) - maybe I'll come back to this when I'm done there.
|
|
22
|
-
|
|
23
|
-
## Install instructions
|
|
24
|
-
|
|
25
|
-
```sh
|
|
26
|
-
npm install
|
|
27
|
-
npm run build
|
|
28
|
-
npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm start
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
This will create a replay (`.rpl`) file that can be [imported into the live game](https://game.chronodivide.com/).
|
|
32
|
-
|
|
33
|
-
## Playing against the bot
|
|
34
|
-
|
|
35
|
-
Contact the developer of Chrono Divide for details if you are seriously interested in playing against a bot (this one or your own).
|
|
36
|
-
|
|
37
|
-
## Debugging
|
|
38
|
-
|
|
39
|
-
```sh
|
|
40
|
-
npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" NODE_OPTIONS="--inspect" npm start
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
# Supalosa's Chrono Divide Bot
|
|
2
|
+
|
|
3
|
+
[Chrono Divide](https://chronodivide.com/) is a ground-up rebuild of Red Alert 2 in the browser. It is feature-complete and allows for online skirmish play against other players.
|
|
4
|
+
It also provides [an API to build bots](https://discord.com/channels/771701199812558848/842700851520339988), as there is no built-in AI yet.
|
|
5
|
+
|
|
6
|
+
This repository is one such implementation of a bot.
|
|
7
|
+
|
|
8
|
+
## Development State
|
|
9
|
+
|
|
10
|
+
Development on this is paused as I'm currently focusing on a Starcraft 2 bot instead.
|
|
11
|
+
The bot only plays Allied, and is not particularly good at the game. Feel free to use its code as a training dummy against your own bot, or extend it if you'd like. Caveat: I'm not a professional AI dev, this was my first foray into this, nor am I particularly experienced with TypeScript or JS.
|
|
12
|
+
|
|
13
|
+
## Future plans (on hold)
|
|
14
|
+
|
|
15
|
+
I was working on three things at once before I put this on hold:
|
|
16
|
+
|
|
17
|
+
- Task System - Something to not only follow actual build orders, but manage attacks, harass/attack the enemy, perform scouting, expand to other bases etc.
|
|
18
|
+
- Squad System - Ability to independently control more than one mass of units (i.e. squads), for example a Harass Squad directed by a Harass Task.
|
|
19
|
+
- Map Control System - Ability to analyse the state of the map and decide whether to fight for control over areas. Currently we already divide the map into square regions with individual threat calculations, but don't really do much with that information.
|
|
20
|
+
|
|
21
|
+
A lot of these concepts are being built into my Starcraft 2 bot, [Supabot](https://github.com/Supalosa/supabot) - maybe I'll come back to this when I'm done there.
|
|
22
|
+
|
|
23
|
+
## Install instructions
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm start
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This will create a replay (`.rpl`) file that can be [imported into the live game](https://game.chronodivide.com/).
|
|
32
|
+
|
|
33
|
+
## Playing against the bot
|
|
34
|
+
|
|
35
|
+
Contact the developer of Chrono Divide for details if you are seriously interested in playing against a bot (this one or your own).
|
|
36
|
+
|
|
37
|
+
## Debugging
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" NODE_OPTIONS="--inspect" npm start
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Publishing
|
|
44
|
+
|
|
45
|
+
Have the npmjs token in ~/.npmrc or somewhere appropriate.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
npm publish
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
# ignore me
|
|
52
|
+
|
|
53
|
+
export GAMEPATH="G:\Origin\Ra2_YurisRevenge\Command and Conquer Red Alert II"
|
|
54
|
+
|
|
55
|
+
export GAMEPATH="D:\EA Games\Command and Conquer Red Alert II"
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
npx cross-env MIX_DIR="${GAMEPATH}" npm start
|
|
60
|
+
npx cross-env MIX_DIR="${GAMEPATH}" NODE_OPTIONS="--inspect" npm start
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
ladder maps: https://github.com/chronodivide/pvpgn-server/blob/26bbbe39613751cff696a73f087ce5b4cd938fc8/conf/bnmaps.conf.in#L321-L328
|
|
65
|
+
|
|
66
|
+
CDR2 1v1 2_malibu_cliffs_le.map
|
|
67
|
+
CDR2 1v1 4_country_swing_le_v2.map
|
|
68
|
+
CDR2 1v1 mp01t4.map
|
|
69
|
+
CDR2 1v1 tn04t2.map
|
|
70
|
+
CDR2 1v1 mp10s4.map
|
|
71
|
+
CDR2 1v1 heckcorners.map
|
|
72
|
+
CDR2 1v1 4_montana_dmz_le.map
|
|
73
|
+
CDR2 1v1 barrel.map
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
to play vs bot
|
|
78
|
+
export SERVER_URL="wss://<region_server>"
|
|
79
|
+
export CLIENT_URL="https://game.chronodivide.com/"
|
package/dist/bot/bot.js
CHANGED
|
@@ -1,30 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiEventType, Bot, QueueStatus, ObjectType, FactoryType, } from "@chronodivide/game-api";
|
|
2
2
|
import { Duration } from "luxon";
|
|
3
|
-
import { determineMapBounds
|
|
3
|
+
import { determineMapBounds } from "./logic/map/map.js";
|
|
4
4
|
import { SectorCache } from "./logic/map/sector.js";
|
|
5
5
|
import { MissionController } from "./logic/mission/missionController.js";
|
|
6
6
|
import { SquadController } from "./logic/squad/squadController.js";
|
|
7
|
-
import { calculateGlobalThreat } from "./logic/threat/threatCalculator.js";
|
|
8
7
|
import { QUEUES, QueueController, queueTypeToName } from "./logic/building/queueController.js";
|
|
9
|
-
|
|
10
|
-
(function (BotState) {
|
|
11
|
-
BotState["Initial"] = "init";
|
|
12
|
-
BotState["Deployed"] = "deployed";
|
|
13
|
-
BotState["Attacking"] = "attack";
|
|
14
|
-
BotState["Defending"] = "defend";
|
|
15
|
-
BotState["Scouting"] = "scout";
|
|
16
|
-
BotState["Defeated"] = "defeat";
|
|
17
|
-
})(BotState || (BotState = {}));
|
|
8
|
+
import { MatchAwarenessImpl } from "./logic/awareness.js";
|
|
18
9
|
const DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS = 60;
|
|
19
10
|
const NATURAL_TICK_RATE = 15;
|
|
20
|
-
const BOT_AUTO_SURRENDER_TIME_SECONDS = 7200; // 2 hours (approx 30 mins in real game)
|
|
21
|
-
export class
|
|
11
|
+
const BOT_AUTO_SURRENDER_TIME_SECONDS = 7200; // 7200; // 2 hours (approx 30 mins in real game)
|
|
12
|
+
export class SupalosaBot extends Bot {
|
|
22
13
|
constructor(name, country, enableLogging = true) {
|
|
23
14
|
super(name, country);
|
|
24
|
-
this.botState = BotState.Initial;
|
|
25
15
|
this.tickOfLastAttackOrder = 0;
|
|
26
|
-
this.
|
|
27
|
-
this.missionController = new MissionController();
|
|
16
|
+
this.matchAwareness = null;
|
|
17
|
+
this.missionController = new MissionController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
18
|
+
this.squadController = new SquadController((message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
28
19
|
this.queueController = new QueueController();
|
|
29
20
|
this.enableLogging = enableLogging;
|
|
30
21
|
}
|
|
@@ -33,42 +24,23 @@ export class ExampleBot extends Bot {
|
|
|
33
24
|
const botApm = 300;
|
|
34
25
|
const botRate = botApm / 60;
|
|
35
26
|
this.tickRatio = Math.ceil(gameRate / botRate);
|
|
36
|
-
this.enemyPlayers = game.getPlayers().filter((p) => p !== this.name && !game.areAlliedPlayers(this.name, p));
|
|
37
27
|
this.knownMapBounds = determineMapBounds(game.mapApi);
|
|
38
|
-
this.
|
|
39
|
-
this.threatCache = undefined;
|
|
28
|
+
this.matchAwareness = new MatchAwarenessImpl(null, new SectorCache(game.mapApi, this.knownMapBounds), game.getPlayerData(this.name).startLocation, (message, sayInGame) => this.logBotStatus(message, sayInGame));
|
|
40
29
|
this.logBotStatus(`Map bounds: ${this.knownMapBounds.x}, ${this.knownMapBounds.y}`);
|
|
41
30
|
}
|
|
42
31
|
onGameTick(game) {
|
|
32
|
+
if (!this.matchAwareness) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const threatCache = this.matchAwareness.getThreatCache();
|
|
43
36
|
if ((game.getCurrentTick() / NATURAL_TICK_RATE) % DEBUG_TIMESTAMP_OUTPUT_INTERVAL_SECONDS === 0) {
|
|
44
37
|
this.logDebugState(game);
|
|
45
38
|
}
|
|
46
39
|
if (game.getCurrentTick() % this.tickRatio === 0) {
|
|
47
40
|
const myPlayer = game.getPlayerData(this.name);
|
|
48
|
-
|
|
49
|
-
this.sectorCache?.updateSectors(game.getCurrentTick(), sectorsToUpdatePerCycle, game.mapApi, myPlayer);
|
|
50
|
-
let updateRatio = this.sectorCache?.getSectorUpdateRatio(game.getCurrentTick() - game.getTickRate() * 60);
|
|
51
|
-
if (updateRatio && updateRatio < 1.0) {
|
|
52
|
-
this.logBotStatus(`${updateRatio * 100.0}% of sectors updated in last 60 seconds.`);
|
|
53
|
-
}
|
|
54
|
-
// Threat decays over time if we haven't killed anything
|
|
55
|
-
let boredomFactor = 1.0 -
|
|
56
|
-
Math.min(1.0, Math.max(0.0, (this.gameApi.getCurrentTick() - this.tickOfLastAttackOrder) / 1600.0));
|
|
57
|
-
let shouldAttack = this.threatCache ? this.isWorthAttacking(this.threatCache, boredomFactor) : false;
|
|
58
|
-
if (game.getCurrentTick() % (this.tickRatio * 150) == 0) {
|
|
59
|
-
let visibility = this.sectorCache?.getOverallVisibility();
|
|
60
|
-
if (visibility) {
|
|
61
|
-
this.logBotStatus(`${Math.round(visibility * 1000.0) / 10}% of tiles visible. Calculating threat.`);
|
|
62
|
-
this.threatCache = calculateGlobalThreat(game, myPlayer, visibility);
|
|
63
|
-
this.logBotStatus(`Threat LAND: Them ${Math.round(this.threatCache.totalOffensiveLandThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiGroundFirepower)}.`);
|
|
64
|
-
this.logBotStatus(`Threat DEFENSIVE: Them ${Math.round(this.threatCache.totalDefensiveThreat)}, us: ${Math.round(this.threatCache.totalDefensivePower)}.`);
|
|
65
|
-
this.logBotStatus(`Threat AIR: Them ${Math.round(this.threatCache.totalOffensiveAirThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiAirFirepower)}.`);
|
|
66
|
-
this.logBotStatus(`Boredom: ${boredomFactor}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
41
|
+
this.matchAwareness.onAiUpdate(game, myPlayer);
|
|
69
42
|
if (game.getCurrentTick() / NATURAL_TICK_RATE > BOT_AUTO_SURRENDER_TIME_SECONDS) {
|
|
70
43
|
this.logBotStatus(`Auto-surrendering after ${BOT_AUTO_SURRENDER_TIME_SECONDS} seconds.`);
|
|
71
|
-
this.botState = BotState.Defeated;
|
|
72
44
|
this.actionsApi.quitGame();
|
|
73
45
|
}
|
|
74
46
|
// hacky resign condition
|
|
@@ -77,149 +49,37 @@ export class ExampleBot extends Bot {
|
|
|
77
49
|
const productionBuildings = game.getVisibleUnits(this.name, "self", (r) => r.type == ObjectType.Building && r.factory != FactoryType.None);
|
|
78
50
|
if (armyUnits.length == 0 && productionBuildings.length == 0 && mcvUnits.length == 0) {
|
|
79
51
|
this.logBotStatus(`No army or production left, quitting.`);
|
|
80
|
-
this.botState = BotState.Defeated;
|
|
81
52
|
this.actionsApi.quitGame();
|
|
82
53
|
}
|
|
83
54
|
// Build logic.
|
|
84
|
-
this.queueController.onAiUpdate(game, this.productionApi, this.actionsApi, myPlayer,
|
|
85
|
-
// Mission logic
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let conYards = game.getVisibleUnits(this.name, "self", (r) => r.constructionYard);
|
|
93
|
-
if (conYards.length) {
|
|
94
|
-
this.botState = BotState.Deployed;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
const units = game.getVisibleUnits(this.name, "self", (r) => baseUnits.includes(r.name));
|
|
98
|
-
if (units.length) {
|
|
99
|
-
this.actionsApi.orderUnits([units[0]], OrderType.DeploySelected);
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case BotState.Deployed: {
|
|
104
|
-
this.botState = BotState.Attacking;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case BotState.Attacking: {
|
|
108
|
-
const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
|
|
109
|
-
if (!shouldAttack) {
|
|
110
|
-
this.logBotStatus(`Not worth attacking, reverting to defence.`);
|
|
111
|
-
this.botState = BotState.Defending;
|
|
112
|
-
}
|
|
113
|
-
const enemyBuildings = game.getVisibleUnits(this.name, "hostile");
|
|
114
|
-
let foundTarget = false;
|
|
115
|
-
if (enemyBuildings.length) {
|
|
116
|
-
const weightedTargets = enemyBuildings
|
|
117
|
-
.filter((unit) => this.isHostileUnit(game, unit))
|
|
118
|
-
.map((unitId) => {
|
|
119
|
-
let unit = game.getUnitData(unitId);
|
|
120
|
-
return {
|
|
121
|
-
unit,
|
|
122
|
-
unitId: unitId,
|
|
123
|
-
weight: getDistanceBetweenPoints(myPlayer.startLocation, {
|
|
124
|
-
x: unit.tile.rx,
|
|
125
|
-
y: unit.tile.rx,
|
|
126
|
-
}),
|
|
127
|
-
};
|
|
128
|
-
})
|
|
129
|
-
.filter((unit) => unit.unit != null);
|
|
130
|
-
weightedTargets.sort((targetA, targetB) => {
|
|
131
|
-
return targetA.weight - targetB.weight;
|
|
132
|
-
});
|
|
133
|
-
const target = weightedTargets.find((_) => true);
|
|
134
|
-
if (target !== undefined) {
|
|
135
|
-
let targetData = target.unit;
|
|
136
|
-
for (const unitId of armyUnits) {
|
|
137
|
-
const unit = game.getUnitData(unitId);
|
|
138
|
-
foundTarget = true;
|
|
139
|
-
if (shouldAttack && unit?.isIdle) {
|
|
140
|
-
let orderType = OrderType.AttackMove;
|
|
141
|
-
if (targetData?.type == ObjectType.Building) {
|
|
142
|
-
orderType = OrderType.Attack;
|
|
143
|
-
}
|
|
144
|
-
else if (targetData?.rules.canDisguise) {
|
|
145
|
-
// Special case for mirage tank/spy as otherwise they just sit next to it.
|
|
146
|
-
orderType = OrderType.Attack;
|
|
147
|
-
}
|
|
148
|
-
this.actionsApi.orderUnits([unitId], orderType, target.unitId);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (!foundTarget) {
|
|
154
|
-
this.logBotStatus(`Can't see any targets, scouting.`);
|
|
155
|
-
this.botState = BotState.Scouting;
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
case BotState.Defending: {
|
|
160
|
-
const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
|
|
161
|
-
const enemy = game.getPlayerData(this.enemyPlayers[0]);
|
|
162
|
-
const fallbackPoint = getPointTowardsOtherPoint(game, myPlayer.startLocation, enemy.startLocation, 10, 10, 0);
|
|
163
|
-
armyUnits.forEach((armyUnitId) => {
|
|
164
|
-
let unit = game.getUnitData(armyUnitId);
|
|
165
|
-
if (unit && !unit.guardMode) {
|
|
166
|
-
let distanceToFallback = getDistanceBetweenPoints({ x: unit.tile.rx, y: unit.tile.ry }, fallbackPoint);
|
|
167
|
-
if (distanceToFallback > 10) {
|
|
168
|
-
this.actionsApi.orderUnits([armyUnitId], OrderType.GuardArea, fallbackPoint.x, fallbackPoint.y);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
if (shouldAttack) {
|
|
173
|
-
this.logBotStatus(`Finished defending, ready to attack.`);
|
|
174
|
-
this.botState = BotState.Attacking;
|
|
175
|
-
}
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
case BotState.Scouting: {
|
|
179
|
-
const armyUnits = game.getVisibleUnits(this.name, "self", (r) => r.isSelectableCombatant);
|
|
180
|
-
let candidatePoints = [];
|
|
181
|
-
// Move to an unseen starting location.
|
|
182
|
-
const unseenStartingLocations = game.mapApi.getStartingLocations().filter((startingLocation) => {
|
|
183
|
-
if (startingLocation == game.getPlayerData(this.name).startLocation) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
let tile = game.mapApi.getTile(startingLocation.x, startingLocation.y);
|
|
187
|
-
return tile ? !game.mapApi.isVisibleTile(tile, this.name) : false;
|
|
188
|
-
});
|
|
189
|
-
candidatePoints.push(...unseenStartingLocations);
|
|
190
|
-
armyUnits.forEach((unitId) => {
|
|
191
|
-
if (candidatePoints.length > 0) {
|
|
192
|
-
const unit = game.getUnitData(unitId);
|
|
193
|
-
if (unit?.isIdle) {
|
|
194
|
-
const scoutLocation = candidatePoints[Math.floor(game.generateRandom() * candidatePoints.length)];
|
|
195
|
-
this.actionsApi.orderUnits([unitId], OrderType.AttackMove, scoutLocation.x, scoutLocation.y);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
const enemyBuildings = game
|
|
200
|
-
.getVisibleUnits(this.name, "hostile")
|
|
201
|
-
.filter((unit) => this.isHostileUnit(game, unit));
|
|
202
|
-
if (enemyBuildings.length > 0) {
|
|
203
|
-
this.logBotStatus(`Scouted a target, reverting to attack mode.`);
|
|
204
|
-
this.botState = BotState.Attacking;
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
default:
|
|
209
|
-
break;
|
|
55
|
+
this.queueController.onAiUpdate(game, this.productionApi, this.actionsApi, myPlayer, threatCache, (message) => this.logBotStatus(message));
|
|
56
|
+
// Mission logic every 6 ticks
|
|
57
|
+
if (this.gameApi.getCurrentTick() % 6 === 0) {
|
|
58
|
+
this.missionController.onAiUpdate(game, myPlayer, this.matchAwareness, this.squadController);
|
|
59
|
+
}
|
|
60
|
+
// Squad logic every 3 ticks
|
|
61
|
+
if (this.gameApi.getCurrentTick() % 3 === 0) {
|
|
62
|
+
this.squadController.onAiUpdate(game, this.actionsApi, myPlayer, this.matchAwareness);
|
|
210
63
|
}
|
|
211
64
|
}
|
|
212
65
|
}
|
|
213
66
|
getHumanTimestamp(game) {
|
|
214
67
|
return Duration.fromMillis((game.getCurrentTick() / NATURAL_TICK_RATE) * 1000).toFormat("hh:mm:ss");
|
|
215
68
|
}
|
|
216
|
-
logBotStatus(message) {
|
|
69
|
+
logBotStatus(message, sayInGame = false) {
|
|
217
70
|
if (!this.enableLogging) {
|
|
218
71
|
return;
|
|
219
72
|
}
|
|
220
|
-
|
|
73
|
+
const timestamp = this.getHumanTimestamp(this.gameApi);
|
|
74
|
+
console.log(`[${timestamp} ${this.name}] ${message}`);
|
|
75
|
+
if (sayInGame) {
|
|
76
|
+
this.actionsApi.sayAll(`${timestamp}: ${message}`);
|
|
77
|
+
}
|
|
221
78
|
}
|
|
222
79
|
logDebugState(game) {
|
|
80
|
+
if (!this.enableLogging) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
223
83
|
const myPlayer = game.getPlayerData(this.name);
|
|
224
84
|
const queueState = QUEUES.reduce((prev, queueType) => {
|
|
225
85
|
if (this.productionApi.getQueueData(queueType).size === 0) {
|
|
@@ -237,21 +97,9 @@ export class ExampleBot extends Bot {
|
|
|
237
97
|
this.logBotStatus(`----- Cash: ${myPlayer.credits} ----- | Queues: ${queueState}`);
|
|
238
98
|
const harvesters = game.getVisibleUnits(this.name, "self", (r) => r.harvester).length;
|
|
239
99
|
this.logBotStatus(`Harvesters: ${harvesters}`);
|
|
100
|
+
this.squadController.debugSquads(this.gameApi);
|
|
240
101
|
this.logBotStatus(`----- End -----`);
|
|
241
|
-
|
|
242
|
-
isWorthAttacking(threatCache, threatFactor) {
|
|
243
|
-
let scaledGroundPower = Math.pow(threatCache.totalAvailableAntiGroundFirepower, 1.125);
|
|
244
|
-
let scaledGroundThreat = (threatFactor * threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat) * 1.1;
|
|
245
|
-
let scaledAirPower = Math.pow(threatCache.totalAvailableAirPower, 1.125);
|
|
246
|
-
let scaledAirThreat = (threatFactor * threatCache.totalOffensiveAntiAirThreat + threatCache.totalDefensiveThreat) * 1.1;
|
|
247
|
-
return scaledGroundPower > scaledGroundThreat || scaledAirPower > scaledAirThreat;
|
|
248
|
-
}
|
|
249
|
-
isHostileUnit(game, unitId) {
|
|
250
|
-
const unitData = game.getUnitData(unitId);
|
|
251
|
-
if (!unitData) {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
return unitData.owner != this.name && game.getPlayerData(unitData.owner)?.isCombatant;
|
|
102
|
+
this.missionController.logDebugOutput();
|
|
255
103
|
}
|
|
256
104
|
onGameEvent(ev) {
|
|
257
105
|
switch (ev.type) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ObjectType } from "@chronodivide/game-api";
|
|
2
|
+
import { calculateGlobalThreat } from "../logic/threat/threatCalculator.js";
|
|
3
|
+
import { getDistanceBetweenPoints, getPointTowardsOtherPoint } from "../logic/map/map.js";
|
|
4
|
+
import { Circle, Quadtree } from "@timohausmann/quadtree-ts";
|
|
5
|
+
const SECTORS_TO_UPDATE_PER_CYCLE = 8;
|
|
6
|
+
const RALLY_POINT_UPDATE_INTERVAL_TICKS = 60;
|
|
7
|
+
const THREAT_UPDATE_INTERVAL_TICKS = 30;
|
|
8
|
+
const rebuildQuadtree = (quadtree, units) => {
|
|
9
|
+
quadtree.clear();
|
|
10
|
+
units.forEach((unit) => {
|
|
11
|
+
quadtree.insert(new Circle({ x: unit.tile.rx, y: unit.tile.ry, r: 1, data: unit.id }));
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
export class MatchAwarenessImpl {
|
|
15
|
+
constructor(threatCache, sectorCache, mainRallyPoint, logger) {
|
|
16
|
+
this.threatCache = threatCache;
|
|
17
|
+
this.sectorCache = sectorCache;
|
|
18
|
+
this.mainRallyPoint = mainRallyPoint;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this._shouldAttack = false;
|
|
21
|
+
const { x: width, y: height } = sectorCache.getMapBounds();
|
|
22
|
+
this.hostileQuadTree = new Quadtree({ width, height });
|
|
23
|
+
}
|
|
24
|
+
getHostilesNearPoint2d(point, radius) {
|
|
25
|
+
return this.getHostilesNearPoint(point.x, point.y, radius);
|
|
26
|
+
}
|
|
27
|
+
getHostilesNearPoint(searchX, searchY, radius) {
|
|
28
|
+
const intersections = this.hostileQuadTree.retrieve(new Circle({ x: searchX, y: searchY, r: radius }));
|
|
29
|
+
return intersections
|
|
30
|
+
.map(({ x, y, data: unitId }) => ({ x, y, unitId: unitId }))
|
|
31
|
+
.filter(({ x, y }) => getDistanceBetweenPoints({ x, y }, { x: searchX, y: searchY }) <= radius)
|
|
32
|
+
.filter(({ unitId }) => !!unitId);
|
|
33
|
+
}
|
|
34
|
+
getThreatCache() {
|
|
35
|
+
return this.threatCache;
|
|
36
|
+
}
|
|
37
|
+
getSectorCache() {
|
|
38
|
+
return this.sectorCache;
|
|
39
|
+
}
|
|
40
|
+
getMainRallyPoint() {
|
|
41
|
+
return this.mainRallyPoint;
|
|
42
|
+
}
|
|
43
|
+
shouldAttack() {
|
|
44
|
+
return this._shouldAttack;
|
|
45
|
+
}
|
|
46
|
+
checkShouldAttack(threatCache, threatFactor) {
|
|
47
|
+
let scaledGroundPower = Math.pow(threatCache.totalAvailableAntiGroundFirepower, 1.025);
|
|
48
|
+
let scaledGroundThreat = (threatFactor * threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat) * 1.1;
|
|
49
|
+
let scaledAirPower = Math.pow(threatCache.totalAvailableAirPower, 1.025);
|
|
50
|
+
let scaledAirThreat = (threatFactor * threatCache.totalOffensiveAntiAirThreat + threatCache.totalDefensiveThreat) * 1.1;
|
|
51
|
+
return scaledGroundPower > scaledGroundThreat || scaledAirPower > scaledAirThreat;
|
|
52
|
+
}
|
|
53
|
+
isHostileUnit(unit, hostilePlayerNames) {
|
|
54
|
+
if (!unit) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return hostilePlayerNames.includes(unit.owner);
|
|
58
|
+
}
|
|
59
|
+
onAiUpdate(game, playerData) {
|
|
60
|
+
const sectorCache = this.sectorCache;
|
|
61
|
+
sectorCache.updateSectors(game.getCurrentTick(), SECTORS_TO_UPDATE_PER_CYCLE, game.mapApi, playerData);
|
|
62
|
+
let updateRatio = sectorCache?.getSectorUpdateRatio(game.getCurrentTick() - game.getTickRate() * 60);
|
|
63
|
+
if (updateRatio && updateRatio < 1.0) {
|
|
64
|
+
this.logger(`${updateRatio * 100.0}% of sectors updated in last 60 seconds.`);
|
|
65
|
+
}
|
|
66
|
+
const hostilePlayerNames = game
|
|
67
|
+
.getPlayers()
|
|
68
|
+
.map((name) => game.getPlayerData(name))
|
|
69
|
+
.filter((other) => other.name !== playerData.name &&
|
|
70
|
+
other.isCombatant &&
|
|
71
|
+
!game.areAlliedPlayers(playerData.name, other.name))
|
|
72
|
+
.map((other) => other.name);
|
|
73
|
+
// Build the quadtree, if this is too slow we should consider doing this periodically.
|
|
74
|
+
const hostileUnitIds = game.getVisibleUnits(playerData.name, "hostile", (r) => r.isSelectableCombatant || r.type === ObjectType.Building);
|
|
75
|
+
try {
|
|
76
|
+
const hostileUnits = hostileUnitIds
|
|
77
|
+
.map((id) => game.getUnitData(id))
|
|
78
|
+
.filter((unit) => this.isHostileUnit(unit, hostilePlayerNames));
|
|
79
|
+
rebuildQuadtree(this.hostileQuadTree, hostileUnits);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Hack. Will be fixed soon.
|
|
83
|
+
console.error(`caught error`, hostileUnitIds);
|
|
84
|
+
}
|
|
85
|
+
if (game.getCurrentTick() % THREAT_UPDATE_INTERVAL_TICKS == 0) {
|
|
86
|
+
let visibility = sectorCache?.getOverallVisibility();
|
|
87
|
+
if (visibility) {
|
|
88
|
+
this.logger(`${Math.round(visibility * 1000.0) / 10}% of tiles visible. Calculating threat.`);
|
|
89
|
+
// Update the global threat cache
|
|
90
|
+
this.threatCache = calculateGlobalThreat(game, playerData, visibility);
|
|
91
|
+
this.logger(`Threat LAND: Them ${Math.round(this.threatCache.totalOffensiveLandThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiGroundFirepower)}.`);
|
|
92
|
+
this.logger(`Threat DEFENSIVE: Them ${Math.round(this.threatCache.totalDefensiveThreat)}, us: ${Math.round(this.threatCache.totalDefensivePower)}.`);
|
|
93
|
+
this.logger(`Threat AIR: Them ${Math.round(this.threatCache.totalOffensiveAirThreat)}, us: ${Math.round(this.threatCache.totalAvailableAntiAirFirepower)}.`);
|
|
94
|
+
// As the game approaches 2 hours, be more willing to attack. (15 ticks per second)
|
|
95
|
+
const gameLengthFactor = Math.max(0, 1.0 - game.getCurrentTick() / (15 * 7200.0));
|
|
96
|
+
this.logger(`Game length multiplier: ${gameLengthFactor}`);
|
|
97
|
+
if (!this._shouldAttack) {
|
|
98
|
+
// If not attacking, make it harder to switch to attack mode by multiplying the opponent's threat.
|
|
99
|
+
this._shouldAttack = this.checkShouldAttack(this.threatCache, 1.25 * gameLengthFactor);
|
|
100
|
+
if (this._shouldAttack) {
|
|
101
|
+
this.logger(`Globally switched to attack mode.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// If currently attacking, make it harder to switch to defence mode my dampening the opponent's threat.
|
|
106
|
+
this._shouldAttack = this.checkShouldAttack(this.threatCache, 0.75 * gameLengthFactor);
|
|
107
|
+
if (!this._shouldAttack) {
|
|
108
|
+
this.logger(`Globally switched to defence mode.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Update rally point every few ticks.
|
|
114
|
+
if (game.getCurrentTick() % RALLY_POINT_UPDATE_INTERVAL_TICKS === 0) {
|
|
115
|
+
const enemyPlayers = game
|
|
116
|
+
.getPlayers()
|
|
117
|
+
.filter((p) => p !== playerData.name && !game.areAlliedPlayers(playerData.name, p));
|
|
118
|
+
const enemy = game.getPlayerData(enemyPlayers[0]);
|
|
119
|
+
this.mainRallyPoint = getPointTowardsOtherPoint(game, playerData.startLocation, enemy.startLocation, 10, 10, 0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -11,8 +11,9 @@ export class BasicGroundUnit {
|
|
|
11
11
|
return undefined;
|
|
12
12
|
}
|
|
13
13
|
getPriority(game, playerData, technoRules, threatCache) {
|
|
14
|
+
const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
|
|
14
15
|
if (threatCache) {
|
|
15
|
-
let priority =
|
|
16
|
+
let priority = this.basePriority;
|
|
16
17
|
if (this.antiGroundPower > 0) {
|
|
17
18
|
// If the enemy's power is increasing we should try to keep up.
|
|
18
19
|
if (threatCache.totalOffensiveLandThreat > threatCache.totalAvailableAntiGroundFirepower) {
|
|
@@ -25,10 +26,11 @@ export class BasicGroundUnit {
|
|
|
25
26
|
else {
|
|
26
27
|
// But also, if our power dwarfs the enemy, keep pressing the advantage.
|
|
27
28
|
priority +=
|
|
28
|
-
this.antiGroundPower *
|
|
29
|
+
(this.antiGroundPower *
|
|
29
30
|
this.basePriority *
|
|
30
31
|
Math.sqrt(threatCache.totalAvailableAntiGroundFirepower /
|
|
31
|
-
Math.max(1, threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat))
|
|
32
|
+
Math.max(1, threatCache.totalOffensiveLandThreat + threatCache.totalDefensiveThreat))) /
|
|
33
|
+
(numOwned + 1);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
if (this.antiAirPower > 0) {
|
|
@@ -40,15 +42,15 @@ export class BasicGroundUnit {
|
|
|
40
42
|
}
|
|
41
43
|
else {
|
|
42
44
|
priority +=
|
|
43
|
-
this.antiAirPower *
|
|
45
|
+
(this.antiAirPower *
|
|
44
46
|
this.basePriority *
|
|
45
47
|
Math.sqrt(threatCache.totalAvailableAntiAirFirepower /
|
|
46
|
-
Math.max(1, threatCache.totalOffensiveAirThreat + threatCache.totalDefensiveThreat))
|
|
48
|
+
Math.max(1, threatCache.totalOffensiveAirThreat + threatCache.totalDefensiveThreat))) /
|
|
49
|
+
(numOwned + 1);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
return priority;
|
|
50
53
|
}
|
|
51
|
-
const numOwned = numBuildingsOwnedOfType(game, playerData, technoRules);
|
|
52
54
|
return this.basePriority * (1.0 - numOwned / this.baseAmount);
|
|
53
55
|
}
|
|
54
56
|
getMaxCount(game, playerData, technoRules, threatCache) {
|
|
@@ -31,7 +31,6 @@ export function getDefaultPlacementLocation(game, playerData, startPoint, techno
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
console.log("Can't find a place to put the " + technoRules.name);
|
|
35
34
|
return undefined;
|
|
36
35
|
}
|
|
37
36
|
export const DEFAULT_BUILDING_PRIORITY = 1;
|
|
@@ -45,14 +44,17 @@ export const BUILDING_NAME_TO_RULES = new Map([
|
|
|
45
44
|
["ENGINEER", new BasicBuilding(1, 1, 10000)],
|
|
46
45
|
["GADEPT", new BasicBuilding(1, 1, 10000)],
|
|
47
46
|
["GAAIRC", new BasicBuilding(8, 1, 6000)],
|
|
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)],
|
|
51
|
-
["E1", new BasicGroundUnit(
|
|
52
|
+
["E1", new BasicGroundUnit(2, 3, 0.25, 0)],
|
|
52
53
|
["MTNK", new BasicGroundUnit(10, 3, 2, 0)],
|
|
53
54
|
["MGTK", new BasicGroundUnit(10, 1, 2.5, 0)],
|
|
54
55
|
["FV", new BasicGroundUnit(5, 2, 0.5, 1)],
|
|
55
56
|
["JUMPJET", new BasicAirUnit(10, 1, 1, 1)],
|
|
57
|
+
["ORCA", new BasicAirUnit(7, 1, 2, 0)],
|
|
56
58
|
["SREF", new ArtilleryUnit(9, 1)],
|
|
57
59
|
["CLEG", new BasicGroundUnit(0, 0)],
|
|
58
60
|
["SHAD", new BasicGroundUnit(0, 0)],
|
|
@@ -65,10 +67,13 @@ export const BUILDING_NAME_TO_RULES = new Map([
|
|
|
65
67
|
["SENGINEER", new BasicBuilding(1, 1, 10000)],
|
|
66
68
|
["NADEPT", new BasicBuilding(1, 1, 10000)],
|
|
67
69
|
["NARADR", new BasicBuilding(8, 1, 4000)],
|
|
70
|
+
["NANRCT", new PowerPlant()],
|
|
71
|
+
["NAYARD", new BasicBuilding(0, 0, 0)],
|
|
72
|
+
["NATECH", new BasicBuilding(20, 1, 4000)],
|
|
68
73
|
["NALASR", new AntiGroundStaticDefence(5, 1, 5)],
|
|
69
74
|
["TESLA", new AntiGroundStaticDefence(5, 1, 10)],
|
|
70
75
|
["NAWALL", new AntiGroundStaticDefence(0, 0, 0)],
|
|
71
|
-
["E2", new BasicGroundUnit(
|
|
76
|
+
["E2", new BasicGroundUnit(2, 3, 0.25, 0)],
|
|
72
77
|
["HTNK", new BasicGroundUnit(10, 3, 3, 0)],
|
|
73
78
|
["APOC", new BasicGroundUnit(6, 1, 5, 0)],
|
|
74
79
|
["HTK", new BasicGroundUnit(5, 2, 0.33, 1.5)],
|
|
@@ -9,7 +9,7 @@ export class Harvester extends BasicGroundUnit {
|
|
|
9
9
|
getPriority(game, playerData, technoRules, threatCache) {
|
|
10
10
|
const refineries = game.getVisibleUnits(playerData.name, "self", (r) => r.refinery).length;
|
|
11
11
|
const harvesters = game.getVisibleUnits(playerData.name, "self", (r) => r.harvester).length;
|
|
12
|
-
const boost = harvesters < this.minNeeded ?
|
|
12
|
+
const boost = harvesters < this.minNeeded ? 3 : 1;
|
|
13
13
|
return this.basePriority * (refineries / Math.max(harvesters / IDEAL_HARVESTERS_PER_REFINERY, 1)) * boost;
|
|
14
14
|
}
|
|
15
15
|
}
|