← Back to AI coach

AI coach reference

What's inside a parsed replay

A StarCraft 2 replay file isn't the visual playback you see in-game. It's a recorded log of every event the game engine processed: every unit built, every ability cast, every kill. The parser replays that log against a simulation and writes out a structured snapshot of what happened. This page covers what comes out of that snapshot.

Most of what follows is straight out of the replay file. A few sections are partly our own work — joining, collapsing, or sampling the raw events into something easier to read. Where we added something, you'll see a small We added note inside the block. Brood War replays go through a different parser, but the output is shaped the same way.

Game metadata

The basics about the match itself.

map:               "10000 Feet LE"
gameType:          "1v1"
durationSeconds:   800
durationFormatted: "13:20"
playedAt:          "2026-04-30T19:14:08"
expansion:         "LotV"
build:             96883
gameVersion:       "5.0.15.96883"
category:          "Ladder"
isLadder:          true

category is one of Ladder, Public, Private, or Co-op. The coach refuses to run on co-op replays since the metagame and unit kits are different from ladder. expansion is WoL, HotS, or LotV for SC2 replays, and SC1 or BW for Brood War replays.

Teams and players

Each team holds a list of player IDs and a final result. Each player carries identity, race, performance, and their per-game records.

team:
  id:       1
  result:   "Loss"
  players:  [<player>, <player>]

player:
  id:                     1
  name:                   "MaSa"
  toonHandle:             "1-S2-1-1234567"
  race:                   "Terran"      // race they actually played
  pickRace:               "Terran"      // race they picked at lobby (Random differs)
  color:                  "#ff0000"
  team:                   1
  result:                 "Loss"
  apm:                    287
  mmr:                    6234
  commander:              null          // co-op only (Raynor, Kerrigan, Artanis…)
  leftAt:                 652           // gg / elimination time, in seconds
  workersCreated:         78
  totalMineralsCollected: 18450
  totalGasCollected:      6320

toonHandle is Blizzard's player identifier. The first segment encodes the server region: 1 = NA, 2 = EU, 3 = KR, 5 = CN. mmr only comes back for player one in team games. That's a limitation of how Blizzard writes the replay file, not a parser bug. leftAt tells us when each player's session ended in game-time seconds, which distinguishes a typed-gg surrender from an elimination (the loser's structures still alive vs all dead).

pickRace matters for Random players: the in-lobby pick is Random, but race is what they ended up rolling. The coach reads both so it can tell the difference between "you opened a Random" and the eventual race decision.

We addedworkersCreated, totalMineralsCollected, and totalGasCollected are running totals we compute by walking the event stream — the replay doesn't store them. leftAt is also ours, computed from each player's PlayerLeaveEvent frame.

Build orders

Every structure and unit each player started, in order, with the supply count at the moment of construction and the frame the order fired.

buildOrder: [
  { frame:  134, time: "0:09", supply: 12, name: "Refinery",   isWorker: false },
  { frame:  201, time: "0:14", supply: 13, name: "SCV",        isWorker: true  },
  { frame:  401, time: "0:28", supply: 14, name: "Barracks",   isWorker: false },
  { frame:  689, time: "0:48", supply: 15, name: "OrbitalCmd", isWorker: false },
  { frame: 1402, time: "1:38", supply: 17, name: "Reaper",     isWorker: false },
  …
]

The replay's tracker stream emits a separate event for every unit ordered, started, and born. We collapse those into one chronological list per player.

We addedisWorker is our flag, set on every SCV / Probe / Drone so the viewer can hide them by default. We also collapse morph variants back to the canonical name (so a Roach morphing into a Ravager doesn't double-count) and detect cancelled constructions so the coach doesn't mistake an extractor trick for two extractors.

Units produced, lost, and killed

Three parallel ledgers tracking every unit that was built, every unit that died, and every unit that was killed by this player. Each entry holds a frame, a game-time string, and the unit name.

unitsProduced: [{ frame:  401, time: "0:28", name: "Marine",   count: 1 }, …]
unitsLost:     [{ frame: 4823, time: "5:22", name: "Marine",   count: 1 }, …]
unitsKilled:   [{ frame: 4901, time: "5:27", name: "Zergling", count: 1 }, …]

Each entry has count: 1 — they're per- event, not aggregated. We need the per-event timestamps to detect battles. Aggregating them up front would wash out which kills happened together.

Resource and supply timelines

Per-player time series. Used to plot the income and supply graphs you see on the replay page, and read by the AI coach to spot supply blocks and unspent banks.

supply:      [{ time: 60, value: 23 },  { time: 90, value: 28 },  …]
apmTimeline: [{ time: 60, value: 165 }, { time: 90, value: 198 }, …]

resources:
  mineralsUnspent: [{ time: 60, value:  235 }, …]   // unspent bank
  gasUnspent:      [{ time: 60, value:    0 }, …]
  mineralIncome:   [{ time: 60, value:  790 }, …]   // resources / min
  gasIncome:       [{ time: 60, value:    0 }, …]   // gas tracked separately

Income is split into mineralIncome and gasIncome so the coach can spot a player who's mining minerals fine but starved of gas (a common cause of delayed tech). A sudden drop of mineral income by ~700 over a 30-second window usually means the opponent killed a base.

We addedapmTimeline is smoothed by us over a rolling window so single-second action spikes don't dominate the chart. The raw replay just has individual action events; computing a human-readable per-minute curve from them is the parser's job.

Upgrades

What completed and when. Stim, +1 attack, Hive, Charge, Blink, all of them. The coach uses these to check whether the player's tech path matched what their build was telegraphing.

upgrades: [
  { frame:  2480, time: "2:45", name: "ShieldsLevel1" },
  { frame:  4150, time: "4:36", name: "Charge"        },
  { frame:  6420, time: "7:08", name: "GroundWeaponsLevel1" },
  …
]

Timeline snapshots

The heart of the dataset. Every 30 seconds of game time, we record a snapshot of the entire match: what each player had alive, what they were spending, where their economy stood. The coach reads from this series more than anything else.

timeline:
  - gameTimeSeconds: 60
    players:
      "1":
        workerCount:           14
        supply:                17
        supplyMax:             23
        armyValue:            100
        mineralsUnspent:       35
        gasUnspent:             0
        mineralCollectionRate:780
        gasCollectionRate:      0
        activeUnits:          { Marine: 2, SCV: 14 }
      "2":
        workerCount:           14
        …
  - gameTimeSeconds: 90
    …

activeUnits is the source of truth for army composition at any time. Battle detection, composition analysis, and the post-fight unit list all read from this map. mineralCollectionRate and gasCollectionRate are the sampled instantaneous rates at that snapshot, separated so the coach can call out gas-starved openings even when mineral income looks healthy.

We addedThe whole timeline series is ours. The replay holds an event log; we replay it against a simulation, sample every player's state every 30 seconds, and write out these snapshots. armyValue is also our calculation — we sum up the resource cost of every unit currently alive on each side, with the same per- unit values used by battle detection.

Unit and base position tracks

Coordinate streams for every unit owned and every town hall built. Used by the heatmap, the fog-of-war replay viewer, and the scouting/aggression timelines.

unitTracks: [
  {
    pid:    1,
    name:   "Reaper",
    bornAt: 92,           // game-time seconds when this unit appeared
    bornX:  124,
    bornY:   88,
    diedAt: 318,          // null while still alive at game end
    positions: [
      { t:  95, x: 130, y:  84 },
      { t: 110, x: 158, y:  72 },
      …
    ]
  },
  …
]

baseTracks: [
  {
    pid:         1,
    name:        "CommandCenter",
    x:         119,
    y:          21,
    builtAt:     0,
    destroyedAt: null,    // null while still standing at game end
    typeChanges: [
      { at: 200, name: "OrbitalCommand" },     // upgraded at 200s
      { at: 720, name: "PlanetaryFortress" }
    ]
  },
  …
]

Position units are in-game tiles. Time is in seconds. For unit tracks the position list is sampled, not per-frame: workers get down-sampled aggressively since their motion is mostly mineral patch to hall and back. Bases hold a single (x, y) instead of a list because buildings don't move (lifted Terran being the exception, which we treat as a separate flying unit track).

typeChanges tracks the same hall as it morphs through tiers: Hatchery → Lair → Hive on Zerg, Command Center → Orbital → Planetary on Terran. The coach reads this to judge expansion timing without getting confused by the upgrade event.

We addedThe replay stores raw position-update events for each unit ID. We join those into per-unit tracks, resolvebornAt / diedAt from the corresponding birth and death events, and stitch tier upgrades into typeChanges on the same base instead of treating them as separate buildings.

Ability events

High-leverage abilities indexed by type, with cast time and map position. Today we track scans, EMPs, storms, forcefields, and MULEs — the abilities that materially change the outcome of a fight or a mining round. Each entry holds a game-time second, an (x, y) on the map, and the player who cast it.

abilityEvents:
  scans:       [{ t: 425.5, x: 151.2, y: 126.2, pid: 1 }, …]
  emps:        [{ t: 552.0, x:  88.5, y:  92.0, pid: 2 }, …]
  storms:      [{ t: 612.4, x: 110.0, y: 124.7, pid: 3 }, …]
  forceFields: [{ t: 488.1, x:  96.0, y: 102.0, pid: 4 }, …]
  mules:       [{ t: 240.0, x: 119.0, y:  21.0, pid: 1 }, …]

The tactic detector reads these for plays that hinge on an ability firing — recall escapes, EMP timings, scan patterns. The audio commentary feature also reads them to call out cool moments live. Other abilities (Stim, Time Warp, Yamato, Mothership recall) aren't indexed today; if you need one, file an issue and we'll add it to the next parser bump.

We addedEvery ability cast is in the replay's event stream. We filter to the five we currently care about and bucket them by name — the structure here is ours. The raw stream has hundreds of ability types, including most things players spam-click that aren't worth a coach's attention.

Map bounds

The playable area of the map in tile coordinates. We need this to scale positions correctly when overlaying tracks on the minimap.

mapBounds:
  minX:  16
  minY:   8
  maxX: 184
  maxY: 152
We addedWe compute these bounds by walking every position we saw across all unit and base tracks. The replay header does carry a logical map size, but it includes unplayable margins; the observed-bounds approach gives us the actual playable rectangle the renderer uses.

What the parser doesn't pull

A few things you might expect to be in there but aren't:

  • Camera positions. We don't track where each player's screen was looking. Camera events are in the file, but they're noisy and expensive to process for the return.
  • Selection groups. We don't track which units were in each control group. Available, just not used by anything we currently do.
  • Chat messages. Player chat is excluded for privacy, even though the parser can read it. The replay viewer only shows public events.
  • Per-frame APM traces. APM is exposed as a smoothed per-minute value, not a per-frame action stream.

Anything missing or wrong here? Drop a note to team@starcraft2.ai and we'll fix it.