├── .gitignore ├── .vscode └── settings.json ├── README.md ├── package-lock.json ├── package.json ├── src └── exampleBot.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env 4 | *.zip -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrono Divide API Playground 2 | 3 | This is a sample project that uses the [Chrono Divide Game API](https://www.npmjs.com/package/@chronodivide/game-api). It can be used to develop and test AI bots for [Chrono Divide](https://chronodivide.com)] by running the game headless, in an isolated command-line environment. The API can create offline games between computer-controlled agents, or even online games, played in real-time versus human opponents. 4 | 5 | ## Prerequisites 6 | 7 | * NodeJS 20+ 8 | * TypeScript 4.8.4+ 9 | * MIX files from an original RA2 installation 10 | 11 | ## Install instructions 12 | 13 | ```sh 14 | npm install 15 | npm run build 16 | npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm start 17 | ``` 18 | 19 | ## Debugging 20 | 21 | ```sh 22 | npx cross-env MIX_DIR="C:\path_to_ra2_install_dir" npm --node-options="${NODE_OPTIONS} --inspect" start 23 | ``` 24 | 25 | ## Additional resources 26 | 27 | * https://www.npmjs.com/package/@chronodivide/game-api - The Game API package on NPM 28 | * https://github.com/Supalosa/supalosa-chronodivide-bot - A Chrono Divide bot implementation by [Supalosa](https://github.com/Supalosa/) 29 | * https://discord.gg/KCRPzQ6EEa - Dedicated channel on the Chrono Divide Discord server for bot development discussions 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chronodivide/game-api-playground", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@chronodivide/game-api-playground", 9 | "version": "1.0.0", 10 | "license": "UNLICENSED", 11 | "dependencies": { 12 | "@chronodivide/game-api": "^0.58.0" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^20.16.13", 16 | "typescript": "^4.8.4" 17 | } 18 | }, 19 | "node_modules/@chronodivide/game-api": { 20 | "version": "0.58.0", 21 | "resolved": "https://registry.npmjs.org/@chronodivide/game-api/-/game-api-0.58.0.tgz", 22 | "integrity": "sha512-KCQ/GX9TDEVkVk+ICisBJu2aao5O6AJYqLqHbzneOy+Tn5u81lLbQOPern3dHavbOHezO25cCPR9WT7EE8cMKw==", 23 | "license": "UNLICENSED", 24 | "dependencies": { 25 | "@types/three": "^0.93.31", 26 | "es-dirname": "^0.1.0", 27 | "file-system-access": "^1.0.1", 28 | "three": "^0.94.0", 29 | "websocket-polyfill": "0.0.3" 30 | }, 31 | "engines": { 32 | "node": ">=20" 33 | } 34 | }, 35 | "node_modules/@types/node": { 36 | "version": "20.16.13", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz", 38 | "integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==", 39 | "dev": true, 40 | "dependencies": { 41 | "undici-types": "~6.19.2" 42 | } 43 | }, 44 | "node_modules/@types/three": { 45 | "version": "0.93.31", 46 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.93.31.tgz", 47 | "integrity": "sha512-lR9eLY3gnrUDAdW4ujITgVTPEv1Oy2yoL3LlfKAnjQuzyjGSR+PvQuXuWmaDCD1IyWhkqbnol1nJwaW0MGe35A==" 48 | }, 49 | "node_modules/@types/wicg-file-system-access": { 50 | "version": "2020.9.8", 51 | "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz", 52 | "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==" 53 | }, 54 | "node_modules/bufferutil": { 55 | "version": "4.0.8", 56 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", 57 | "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", 58 | "hasInstallScript": true, 59 | "dependencies": { 60 | "node-gyp-build": "^4.3.0" 61 | }, 62 | "engines": { 63 | "node": ">=6.14.2" 64 | } 65 | }, 66 | "node_modules/d": { 67 | "version": "1.0.1", 68 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 69 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 70 | "dependencies": { 71 | "es5-ext": "^0.10.50", 72 | "type": "^1.0.1" 73 | } 74 | }, 75 | "node_modules/debug": { 76 | "version": "2.6.9", 77 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 78 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 79 | "dependencies": { 80 | "ms": "2.0.0" 81 | } 82 | }, 83 | "node_modules/es-dirname": { 84 | "version": "0.1.0", 85 | "resolved": "https://registry.npmjs.org/es-dirname/-/es-dirname-0.1.0.tgz", 86 | "integrity": "sha512-tcTj4pVFXe5EdiHCybjynDTlvkwuNN6JCg9+5BVB+n9qQoMyWOtxpREoL7rGO3sCIAX55Y6CYZi4s1c30uxvPg==" 87 | }, 88 | "node_modules/es5-ext": { 89 | "version": "0.10.62", 90 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", 91 | "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", 92 | "hasInstallScript": true, 93 | "dependencies": { 94 | "es6-iterator": "^2.0.3", 95 | "es6-symbol": "^3.1.3", 96 | "next-tick": "^1.1.0" 97 | }, 98 | "engines": { 99 | "node": ">=0.10" 100 | } 101 | }, 102 | "node_modules/es6-iterator": { 103 | "version": "2.0.3", 104 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 105 | "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", 106 | "dependencies": { 107 | "d": "1", 108 | "es5-ext": "^0.10.35", 109 | "es6-symbol": "^3.1.1" 110 | } 111 | }, 112 | "node_modules/es6-symbol": { 113 | "version": "3.1.3", 114 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 115 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 116 | "dependencies": { 117 | "d": "^1.0.1", 118 | "ext": "^1.1.2" 119 | } 120 | }, 121 | "node_modules/ext": { 122 | "version": "1.7.0", 123 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", 124 | "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", 125 | "dependencies": { 126 | "type": "^2.7.2" 127 | } 128 | }, 129 | "node_modules/ext/node_modules/type": { 130 | "version": "2.7.2", 131 | "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", 132 | "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" 133 | }, 134 | "node_modules/fetch-blob": { 135 | "version": "3.2.0", 136 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 137 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 138 | "funding": [ 139 | { 140 | "type": "github", 141 | "url": "https://github.com/sponsors/jimmywarting" 142 | }, 143 | { 144 | "type": "paypal", 145 | "url": "https://paypal.me/jimmywarting" 146 | } 147 | ], 148 | "dependencies": { 149 | "node-domexception": "^1.0.0", 150 | "web-streams-polyfill": "^3.0.3" 151 | }, 152 | "engines": { 153 | "node": "^12.20 || >= 14.13" 154 | } 155 | }, 156 | "node_modules/file-system-access": { 157 | "version": "1.0.4", 158 | "resolved": "https://registry.npmjs.org/file-system-access/-/file-system-access-1.0.4.tgz", 159 | "integrity": "sha512-JDlhH+gJfZu/oExmtN4/6VX+q1etlrbJbR5uzoBa4BzfTRQbEXGFuGIBRk3ZcPocko3WdEclZSu+d/SByjG6Rg==", 160 | "dependencies": { 161 | "@types/wicg-file-system-access": "^2020.9.2", 162 | "fetch-blob": "^3.0.0", 163 | "node-domexception": "^1.0.0" 164 | }, 165 | "engines": { 166 | "node": ">=14" 167 | }, 168 | "optionalDependencies": { 169 | "web-streams-polyfill": "^3.1.0" 170 | } 171 | }, 172 | "node_modules/is-typedarray": { 173 | "version": "1.0.0", 174 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 175 | "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 176 | }, 177 | "node_modules/ms": { 178 | "version": "2.0.0", 179 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 180 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 181 | }, 182 | "node_modules/next-tick": { 183 | "version": "1.1.0", 184 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", 185 | "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" 186 | }, 187 | "node_modules/node-domexception": { 188 | "version": "1.0.0", 189 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 190 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 191 | "funding": [ 192 | { 193 | "type": "github", 194 | "url": "https://github.com/sponsors/jimmywarting" 195 | }, 196 | { 197 | "type": "github", 198 | "url": "https://paypal.me/jimmywarting" 199 | } 200 | ], 201 | "engines": { 202 | "node": ">=10.5.0" 203 | } 204 | }, 205 | "node_modules/node-gyp-build": { 206 | "version": "4.6.1", 207 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", 208 | "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", 209 | "bin": { 210 | "node-gyp-build": "bin.js", 211 | "node-gyp-build-optional": "optional.js", 212 | "node-gyp-build-test": "build-test.js" 213 | } 214 | }, 215 | "node_modules/three": { 216 | "version": "0.94.0", 217 | "resolved": "https://registry.npmjs.org/three/-/three-0.94.0.tgz", 218 | "integrity": "sha512-UFyqFrb/CaTAHStYaPNxNeddNo/wlApRMJK0oIWjx5WMj/xhgXWKMRuAJ+leZBIA4wgeqifrbjKiwmNiysgMLg==" 219 | }, 220 | "node_modules/tstl": { 221 | "version": "2.5.13", 222 | "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.13.tgz", 223 | "integrity": "sha512-h9wayHHFI5+yqt8iau0vqH96cTNhezhZ/Fk/hrIdpfkiMu3lg9nzyvMfs5bIdX51IVzZO6DudLqhkL/rVXpT6g==" 224 | }, 225 | "node_modules/type": { 226 | "version": "1.2.0", 227 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 228 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 229 | }, 230 | "node_modules/typedarray-to-buffer": { 231 | "version": "3.1.5", 232 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 233 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 234 | "dependencies": { 235 | "is-typedarray": "^1.0.0" 236 | } 237 | }, 238 | "node_modules/typescript": { 239 | "version": "4.8.4", 240 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", 241 | "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", 242 | "dev": true, 243 | "bin": { 244 | "tsc": "bin/tsc", 245 | "tsserver": "bin/tsserver" 246 | }, 247 | "engines": { 248 | "node": ">=4.2.0" 249 | } 250 | }, 251 | "node_modules/undici-types": { 252 | "version": "6.19.8", 253 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 254 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 255 | "dev": true 256 | }, 257 | "node_modules/utf-8-validate": { 258 | "version": "5.0.10", 259 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", 260 | "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", 261 | "hasInstallScript": true, 262 | "dependencies": { 263 | "node-gyp-build": "^4.3.0" 264 | }, 265 | "engines": { 266 | "node": ">=6.14.2" 267 | } 268 | }, 269 | "node_modules/web-streams-polyfill": { 270 | "version": "3.2.1", 271 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 272 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 273 | "engines": { 274 | "node": ">= 8" 275 | } 276 | }, 277 | "node_modules/websocket": { 278 | "version": "1.0.34", 279 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", 280 | "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", 281 | "dependencies": { 282 | "bufferutil": "^4.0.1", 283 | "debug": "^2.2.0", 284 | "es5-ext": "^0.10.50", 285 | "typedarray-to-buffer": "^3.1.5", 286 | "utf-8-validate": "^5.0.2", 287 | "yaeti": "^0.0.6" 288 | }, 289 | "engines": { 290 | "node": ">=4.0.0" 291 | } 292 | }, 293 | "node_modules/websocket-polyfill": { 294 | "version": "0.0.3", 295 | "resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz", 296 | "integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==", 297 | "dependencies": { 298 | "tstl": "^2.0.7", 299 | "websocket": "^1.0.28" 300 | } 301 | }, 302 | "node_modules/yaeti": { 303 | "version": "0.0.6", 304 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 305 | "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", 306 | "engines": { 307 | "node": ">=0.10.32" 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chronodivide/game-api-playground", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "main": "dist/exampleBot.js", 7 | "type": "module", 8 | "scripts": { 9 | "build": "tsc -p .", 10 | "watch": "tsc -p . -w", 11 | "start": "node .", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "license": "UNLICENSED", 15 | "devDependencies": { 16 | "@types/node": "^20.16.13", 17 | "typescript": "^4.8.4" 18 | }, 19 | "dependencies": { 20 | "@chronodivide/game-api": "^0.58.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/exampleBot.ts: -------------------------------------------------------------------------------- 1 | import { cdapi, OrderType, ApiEventType, Bot, GameApi, ApiEvent, CreateBaseOpts, CreateOpts } from "@chronodivide/game-api"; 2 | 3 | enum BotState { 4 | Initial, 5 | Deployed, 6 | MovingToEnemy, 7 | AttackingEnemy, 8 | Defeated 9 | } 10 | 11 | class ExampleBot extends Bot { 12 | private botState = BotState.Initial; 13 | private tickRatio!: number; 14 | private enemyPlayers!: string[]; 15 | 16 | override onGameStart(game: GameApi) { 17 | const gameRate = game.getTickRate(); 18 | const botApm = 300; 19 | const botRate = botApm / 60; 20 | this.tickRatio = Math.ceil(gameRate / botRate); 21 | 22 | this.enemyPlayers = game.getPlayers().filter(p => p !== this.name && !game.areAlliedPlayers(this.name, p)); 23 | } 24 | 25 | override onGameTick(game: GameApi) { 26 | if (game.getCurrentTick() % this.tickRatio === 0) { 27 | switch (this.botState) { 28 | case BotState.Initial: { 29 | const baseUnits = game.getGeneralRules().baseUnit; 30 | let conYards = game.getVisibleUnits(this.name, "self", r => r.constructionYard); 31 | if (conYards.length) { 32 | this.botState = BotState.Deployed; 33 | break; 34 | } 35 | const units = game.getVisibleUnits(this.name, "self", r => baseUnits.includes(r.name)); 36 | if (units.length) { 37 | this.actionsApi.orderUnits([units[0]], OrderType.DeploySelected); 38 | } 39 | break; 40 | } 41 | 42 | case BotState.Deployed: { 43 | const armyUnits = game.getVisibleUnits(this.name, "self", r => r.isSelectableCombatant); 44 | const { x: rx, y: ry } = game.getPlayerData(this.enemyPlayers[0]).startLocation; 45 | this.actionsApi.orderUnits(armyUnits, OrderType.AttackMove, rx, ry); 46 | this.botState = BotState.MovingToEnemy; 47 | break; 48 | } 49 | 50 | case BotState.MovingToEnemy: 51 | case BotState.AttackingEnemy: { 52 | const armyUnits = game.getVisibleUnits(this.name, "self", r => r.isSelectableCombatant); 53 | if (!armyUnits.length) { 54 | this.botState = BotState.Defeated; 55 | this.actionsApi.quitGame(); 56 | break; 57 | } 58 | 59 | if (this.botState === BotState.MovingToEnemy) { 60 | const baseUnits = game.getGeneralRules().baseUnit; 61 | const enemyBase = game.getVisibleUnits(this.name, "hostile", 62 | r => r.constructionYard || baseUnits.includes(r.name)); 63 | 64 | if (enemyBase.length) { 65 | this.actionsApi.orderUnits(armyUnits, OrderType.AttackMove, enemyBase[0]); 66 | this.botState = BotState.AttackingEnemy; 67 | } 68 | } 69 | break; 70 | } 71 | 72 | default: 73 | break; 74 | } 75 | } 76 | } 77 | 78 | override onGameEvent(ev: ApiEvent) { 79 | switch (ev.type) { 80 | case ApiEventType.ObjectOwnerChange: { 81 | this.logger.info(`Owner change: ${ev.prevOwnerName} -> ${ev.newOwnerName}`); 82 | break; 83 | } 84 | 85 | case ApiEventType.ObjectDestroy: { 86 | this.logger.info(`Object destroyed: ${ev.target}`); 87 | break; 88 | } 89 | 90 | default: 91 | break; 92 | } 93 | } 94 | } 95 | 96 | async function main() { 97 | await cdapi.init(process.env.MIX_DIR || "./"); 98 | 99 | const mapName = "mp03t4.map"; 100 | 101 | const baseOpts: CreateBaseOpts = { 102 | buildOffAlly: false, 103 | cratesAppear: false, 104 | credits: 10000, 105 | gameMode: cdapi.getAvailableGameModes(mapName)[0], 106 | gameSpeed: 5, 107 | mapName, 108 | mcvRepacks: true, 109 | shortGame: true, 110 | superWeapons: false, 111 | unitCount: 10 112 | }; 113 | let opts: CreateOpts; 114 | 115 | const onlineMode = !!process.env.SERVER_URL; 116 | if (onlineMode) { 117 | const botName = process.env.BOT_USER; 118 | if (!botName) { 119 | throw new Error(`Missing env BOT_USER`); 120 | } 121 | const botPassword = process.env.BOT_PASS; 122 | if (!botPassword) { 123 | throw new Error(`Missing env BOT_PASS`); 124 | } 125 | const playerName = process.env.PLAYER_USER; 126 | if (!playerName) { 127 | throw new Error(`Missing env PLAYER_USER`); 128 | } 129 | opts = { 130 | ...baseOpts, 131 | online: true, 132 | serverUrl: process.env.SERVER_URL!, 133 | clientUrl: process.env.CLIENT_URL!, 134 | botPassword, 135 | agents: [new ExampleBot(botName, "Americans").setDebugMode(true), { name: playerName, country: "Africans" }] 136 | }; 137 | } else { 138 | opts = { 139 | ...baseOpts, 140 | agents: [new ExampleBot("Joe", "Americans").setDebugMode(true), new ExampleBot("Bob", "Africans")] 141 | }; 142 | } 143 | 144 | const game = await cdapi.createGame(opts); 145 | 146 | while (!game.isFinished()) { 147 | await game.update(); 148 | } 149 | 150 | game.saveReplay(); 151 | game.dispose(); 152 | } 153 | 154 | main().catch(e => { 155 | console.error(e); 156 | process.exit(1); 157 | }); 158 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | --------------------------------------------------------------------------------