gem
v0.13.0

A Dota 2 replay parser for Python

The replay, read back as facts.

gem turns Source 2 .dem files into typed Python objects, DataFrames and interactive reports. It reports what happened; the interpretation is yours.

Read the guideSee a reportpip install gem-dota
Offline
No servers, no API keys
Old and new
Both combat-log formats, one schema
Pure Python
3.10 and newer, MIT licensed

01 · Parsing

From replay bytes to a table.

A replay is a stream of protobuf messages and entity updates. gem decodes it into aParsedMatch: players, fights, wards, objectives and the full combat log.

import gem
from gem.constants import hero_display

match = gem.parse("8856501050.dem")
for p in match.players[:5]:
    print(hero_display(p.hero_name),
          p.kills, p.deaths, p.assists,
          p.net_worth, p.gold_per_min)

frames = gem.parse_to_dataframe("8856501050.dem")
HeroK / D / ANet worthGPM
Tiny10 / 6 / 1251,761718
Sniper11 / 8 / 1360,021773
Timbersaw6 / 8 / 2058,262660
Lion4 / 15 / 1919,033332
Ancient Apparition0 / 7 / 1924,409432
Table 1. Vici Gaming, who won match 8856501050, straight from match.players.

02 · The map

A map model you can check.

Regions and camps are traced on the 7.41 map and checked against the replays' own neutral spawners and combat log. gem.region_of(x, y) tells you where any position is. Wards carry exact positions; gem samples heroes each second, and Figure 3 reads them on every replay packet.

Figure 1. gem's map model on the 7.41 map: both halves, the river, the two lotus pools and all 28 neutral camps, coloured by type.
  • large
  • small
  • ancient
  • medium
  • flooded small
  • flooded medium
Figure 3
Figure 2. The 7 wards up at 42:49, the first death of Figure 3's fight. Radiant had 2 observers and 4 sentries up, Dire 1 sentry; circles are observer vision. Over the whole match the teams placed 82 observers, 63 of them in the Radiant half, and 139 sentries.
  • Observer
  • Sentry
  • Radiant
  • Dire
Shadow FiendEmber SpiritTreant Protector
Figure 3. The match's biggest fight, 42:34–43:46: each hero, ringed in its team's colour, where it was at the last death, and ✕ where each hero died.

04 · Where to start