@chronodivide/game-api 0.44.0 → 0.46.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ # 0.46.0
4
+
5
+ - Update game engine version to 0.52
6
+ - Add `actionsApi.setGlobalDebugText` and `actionsApi.setUnitDebugText`
7
+ - Add `Bot.logger` and `Bot.getDebugMode`/`Bot.setDebugMode` as preferred method of logging
8
+ - Deprecated `Point2D` inteface in favor of `Vector2`
9
+ - **BREAKING**: Add `CreateOnlineOpts.botPassword`. This is used for the bot to login to the server, using `Bot.name` as nickname.
10
+ - Add filter predicate to `gameApi.getAllUnits`
11
+ - Add `gameApi.getNeutralUnits` and `gameApi.getUnitsInArea`
12
+ - Add `gameApi.getAiIni`
13
+ - Add `gameApi.getBaseTickRate` and `gameApi.getCurrentTime`
14
+ - Add `GameMath` export, which provides deterministic cross-platform versions of `Math.pow`, `Math.sqrt` and trigonometric functions
15
+ - Add `mapApi.getRealMapSize`
16
+ - Add `mapApi.findPath`
17
+ - Improve performance of `gameApi.canPlaceBuilding` when called many times in succession using different tile values.
18
+
19
+ # 0.45.0
20
+
21
+ - Update game engine version to 0.51
22
+
3
23
  # 0.44.0
4
24
 
5
25
  - Add `UnitData.hasWrenchRepair`
package/README.md CHANGED
@@ -64,16 +64,17 @@ main().catch(e => {
64
64
 
65
65
  ## Online Games
66
66
 
67
- Online games between an AI and a human opponent are also possible, but require a Chrono Divide server URL to connect to. Online games can be created with minimal changes to the `createGame` function from the previous example:
67
+ Online games between an AI and a human opponent are also possible, but require a Chrono Divide server URL to connect to and a bot account. Online games can be created with minimal changes to the `createGame` function from the previous example:
68
68
 
69
69
  ```ts
70
70
  const game = await cdapi.createGame({
71
71
  online: true,
72
72
  serverUrl: process.env.SERVER_URL!,
73
73
  clientUrl: process.env.CLIENT_URL!,
74
+ botPassword: process.env.BOT_PASSWORD!,
74
75
  agents: [
75
- new ExampleBot(`Agent${String(Date.now()).substr(-6)}`, "Americans"),
76
- { name: `Human${String(Date.now()).substr(-6)}`, country: "French" }
76
+ new ExampleBot(`BotNickname`, "Americans"),
77
+ { name: `PlayerNickname`, country: "French" }
77
78
  ],
78
79
 
79
80
  buildOffAlly: false,
@@ -92,3 +93,82 @@ const game = await cdapi.createGame({
92
93
  ## API Reference
93
94
 
94
95
  Please refer to the [TypeScript definitions](./dist/index.d.ts).
96
+
97
+ ## Debugging
98
+
99
+ ### Application logging
100
+
101
+ Debug logging can be enabled by setting the `DEBUG_LOGGING` environment variable when running in the sandbox, or the `r.debug_logging` console variable in the game client. To enable, set this variable to a string containing a comma-delimited list of loggers.
102
+
103
+ For example, to print player actions as they are processed, you can set `DEBUG_LOGGING=action`.
104
+
105
+ ### Bot logger
106
+
107
+ The `Bot` class exposes its own `logger`, which allows more granular control and filtering, especially when there are multiple bots printing messages at the same time. Logged messages are also prefixed with the bot name and in-game timestamp.
108
+
109
+ By default, the logger prints only warnings and errors. Debug mode (see below) enables all log levels.
110
+
111
+ ### Debug mode
112
+
113
+ Bot debug mode is a flag set on the `Bot` class, which toggles certain debug features on or off, like logging or setting debug labels. Debug mode is generally controlled automatically by the game client, but it can also be enabled from the sandbox, by calling `botInstance.setDebugMode(true)`.
114
+
115
+ **IMPORTANT**: `setDebugMode()` should never be called from within the bot implementation class itself.
116
+
117
+ Debug mode can be enabled within the game client by setting the value of the `r.debug_bot` console variable to the player index (0-based) of the bot that is being debugged.
118
+
119
+ E.g.: `r.debug_bot=1` will debug the bot in player slot index 1 (the second slot) and `r.debug_bot=0` will disable debugging.
120
+
121
+ ### Unit debug labels and global debug text
122
+
123
+ The game client offers the following features which can aid debugging bot code:
124
+
125
+ - Displaying a multiline debug string attached to a unit or building. To use this feature, the bot implementation should call `actionsApi.setUnitDebugText`. Once set, this text is persistent, until overwritten by a new value.
126
+ - Displaying a sticky always-on-top multiline text, at a fixed position on the screen, below the chat area. To use this, call `actionsApi.setGlobalDebugText`. This text is also persistent and offers no scrolling functionality. The value is simply overwritten. Use this feature to display relevant debug stats on the screen. Logging should be done using the bot logger instead.
127
+
128
+ In both cases, debug text will be printed in the game client only if the console variable `r.debug_text=1` is set.
129
+
130
+ **IMPORTANT**: Both `actionsApi.setUnitDebugText` and `actionsApi.setGlobalDebugText` will generate a player action as workaround for not being able to directly remote control a game client. This is a consequence of the bot code running in its own sandbox and not being directly connected to a game client. As a result, this can generate considerable network noise, as well as increased replay file size. For this reason, both functions only work when bot debug mode is enabled and require an account with bot privileges in online mode.
131
+
132
+ ## Notes on deterministic code
133
+
134
+ The game loop must be guaranteed to give the exact same game simulation provided that the player inputs/commands and initial state are identical. This means that code must be deterministic, or, given the same input, it will always produce the same output. Otherwise, multiplayer games will often result in fatal desyncs, because each client will have its own slightly different version of reality. Singleplayer games will not crash, but loading replay files or save games will be unreliable for the same reason.
135
+
136
+ Even though a bot running in the sandbox doesn't necessarily need to be deterministic when fighting a real player over the network, it does however need to be once integrated in the game loop. Below is a non-exhaustive table of unsafe JavaScript built-in functions, and their safe version offered through the game API.
137
+
138
+ |JavaScript built-in | Game API equivalent | Notes |
139
+ |--|--|--|
140
+ | `Math.random` | `gameApi.generateRandom` and `gameApi.generateRandomInt` | Uses a PRNG |
141
+ | `Math.pow`, `**` operator | `GameMath.pow` | Only works with integer exponents; precision of 6 decimals |
142
+ | `Math.sqrt` | `GameMath.sqrt` | |
143
+ | `Math.sin` | `GameMath.sin` | Uses a LUT; resolution of ~0.006rad |
144
+ | `Math.cos` | `GameMath.cos` | Uses a LUT; resolution of ~0.006rad |
145
+ | `Math.asin` | `GameMath.asin` | Uses reverse LUT lookup |
146
+ | `Math.acos` | `GameMath.acos` | Uses reverse LUT lookup |
147
+ | `Math.atan2` | `GameMath.atan2` | Precision of 3 decimals |
148
+ | `Math.acosh` | Unsupported | |
149
+ | `Math.asinh` | Unsupported | |
150
+ | `Math.atan` | Unsupported | |
151
+ | `Math.atanh` | Unsupported | |
152
+ | `Math.cbrt` | Unsupported | |
153
+ | `Math.cosh` | Unsupported | |
154
+ | `Math.exp` | Unsupported | |
155
+ | `Math.expm1` | Unsupported | |
156
+ | `Math.hypot` | Unsupported | |
157
+ | `Math.log` | Unsupported | |
158
+ | `Math.log10` | Unsupported | |
159
+ | `Math.log1p` | Unsupported | |
160
+ | `Math.log2` | Unsupported | |
161
+ | `Math.sinh` | Unsupported | |
162
+ | `Math.tan` | Unsupported | |
163
+ | `Math.tanh` | Unsupported | |
164
+
165
+ Safe version of THREE.js geometry/math classes are also provided:
166
+
167
+ | THREE.js class | Game API equivalent |
168
+ |--|--|
169
+ | THREE.Vector2 | Vector2 |
170
+ | THREE.Vector3 | Vector3 |
171
+ | THREE.Box2 | Box2 |
172
+ | THREE.Euler | Euler |
173
+ | THREE.Quaternion | Quaternion |
174
+ | THREE.Matrix4 | Matrix4 |
package/dist/index.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { Euler as Euler_2 } from 'three';
2
- import { Quaternion as Quaternion_2 } from 'three';
3
-
4
1
  export declare class ActionsApi {
5
2
  #private;
6
3
  placeBuilding(buildingName: string, rx: number, ry: number): void;
@@ -22,6 +19,30 @@ export declare class ActionsApi {
22
19
  orderUnits(unitIds: number[], orderType: OrderType, targetUnit: number): void;
23
20
  orderUnits(unitIds: number[], orderType: OrderType, rx: number, ry: number, onBridge?: boolean): void;
24
21
  sayAll(text: string): void;
22
+ /**
23
+ * Prints a persistent multiline debug string in the game client at a fixed position on the screen
24
+ *
25
+ * **IMPORTANT**: This method only works when bot debug mode is enabled
26
+ *
27
+ * If the bot is in online mode, it will send this text as a player action (only enabled for bot accounts).
28
+ * The game client also needs to have r.debug_text=1 enabled in the dev console.
29
+ *
30
+ * If the bot is in offline mode, this method will still generate actions that may increase the replay file size
31
+ * significantly.
32
+ */
33
+ setGlobalDebugText(text: string | undefined): void;
34
+ /**
35
+ * Sets a persistent multiline debug label for a given unit, which the game client will display anchored to the unit
36
+ *
37
+ * **IMPORTANT**: This method only works when bot debug mode is enabled
38
+ *
39
+ * If the bot is in online mode, it will send this text as a player action (only enabled for bot accounts).
40
+ * The game client also needs to have r.debug_text=1 enabled in the dev console.
41
+ *
42
+ * If the bot is in offline mode, this method will still generate actions that may increase the replay file size
43
+ * significantly.
44
+ */
45
+ setUnitDebugText(unitId: number, text: string | undefined): void;
25
46
  quitGame(): void;
26
47
  }
27
48
 
@@ -76,17 +97,30 @@ export declare enum AttackState {
76
97
  }
77
98
 
78
99
  export declare class Bot {
100
+ #private;
79
101
  name: string;
80
102
  country: string;
81
103
  protected gameApi: GameApi;
82
104
  protected actionsApi: ActionsApi;
83
105
  protected productionApi: ProductionApi;
106
+ protected logger: LoggerApi;
84
107
  constructor(name: string, country: string);
108
+ /**
109
+ * Toggles bot debug mode, allowing the bot implementation to switch some debug features on or off.
110
+ * This also enables or disables debug logging.
111
+ *
112
+ * This method is controlled by internal game code and should not be called by the bot implementation itself.
113
+ */
114
+ setDebugMode(debugMode: boolean): this;
115
+ getDebugMode(): boolean;
85
116
  onGameStart(gameApi: GameApi): void;
86
117
  onGameTick(gameApi: GameApi): void;
87
118
  onGameEvent(ev: ApiEvent, gameApi: GameApi): void;
88
119
  }
89
120
 
121
+ export declare class Box2 extends THREE.Box2 {
122
+ }
123
+
90
124
  export declare enum BuildCat {
91
125
  Combat = 0,
92
126
  Tech = 1,
@@ -98,7 +132,7 @@ export declare interface BuildingPlacementData {
98
132
  /** The size of the building foundation in tiles */
99
133
  foundation: Size;
100
134
  /** The offset of the foundation center tile */
101
- foundationCenter: Point2D;
135
+ foundationCenter: Vector2;
102
136
  }
103
137
 
104
138
  export declare enum BuildStatus {
@@ -208,6 +242,7 @@ export declare interface CreateOnlineOpts extends CreateBaseOpts {
208
242
  online: true;
209
243
  serverUrl: string;
210
244
  clientUrl: string;
245
+ botPassword: string;
211
246
  agents: [Bot, ...Agent[]];
212
247
  }
213
248
 
@@ -238,7 +273,7 @@ export declare class Euler extends THREE.Euler {
238
273
  private _z;
239
274
  private _order;
240
275
  setFromRotationMatrix(m: Matrix4, order?: string, update?: boolean): this;
241
- reorder(newOrder: string): Euler_2;
276
+ reorder(newOrder: string): this;
242
277
  toVector3(optionalResult?: Vector3): Vector3;
243
278
  }
244
279
 
@@ -278,7 +313,12 @@ export declare class GameApi {
278
313
  getPlayers(): string[];
279
314
  getPlayerData(playerName: string): PlayerData;
280
315
  getAllTerrainObjects(): number[];
281
- getAllUnits(): number[];
316
+ /** Queries all units and buildings on the map, regardless of owner */
317
+ getAllUnits(filter?: (r: TechnoRules) => boolean): number[];
318
+ /** Queries neutral units and buildings on the map */
319
+ getNeutralUnits(filter?: (r: TechnoRules) => boolean): number[];
320
+ /** Queries units in a given area of tiles. Uses a quadtree and is more efficient than scanning tile by tile. */
321
+ getUnitsInArea(tileRange: Box2): number[];
282
322
  /**
283
323
  * Gets a list of units which are visible to the given player (not under the shroud), based on hostility
284
324
  *
@@ -292,6 +332,7 @@ export declare class GameApi {
292
332
  getGeneralRules(): GeneralRules;
293
333
  getRulesIni(): IniFile;
294
334
  getArtIni(): IniFile;
335
+ getAiIni(): IniFile;
295
336
  /**
296
337
  * Generates a random integer in the specified [min, max] interval using the internal PRNG.
297
338
  * Must be used instead of Math.random() when computing game state to prevent desyncs between clients
@@ -302,9 +343,16 @@ export declare class GameApi {
302
343
  * Must be used instead of Math.random() when computing game state to prevent desyncs between clients
303
344
  */
304
345
  generateRandom(): number;
305
- /** Current game speed in ticks/second. IMPORTANT: The game speed can change during a game */
346
+ /** Current game speed in ticks per real-time second. IMPORTANT: The game speed can change during a game */
306
347
  getTickRate(): number;
348
+ /** Game speed in ticks per in-game second (at game speed 4) */
349
+ getBaseTickRate(): number;
307
350
  getCurrentTick(): number;
351
+ /**
352
+ * Current game time in game seconds.
353
+ * The get the time in real-time seconds, use {@link GameApi.getCurrentTick} / {@link GameApi.getTickRate} instead
354
+ */
355
+ getCurrentTime(): number;
308
356
  }
309
357
 
310
358
  export declare class GameInstanceApi {
@@ -319,6 +367,23 @@ export declare class GameInstanceApi {
319
367
  dispose(): void;
320
368
  }
321
369
 
370
+ /**
371
+ * Defines custom approximations of built-in Math functions, guaranteed to yield consistent results across platforms
372
+ *
373
+ * See https://stackoverflow.com/questions/42181795/is-ieee-754-2008-deterministic and
374
+ * https://stackoverflow.com/a/56486907
375
+ */
376
+ export declare class GameMath {
377
+ /** Raises base to an integer power */
378
+ static pow(x: number, y: number): number;
379
+ static sqrt(x: number): number;
380
+ static sin(x: number): number;
381
+ static cos(x: number): number;
382
+ static asin(x: number): number;
383
+ static acos(x: number): number;
384
+ static atan2(y: number, x: number): number;
385
+ }
386
+
322
387
  export declare interface GameObjectData {
323
388
  id: number;
324
389
  type: ObjectType;
@@ -402,7 +467,7 @@ export declare class GeneralRules {
402
467
  wallBuildSpeedCoefficient: number;
403
468
  readIni(ini: IniSection): void;
404
469
  private readPrereqCategories;
405
- getMissileRules(type: string): DMislRules | V3RocketRules;
470
+ getMissileRules(type: string): V3RocketRules | DMislRules;
406
471
  }
407
472
 
408
473
  export declare class HoverRules {
@@ -551,10 +616,30 @@ export declare enum LocomotorType {
551
616
  Vehicle = 8
552
617
  }
553
618
 
619
+ export declare class LoggerApi {
620
+ #private;
621
+ debug(...x: any[]): void;
622
+ info(...x: any[]): void;
623
+ log(...x: any[]): void;
624
+ warn(...x: any[]): void;
625
+ error(...x: any[]): void;
626
+ time(label: string): void;
627
+ timeEnd(label: string): void;
628
+ }
629
+
554
630
  export declare class MapApi {
555
631
  #private;
632
+ /**
633
+ * The map size in real/world space tiles.
634
+ *
635
+ * IMPORTANT: Tile coordinates within the real map size only correspond to actual tiles if those tiles are
636
+ * within the visible map area. The visible map area uses ISO screen projection,
637
+ * depends on tile elevations, and cannot be deduced using linear transformations or used to compute map boundaries.
638
+ * Use {@link MapApi.getTile} instead to check if a given tile is within the map boundaries.
639
+ */
640
+ getRealMapSize(): Size;
556
641
  /** Tile coordinates for each player starting position */
557
- getStartingLocations(): Point2D[];
642
+ getStartingLocations(): Vector2[];
558
643
  getTheaterType(): TheaterType;
559
644
  getTile(rx: number, ry: number): Tile | undefined;
560
645
  getTilesInRect(baseTile: Tile, size: Size): Tile[];
@@ -562,6 +647,13 @@ export declare class MapApi {
562
647
  hasBridgeOnTile(tile: Tile): boolean;
563
648
  hasHighBridgeOnTile(tile: Tile): boolean;
564
649
  isPassableTile(tile: Tile, speedType: SpeedType, onBridge: boolean): boolean;
650
+ /**
651
+ * Returns the path between two points on the map for the given SpeedType.
652
+ *
653
+ * Should only be used with ground, non-teleporting units.
654
+ * This method has a big performance penalty and should be used with care.
655
+ */
656
+ findPath(speedType: SpeedType, from: PathNode, to: PathNode, options?: PathFinderOptions): PathNode[];
565
657
  /** If the tile is not covered by shroud for the specified player */
566
658
  isVisibleTile(tile: Tile, playerName: string): boolean;
567
659
  getTileResourceData(tile: Tile): TileResourceData | undefined;
@@ -792,6 +884,20 @@ export declare interface ParadropSquad {
792
884
  num: number;
793
885
  }
794
886
 
887
+ export declare interface PathFinderOptions {
888
+ /** Restricts the number of searched path nodes (default = Infinity) */
889
+ maxExpandedNodes?: number;
890
+ /** If true, returns a partial path, even if the target is unreachable (default = true) */
891
+ bestEffort?: boolean;
892
+ /** Dynamically exclude path nodes */
893
+ excludeNodes?(node: PathNode): boolean;
894
+ }
895
+
896
+ export declare interface PathNode {
897
+ tile: Tile;
898
+ onBridge: boolean;
899
+ }
900
+
795
901
  export declare enum PipColor {
796
902
  Green = 0,
797
903
  Yellow = 1,
@@ -804,7 +910,7 @@ export declare interface PlayerData {
804
910
  name: string;
805
911
  country: Country | undefined;
806
912
  /** The starting tile coordinates for the player (where the initial MCV is placed) */
807
- startLocation: Point2D;
913
+ startLocation: Vector2;
808
914
  isObserver: boolean;
809
915
  /** Whether the player is an integrated AI. Not applicable to sandbox agents. */
810
916
  isAi: boolean;
@@ -824,6 +930,7 @@ export declare interface PlayerStats {
824
930
  startLocation: number;
825
931
  }
826
932
 
933
+ /** @deprecated use Vector2 instead */
827
934
  export declare interface Point2D {
828
935
  x: number;
829
936
  y: number;
@@ -923,7 +1030,7 @@ export declare class Quaternion extends THREE.Quaternion {
923
1030
  setFromAxisAngle(axis: Vector3, angle: number): this;
924
1031
  setFromRotationMatrix(m: Matrix4): this;
925
1032
  length(): number;
926
- slerp(qb: Quaternion, t: number): this | Quaternion_2;
1033
+ slerp(qb: Quaternion, t: number): this;
927
1034
  }
928
1035
 
929
1036
  export declare interface QueueData {
@@ -1660,6 +1767,13 @@ export declare class V3RocketRules extends MissileRules {
1660
1767
  readIni(ini: IniSection): this;
1661
1768
  }
1662
1769
 
1770
+ export declare class Vector2 extends THREE.Vector2 {
1771
+ length(): number;
1772
+ angle(): number;
1773
+ distanceTo(v: Vector2): number;
1774
+ rotateAround(center: Vector2, angle: number): this;
1775
+ }
1776
+
1663
1777
  export declare class Vector3 extends THREE.Vector3 {
1664
1778
  applyEuler(euler: Euler): this;
1665
1779
  applyAxisAngle(axis: Vector3, angle: number): this;