Parsing Replay Files

Does anyone have a map of the structure of the .replay file format?

The files are simple enough to figure out through trial and error but if someone’s already done the legwork or if someone at C1 has a breakdown available, that would save some time.

5 Likes

I’m not doing a ton of stuff with replays yet, but I was thinking of posting my notes. Here they are, it is formatted with Emacs orgmode (https://orgmode.org/). If you are an Emacs user and don’t know about org, just paste the below into a buffer and do M-x org-mode.

It’s not entirely complete but maybe others will find it useful.

** Replay representation and server config
The first line seems to always be blank, followed by printing
basically just the server config in a {“debug”: {…}}.
*** server config
The important config information is the first bit, none of this seems
to have any affect on the replay representation.

  • printMapString: This will print a string representation of the whole
    mape at each step of the game. Useful maybe just for visualizing…

  • printTStrings: This seems to toggle the printing of what would go in
    the replay

  • printActStrings: I can’t tell the difference between this and
    printTStrings.

  • printHitStrings: Seems to toggle the printing of attacking
    information.

  • printPlayerinputstrings: Prints the actions of the players.

  • printBotErrors: Just prints out errors presumably…

  • printPlayerGethitstrings: Prints out a statement when a player is hit
    and their new HP

Basic config
#+BEGIN_SRC json
“debug”: {
“printMapString”: “false”,
“printTStrings”: “false”,
“printActStrings”: “false”,
“printHitStrings”: “false”,
“printPlayerInputStrings”: “true”,
“printBotErrors”: “true”,
“printPlayerGetHitStrings”: “false”
},
#+END_SRC

*** Replay data
The config is followed by another blank line and then we get the
sequence of events. The basic string follows:

#+BEGIN_SRC json
{“p2Units”:[[],[],[],[],[],[],[]],
“turnInfo”:[0,0,-1],
“p1Stats”:[30.0,25.0,5.0,0],
“p1Units”:[[],[],[],[],[],[],[]],
“p2Stats”:[30.0,25.0,5.0,0],
“events”:{“selfDestruct”:[],
“breach”:[],
“damage”:[],
“shield”:[],
“move”:[],
“spawn”:[],
“death”:[],
“attack”:[],
“melee”:[]}
}
#+END_SRC

  • p1Stats: [health, cores, bits, time_of_last_turn]

  • p1Units: [filters, encryptors, Destructors, ping?, EMP?, Scrambler? Remove?]
    sublists of p1Units: [x, y, stability, id]

  • turnInfo: [state_type, turn_number, frame_number]

    • state_type:

      • 0 for beginning of turn, system is waiting for deploy
        information. 3s time limit here. We run on_turn function
      • 1 for action phase, e.g., a single frame. We could run
        something like self.on_frame() here in the algorithm.
      • 2 for end of game. That’s all folks.
    • turn_number: Pretty clear

    • frame_number: -1, 0, or > 0
      I believe that this always starts at -1 where the turn is
      initializing. If neither player has spawned any units, the
      next frame is frame 0 where presumably the map becomes visible
      for both players, and then the turn ends. However, if a
      player has spawned units, the frame counter then continually

  • events: Seems to keep track of the events of the frame, where they
    occur etc…

3 Likes

Hey @RJTK, that’s fantastic. I hadn’t yet spent the time to figure out the turnInfo values and what goes on behind them so thank you for the detailed notes!

Events seem straightforward but I’ll need to test an example of each occurrence to be sure how they are represented. For instance, “spawn” is just a sequence of individual unit spawns that look like: [[x,y],type,id]. These only happen on lines representing frame 0 of a turn. I’m guessing the type numbers match up with the order of the units listed in the configuration line:

0=Filter
1=Encryptor
2=Destructor
3=Ping
4=EMP,
5=Scrambler
6=Remove
(I’ll test this before I do anything with these values)

And the unique Id goes on to be used in the p1Units and p2Units lists which can be used to track each individual unit.

I’ll poke around tonight and share what I find.

3 Likes

I think you are right about the ordering and the id. When I wrote those notes I had verified that filters, encryptors, and destructors are certainly the first 3.

1 Like

I just now verified the number codes above are correct, including 6=Remove. It is treated as a unit spawning at a location and is even given an id.

I also noticed I missed a final value for the spawn segment. It seems to always be equal to 1 no matter what unit is being created.

Example: “spawn”:[[[2,13],6,“10”,1]]
2,13 - is the location
6 - is the Remove unit code
“10” - is an id (The id of the “Remove” unit, not the unit being removed)
1 - is the unknown value. I have yet to see it set to anything but 1.

2 Likes

I believe the last digit is the player number. I think its 1 for player 1 and 2 for player 2.

3 Likes

Well, I feel silly. Confirmed the last number is either 1 for player1 or 2 for player2.

I’ve been testing against a player 2 algo that passes every turn without taking any actions, so I didn’t see what happened when player 2 spawned things. Good call @RuberCuber.

1 Like

Worth noting that if you try and create a unit from the “spawn” data it will fail with:

my-bot:     self.speed = type_config["speed"]
my-bot: KeyError: 'speed'

if there is any Unit with the ‘RM’ property (code 6) since the remove action has no speed associated (makes sense, since you are getting rid of it).

You have to check for this if creating units from “speed”.

2 Likes

Does anyone have any additional information about the “move” option:

When I print it out it’s all pretty clear, except for the third item:

[[16, 2], [16, 3], [0, 0], 5, '182', 1]

I always get [0,0] so I’m wondering if anyone knows what it is measuring. The only thing I can think of is damage taken/received or maybe the position of the position it is targeting, except I don’t see the value change when close to another unit, but maybe I’m just missing it.

1 Like

Tested and confirmed the “selfDestruct” segment, broken down below.

Note that in accordance with the rules, an information unit that travels less than 5 spaces before running out of move options does not self destruct (does no damage) but does get removed from play. I’ve confirmed that such units that run out of moves do not appear in the “selfDestruct” segment, only in the “death” segment. An IU that travels at least 5 spaces before getting stuck appears in both the “selfDestruct” and “death” segments.

“selfDestruct”:[[[25,13], # x,y position of the self-destructed unit
[[24,14], # x,y position of an affected unit
[25,14], # x,y position of a second affected unit
[26,14]], # x,y position of a third affected unit
15.0, # damage dealt ( = starting stability of the self-destructing unit)
3, # Type code of unit that self destructed (3 = Ping, 4=EMP, 5=Scrambler)
“155”, # id of self destructing unit
1]], # player number (1 or 2) who owned the self-destructing unit

3 Likes

Tested and confirmed the “breach” segment. One entry in this segment appears for every Information Unit that makes it to the opponent’s opposite edge and scores, dealing health damage. IU’s that appear in the “breach” segment also appear in the “death” segment of the same frame as they are removed from the board.

“breach”:[[[13,27], # x,y position where IU hit opponents edge
1.0, # Amount of health damage done to opponent (always 1.0)
3, # Type code of unit that hit (3 = Ping, 4 = EMP, 5 = Scrambler)
“7”, # id of unit that scored
1]], # Owner of the scoring unit (1 = Player1, 2 = Player2)

5 Likes

Tested and confirmed the “damage” segment. One entry appears for every instance of damage dealt to any unit. The same unit can appear multiple times, once for each source of damage that turn. Damage can come from enemy units attacking or self-destructing, each of which have corresponding entries in the “attack” and “selfDestruct” segments.

“damage”:[[[27,14], # x,y position of unit which took damage
1.0, # amount of damage done (depends on attacking unit)
0, # Type code of unit which took damage (0 = Filter, 1 = Encryptor, 2 = Decryptor, 3 = Ping, 4 = EMP, 5 = Scrambler)
“4”, # id of unit which took damage
2]], # Owner of unit which took damage (1 = Player1, 2 = Player2)

4 Likes

Tested and confirmed the “shield” segment. One entry appears for every instance of a shield being applied to an Information Unit. The same IU can appear in the segment multiple times, each time affected by a different encryptor. The same encryptor can appear in the segment multiple times, each time shielding a different IU. Affected IU’s entries in the “p1Units” segment reflect the shielded amount in the same frame. Note that the decay on shields does not seem to be indicated in any special way and can only be seen as its effect on IU stability values.

“shield”:[[[1,13], # x,y position of shield-giving unit
[2,11], # x,y position of shield-receiving unit
10.0, # amount of shield given
1, # Probably type code of shield-giving unit (Always 1 = Encryptor)
“54”, # id of the shield-giving unit
“117”, # id of the shield-receiving unit
1]], # Owner of the units (1 = Player1, 2 = Player2)

3 Likes

Tested and confirmed the “move” segment. The 3rd value of each move seems to always be [0,0], still not sure what that represents. Note that newly spawned units do not move in the same frame they spawned in but their spawn frame counts towards their next move (A ping spawned on turn 0 will first move on turn 1).

“move”:[[[2,11], # x,y starting position of unit
[2,12], # x,y ending position of unit
[0,0], # ???
3, # Type code of unit (3 = Ping, 4 = EMP, 5 = Scrambler)
“117”, # id of unit
1]], # Owner of the unit (1 = Player1, 2 = Player2)

4 Likes

Tested and confirmed the “death” segment. Units removed from play for any reason show in this segment (attacked to death, successfully breached opposite edge, self-destructed, removed by owner).

“death”:[[[13,27], # x,y position of unit
3, # Type code of unit (0 = Filter, 1 = Encryptor, 2 = Destructor, 3 = Ping, 4 = EMP, 5 = Scrambler)
“7”, # id of unit
1, # Owner of unit (1 = Player1, 2 = Player2)
false]], # Unit was destroyed by a “Remove” command (true/false)

4 Likes

Last one, since “melee” seems to be completely unused.

Tested and confirmed the “attack” segment.

“attack”:[[[0,13], # x,y position of attacking unit
[0,14], # x,y position of target unit
1.0, # damage of attack
3, # Type code of attacking unit (2 = Destructor, 3 = Ping, 4 = EMP, 5 = Scrambler)
“117”, # id of attacking unit
“60”, # id of target unit
1]], # Owner of attacking unit (1 = Player1, 2 = Player2)

5 Likes

On the last line of the replay file there should also be a field keyed by “endStats”. This contains a few summary statistics, which is quite convenient because I was planning on calculating these things myself. Here’s a dump of my org-mode notes on this section:

**** endStats
This appears only in the last frame and has some summary information.
#+BEGIN_SRC json
{
“endStats”: {
“duration”: 6945,
“winner”: 1,
“player1”: {
“stationary_resource_spent”: 277.0,
“dynamic_resource_spoiled”: 70.0,
“crashed”: false,
“name”: “T035”,
“dynamic_resource_destroyed”: 298.0,
“dynamic_resource_spent”: 330.0,
“stationary_resource_left_on_board”: 112.0,
“points_scored”: 32.0,
“total_computation_time”: 441
},
“frames”: 2323,
“player2”: {
“stationary_resource_spent”: 258.0,
“dynamic_resource_spoiled”: 70.0,
“crashed”: false,
“name”: “T036”,
“dynamic_resource_destroyed”: 324.0,
“dynamic_resource_spent”: 330.0,
“stationary_resource_left_on_board”: 37.0,
“points_scored”: 6.0,
“total_computation_time”: 458
},
“turns”: 54
}
}
#+END_SRC

- _duration_: (int) Duration of the game in milliseconds?
- _winner_: (int) player id of the winner (1 or 2)
- _frames_: (int) number of frames elapsed
- _turns_: (int) number of turns elapsed
- _player#_: (json) More summary info described below

***** player#
- stationary_resources_spent: (float) Number of cores spent
- dynamic_resources_spoiled: (float) Number of bits decayed
- crashed: (bool) Whether or not the algo crashed
- name: (str) name of the player’s algorithm
- dynamic_resource_destroyed: (float) Amount of bits worth of information lost.
- dynamic_resource_spent: (float) number of bits spent
- stationary_resources_left_on_board: (float) cores worth of
units remaining
- points_scored: (float) damage dealt to the opponent
- total_computation_time: (int) compute time in millis.

2 Likes

Im giving Jumpster the community helper badge for doing all this great data mining

5 Likes

so I just wanted to put all of Jumpster’s stuff and RJTK’s stuff into one example string, I know I got something wrong so someone tell me what: Here is what I have so far

turn_0 = {
    "p2Units":[[],[],[],[],[],[],[]], # [filters, encryptors, Destructors, ping?, EMP?, Scrambler? Remove?] (I do not get the sublists someone explain that.)
    "turnInfo":[0,0,-1], # [state_type, turn_number, frame_number]
    "p1Stats":[30.0,25.0,5.0,0], # [health, cores, bits, time_of_last_turn]
    "p1Units":[[],[],[],[],[],[],[]], # [filters, encryptors, Destructors, ping?, EMP?, Scrambler? Remove?] (I do not get the sublists someone explain that.)
    "p2Stats":[30.0,25.0,5.0,0], # [health, cores, bits, time_of_last_turn]
    "events": {
        "selfDestruct":[[[25,13], [[24,14], [25,14], [26,14]], 15.0, 3, "155", 1]], # [[x, y](of unit), [x, y](of affected unit1), [x, y](of affected unit2), [x, y](of affected unit3), damage dealt, type of unit, id, player number]
        "breach":[[[13,27], 1.0, 3, "7", 1]], # [[x, y], damage, type of unit, id, player number]
        "damage":[[[27,14], 1.0, 0, "4", 2]], # [[x, y], damage, type of unit, id, player number]
        "shield":[[[1,13], [2,11], 10.0, 1, "54", "117", 1]], # [[x, y](of Encrypter), [x, y](of reciving unit), amount of shield, type of unit (1), id(shield giving), id(shield receiving), player number]
        "move":[[[2,11], [2,12], [0,0], 3, "117", 1]], # [[x, y](starting), [x, y](ending), ???, type of unit, id, player number]
        "spawn":[[[2,13],6,"10",1]], # [[x, y], Remove unit code, id, player number]
        "death":[[[13,27], 3, "7", 1, false]], # [[x, y], type of unit, id, player number, if it was using "Remove"]
        "attack":[[[0,13], [0,14], 1.0, 3, "117", "60", 1]], # [[x, y](attacking unit), [x, y](targeted unit), damage, type of unit (attacking), id (attacking), id (targeted), player number]
        "melee":[] # unused
        }
}

To get a better view you might want to copy paste into a new python file

5 Likes

Quick question, does 2 always refer to enemy and 1 always refer to myself in the last element of “damage”:[[[27,14], 1.0, 0, “4”, 2]]? If not, how can we manipulate to make sure I always get the information on my side or the enemy’s side?

Thank you!

1 Like