gem
v0.13.0

Analysis Helpers

Post-parse utilities in gem.analysis that transform raw ParsedMatch / ParsedPlayer data into higher-level structures for agentic and analytical use.

Note: assess_point_vision and match.vision_modifiers are experimental. Point calculations use straight-line geometry only — high-ground penalties, terrain line-of-sight (trees/cliffs), and per-hero vision range modifiers are not modelled. Treat geometry as an approximation and prefer hero_visibility_at(...) for a canonical player hero.

For the full derivation, data flow, and limitations, see Experimental Features → Point-Vision Evidence.

Canonical implementation modules are split by responsibility:

  • gem.analysis.spatial — position, nearby-hero, and net-worth lookup helpers
  • gem.analysis.combat — ability-hit grouping and fight helpers
  • gem.analysis.abilities — ability-level lookup helpers
  • gem.analysis.vision — geometry-based vision approximation helpers
  • gem.analysis.farming — camp-by-camp farming routes
  • gem.analysis.roshan — Roshan kills, Aegis lifecycles and the window that followed
  • gem.analysis.smoke — evidence-first Smoke of Deceit lifecycle analysis
  • gem.analysis.fight_positioning — bounded fight-moment spatial evidence

gem.analysis re-exports the public helpers below. Use the implementation modules directly only when module-level imports are useful.

All functions are exported directly from gem.*:

import gem

pos     = gem.position_at_tick(player, tick)
casts   = gem.group_ability_hits(match.combat_log)
fight   = gem.fight_at_tick(match, tick)
near    = gem.heroes_near(match, tick, x, y, radius=2000)
lvl     = gem.ability_level_at_tick(player, "axe_berserkers_call", tick)
vision  = gem.assess_point_vision(match, team=2, tick=tick, x=x, y=y)
smokes  = gem.build_smoke_analysis(match)
fights  = gem.build_fight_positioning(match)
rosh    = gem.build_rosh_conversions(match)

Roshan records expose roshan_team_source, conversion_team_source, aegis_fate_source, engagement-aware fight_evidence and the signed differential_profile. gem 0.13 removed the tags, verdicts and territory fields. See Roshan Conversion and its calibration record.


position_at_tick

gem.position_at_tick(player: ParsedPlayer, tick: int) -> tuple[float, float] | None

Return the closest recorded (x, y) position for a player at a given tick.

Searches player.position_log (sampled at ~1-second intervals) for the entry with the smallest tick distance to the requested tick. Ties go to the earlier sample.

Returns None if position_log is empty.

Example:

pos = gem.position_at_tick(axe_player, fight.start_tick)
if pos:
    print(f"Axe was at ({pos[0]:.0f}, {pos[1]:.0f}) when the fight started")

group_ability_hits

gem.group_ability_hits(
    combat_log: list[CombatLogEntry],
    window_ticks: int = 5,
) -> list[AbilityCast]

Group DAMAGE combat log entries into per-cast AbilityCast records.

Many abilities hit multiple targets simultaneously (Ravage, Black Hole, RP). The combat log emits one DAMAGE entry per target. This function collapses those into a single AbilityCast record with a targets list and total_damage sum.

Only entries with a non-empty inflictor_name are considered (auto-attacks are excluded). Entries from the same (caster, ability) pair within window_ticks are merged.

AbilityCast fields:

cast.tick          # int: tick of the first hit
cast.caster        # str: NPC name of the casting unit
cast.ability       # str: ability/item inflictor name
cast.targets       # list[str]: NPC names of all units hit
cast.total_damage  # int: sum of all damage values
cast.damage_type   # str: damage type of the first hit
cast.stun_duration # float: stun seconds (first hit with a stun)
cast.entries       # list[CombatLogEntry]: raw entries that compose this cast

Example:

casts = gem.group_ability_hits(match.combat_log)
big_hits = [c for c in casts if len(c.targets) >= 3]
for cast in big_hits:
    print(f"{cast.caster} hit {len(cast.targets)} heroes with {cast.ability} "
          f"for {cast.total_damage:,} total damage")

fight_at_tick

gem.fight_at_tick(match: ParsedMatch, tick: int) -> Fight | None

Return the Fight whose [start_tick, end_tick] window contains tick, or None.

Uses binary search — O(log N). Fights are assumed non-overlapping and sorted by start_tick (as produced by detect_fights).

Example:

for entry in match.combat_log:
    fight = gem.fight_at_tick(match, entry.tick)
    if fight:
        print(f"Event at tick {entry.tick} during fight won by {fight.winner}")

heroes_near

gem.heroes_near(
    match: ParsedMatch,
    tick: int,
    x: float,
    y: float,
    radius: float,
) -> list[ParsedPlayer]

Return all heroes within radius world units of (x, y) at tick, sorted by ascending distance.

Uses position_at_tick internally. Heroes with no position data are excluded.

Example:

# Who was close enough to join the fight at initiation?
if fight.centroid_x is not None:
    nearby = gem.heroes_near(match, fight.start_tick,
                             fight.centroid_x, fight.centroid_y, radius=2000)
    for player in nearby:
        pos = gem.position_at_tick(player, fight.start_tick)
        print(f"{player.hero_name} at {pos}")

ability_level_at_tick

gem.ability_level_at_tick(
    player: ParsedPlayer,
    ability: str,
    tick: int,
) -> int

Return the level (1–4) of an ability for a player at a given tick.

Uses per-minute ability level snapshots stored in player._ability_snapshots (populated automatically by gem.parse()). Returns the last known level at or before tick. Returns 0 if the ability was not yet learned.

Ability names match the inflictor_name field in the combat log (e.g. "axe_berserkers_call").

Example:

casts = gem.group_ability_hits(match.combat_log)
for cast in casts:
    player = gem.find_player(match, cast.caster)
    if player:
        lvl = gem.ability_level_at_tick(player, cast.ability, cast.tick)
        print(f"{cast.ability} cast at level {lvl}")

hero_visibility_at

state = gem.hero_visibility_at(
    match,
    player_id=7,
    observing_team=2,
    tick=120_000,
)

if state is gem.VisibilityState.VISIBLE:
    print("Radiant could see player 7's canonical hero")

This query reads the replay's team visibility bitsets. It returns one of VISIBLE, HIDDEN, or UNKNOWN; times before the first observation and after an entity is deleted or replaced are UNKNOWN. The result applies to that hero entity only. It does not reconstruct the visible map area or identify which hero, ward, or spell supplied the vision.

Source 2 combat-log entries also expose event-local visible_radiant and visible_dire values. Those values are None when the optional protobuf field was absent, including for legacy Source 1 events.

entity_visibility_at

state = gem.entity_visibility_at(
    match,
    entity_index=427,
    entity_serial=12,
    observing_team=3,
    tick=120_000,
)

The index-plus-serial identity prevents slot reuse from leaking state. The latest eligible event wins; inactive terminal events return UNKNOWN. Coverage is limited to active networked Dota NPC entities and packet-boundary evidence, not arbitrary-point fog of war, source attribution, temporary viewers, or terrain/navigation simulation.


assess_point_vision (experimental)

Detailed explanation:

  1. Experimental Features → Point-Vision Evidence
assessment = gem.assess_point_vision(
    match: ParsedMatch,
    team: int,
    tick: int,
    x: float,
    y: float,
    *,
    target_player_id: int | None = None,
    max_position_age_ticks: int = 150,
) -> PointVisionAssessment

Assess the evidence that currently modelled sources covered (x, y) for the given team. The result distinguishes supported, unsupported, and incomplete; negative geometry is never presented as authoritative fog.

Hero sources retain their sampled coordinate, sample tick, age, player identity, and freshness provenance. Samples older than max_position_age_ticks are excluded and recorded as evidence gaps. Observer wards use their entity-derived placement coordinates and a half-open placement-to-removal lifetime.

Point-coverage sources:

SourceRadiusNight penalty
Allied hero1800 (day) / 800 (night)Yes
Observer ward1600No

Sentries are not standard vision sources. Missing or stale hero positions, unknown ward ownership, missing observer coordinates, and queries beyond the observed replay horizon are represented as gaps.

When target_player_id is supplied, the assessment also exposes the canonical target's authoritative visible/hidden/unknown state and any defensible direct-target reveal intervals. Those facts remain separate from map-point geometry.

if assessment.status is gem.PointVisionStatus.SUPPORTED:
    print(assessment.sources[0])
elif assessment.status is gem.PointVisionStatus.INCOMPLETE:
    print([gap.code for gap in assessment.gaps])

Point-source limits (experimental)

assess_point_vision(...).sources is the distance-sorted modelled geometry (gem 0.13 removed the old estimate_vision list view). Direct-target modifiers are not arbitrary-point sources; query them for a specific canonical target through assess_point_vision(..., target_player_id=...).

The point geometry is an approximation:

  • No high-ground vision penalties
  • No summon/creep vision (only heroes and observer wards)
  • No sentry ward true-sight (sentries do not grant standard vision)
  • Aura/carrier geometry is not modelled; those events are not direct reveal sources

Vision modifiers (match.vision_modifiers) (experimental)

Detailed explanation:

  1. Experimental Features → Vision Modifiers

ParsedMatch.vision_modifiers is a list[VisionModifierEvent] populated by gem.parse(). It retains direct reveals, aura/carrier evidence, non-hero targets, lifecycle evidence, and team provenance. Ambiguous/orphan removals are available separately as match.vision_modifier_pairing_issues.

VisionModifierEvent fields:

ev.tick           # int: tick when modifier was applied
ev.end_tick       # int | None: observed removal tick only
ev.modifier_name  # str: e.g. "modifier_slardar_amplify_damage"
ev.target_name    # str: NPC name of the revealed hero
ev.caster_name    # str: NPC name of the caster
ev.caster_team    # int: team of the caster (2=Radiant, 3=Dire)
ev.semantic       # direct_target_reveal, reveal_aura, aura_carrier, ...
ev.lifecycle_status  # removed, expired, open, or incomplete
ev.close_evidence    # observed, duration_inferred, unobserved, or ambiguous

Tracked modifier names:

ModifierSource
modifier_slardar_amplify_damageSlardar — Corrosive Haze (ultimate)
modifier_bounty_hunter_trackBounty Hunter — Track
modifier_item_dustofappearanceDust of Appearance (item)
modifier_item_gem_of_true_sightGem carrier evidence (not a direct reveal)
modifier_gem_active_truesightGem reveal-aura evidence

Example — how long was each hero tracked by BH?

for ev in match.vision_modifiers:
    if ev.modifier_name == "modifier_bounty_hunter_track":
        print(ev.target_name, ev.lifecycle_status, ev.end_tick)

net_worth_at

gem.net_worth_at(player: ParsedPlayer, tick: int) -> int

Return the closest sampled net worth for a player at a given tick.

Uses a linear scan over player.times / player.net_worth_t to find the sample with the smallest tick distance. Returns 0 if no data is available.

Example:

for fight in match.fights:
    for p in fight.players:
        player = match.players[p.player_id]
        nw = gem.net_worth_at(player, fight.start_tick)
        print(f"{player.hero_name}: {nw:,} NW at fight start")

ward_vision_impact

gem.ward_vision_impact(ward, match: ParsedMatch) -> int

Count distinct enemy heroes spotted by an observer ward during its lifetime.

Checks whether any hero position sample falls within the 1600-unit observer ward vision radius while the ward was alive. Only the first sighting per hero is counted. Sentry wards return 0.

Example:

for ward in match.wards:
    if ward.ward_type == "observer":
        impact = gem.ward_vision_impact(ward, match)
        print(f"Ward at ({ward.x:.0f}, {ward.y:.0f}) spotted {impact} enemy heroes")

is_active_fight_participant

gem.is_active_fight_participant(player_stats) -> bool

Return True if a player was an active participant in a fight — i.e. they had direct hero-vs-hero combat: a death, damage dealt, damage taken, or healing.

Passive presence (farming nearby, casting only on creeps) does not count.

Example:

fight = match.fights[0]
active = [p for p in fight.players if gem.is_active_fight_participant(p)]
print(f"{len(active)} active participants in fight")

format_npc_name

gem.format_npc_name(name: str) -> str

Convert an NPC name to a human-readable label by stripping Dota 2 prefixes (npc_dota_, goodguys_, badguys_) and replacing underscores with spaces. For heroes, prefer gem.constants.hero_display() which returns the official display name.

Example:

gem.format_npc_name("npc_dota_goodguys_tower_top_1")
# → "tower top 1"

resolve_pick_team

gem.resolve_pick_team(event: DraftEvent, players: list[ParsedPlayer]) -> int

Resolve the correct team (2=Radiant, 3=Dire) for a draft event. DraftEvent.team comes from m_pGameRules.m_iActiveTeam, which is reliable for bans but can be wrong for picks in HLTV replays and coach-slot edge cases.

For picks, cross-references the hero name against the post-game player roster. For bans, falls back to event.team.

Example:

for event in match.draft:
    team = gem.resolve_pick_team(event, match.players)
    side = "Radiant" if team == 2 else "Dire"
    action = "picks" if event.is_pick else "bans"
    print(f"{side} {action} {event.hero_name}")

Replay download helpers

fetch_replay

gem.fetch_replay(match_id: int, out_dir: str | Path = ".") -> Path

Download and decompress a replay from OpenDota in one call. Returns the path to the decompressed .dem file.

dem_path = gem.fetch_replay(8734577999, out_dir="replays/")
match = gem.parse(str(dem_path))

fetch_replay_url

gem.fetch_replay_url(match_id: int) -> str

Fetch just the replay download URL from the OpenDota API. Raises ValueError if no replay URL is available.

download_and_decompress

gem.download_and_decompress(match_id: int, replay_url: str, out_dir: str | Path = ".") -> Path

Lower-level helper: download from a known URL and decompress .bz2 → .dem.


Generated API

Module gem.analysis.spatial

Spatial and time-series helpers for parsed match analysis.

Source: src/gem/analysis/spatial.py

Top-level functions

position_sample_at_tick

def position_sample_at_tick(player: ParsedPlayer, tick: int) -> SampledPosition | None

Return the nearest recorded position with its sampling metadata.

Source: src/gem/analysis/spatial.py:35

position_at_tick

def position_at_tick(player: ParsedPlayer, tick: int) -> tuple[float, float] | None

Return the closest recorded (x, y) position for a player at a given tick.

Source: src/gem/analysis/spatial.py:73

heroes_near

def heroes_near(match: ParsedMatch, tick: int, x: float, y: float, radius: float) -> list[ParsedPlayer]

Return all heroes within radius world units of (x, y) at tick.

Source: src/gem/analysis/spatial.py:102

net_worth_at

def net_worth_at(player: ParsedPlayer, tick: int) -> int

Return the closest sampled net worth for a player at the given tick.

Source: src/gem/analysis/spatial.py:143

Top-level classes

SampledPosition

class SampledPosition

Nearest recorded player position and its sampling provenance.

Source: src/gem/analysis/spatial.py:19

Dataclass fields

NameTypeDefault
xfloat-
yfloat-
sample_tickint-
age_ticksint-

Module gem.analysis.regions

Map regions: which named area of the 7.41 map a world position is in.

Source: src/gem/analysis/regions.py

Top-level functions

region_of

def region_of(x: float, y: float) -> str

Classify a world position into a map region.

Source: src/gem/analysis/regions.py:84

Module gem.analysis.combat

Combat-log and fight analysis helpers.

Source: src/gem/analysis/combat.py

Top-level functions

group_ability_hits

def group_ability_hits(combat_log: list[CombatLogEntry], window_ticks: int = 5) -> list[AbilityCast]

Group DAMAGE combat log entries into per-cast AbilityCast records.

Source: src/gem/analysis/combat.py:50

find_fights

def find_fights(match: ParsedMatch, *, window_s: float = FIGHT_WINDOW_S, radius: float | None = FIGHT_RADIUS) -> list[Fight]

Group a parsed match's hero deaths into fights with your own settings.

Source: src/gem/analysis/combat.py:124

fight_at_tick

def fight_at_tick(match: ParsedMatch, tick: int) -> Fight | None

Return the fight window that contains the given tick, or None.

Source: src/gem/analysis/combat.py:196

is_active_fight_participant

def is_active_fight_participant(player_stats: object) -> bool

Return True if a player was an active participant in a fight.

Source: src/gem/analysis/combat.py:229

Top-level classes

AbilityCast

class AbilityCast

A single ability (or item) cast with all targets it hit.

Source: src/gem/analysis/combat.py:25

Dataclass fields

NameTypeDefault
tickint-
casterstr-
abilitystr-
targetslist[str]field(...)
total_damageint0
damage_typestr''
stun_durationfloat0.0
entrieslist[CombatLogEntry]field(...)

Module gem.analysis.abilities

Ability-level lookup helpers.

Source: src/gem/analysis/abilities.py

Top-level functions

ability_level_at_tick

def ability_level_at_tick(player: ParsedPlayer, ability: str, tick: int) -> int

Return the level of an ability for a player at a given tick.

Source: src/gem/analysis/abilities.py:12

Module gem.analysis.vision

Evidence-aware visibility helpers for parsed matches.

Source: src/gem/analysis/vision.py

Top-level functions

is_daytime

def is_daytime(game_start_tick: int | None, tick: int) -> bool

Return True if it is daytime at the given absolute tick.

Source: src/gem/analysis/vision.py:216

hero_visibility_at

def hero_visibility_at(match: ParsedMatch, *, player_id: int, observing_team: int, tick: int) -> VisibilityState

Return authoritative hero-entity visibility at or before tick.

Source: src/gem/analysis/vision.py:243

entity_visibility_at

def entity_visibility_at(match: ParsedMatch, *, entity_index: int, entity_serial: int, observing_team: int, tick: int) -> VisibilityState

Return authoritative NPC-entity visibility at or before tick.

Source: src/gem/analysis/vision.py:285

assess_point_vision

def assess_point_vision(match: ParsedMatch, team: int, tick: int, x: float, y: float, *, target_player_id: int | None = None, max_position_age_ticks: int = 150) -> PointVisionAssessment

Assess bounded modeled evidence for team vision of one map point.

Source: src/gem/analysis/vision.py:331

ward_vision_impact

def ward_vision_impact(ward: object, match: ParsedMatch) -> int

Count distinct enemy heroes spotted by an observer ward during its lifetime.

Source: src/gem/analysis/vision.py:611

Top-level classes

VisionSource

class VisionSource

One modeled geometry source covering a map point at a given tick.

Source: src/gem/analysis/vision.py:49

Dataclass fields

NameTypeDefault
kindLiteral['hero', 'ward', 'modifier']-
namestr-
distancefloat-
vision_radiusint-
xfloat | NoneNone
yfloat | NoneNone
position_tickint | NoneNone
position_age_ticksint | NoneNone
player_idint | NoneNone
position_provenanceLiteral['sampled_player_position', 'ward_placement'] | NoneNone

PointVisionStatus

class PointVisionStatus(str, Enum)

Modeled support state for an arbitrary map point.

Source: src/gem/analysis/vision.py:86

PointVisionSource

class PointVisionSource

One bounded geometry source supporting point coverage.

Source: src/gem/analysis/vision.py:97

Dataclass fields

NameTypeDefault
kindLiteral['hero', 'observer_ward']-
namestr-
distancefloat-
vision_radiusint-
xfloat-
yfloat-
position_provenanceLiteral['sampled_player_position', 'ward_placement']-
position_tickint | NoneNone
position_age_ticksint | NoneNone
player_idint | NoneNone

Properties

identity

Signature: def PointVisionSource.identity(self) -> str

Return the source name as its stable identity.

Source: src/gem/analysis/vision.py:125

radius

Signature: def PointVisionSource.radius(self) -> int

Return the modeled circular vision radius.

Source: src/gem/analysis/vision.py:130

PointVisionGap

class PointVisionGap

One material omission or ambiguity in a point-vision assessment.

Source: src/gem/analysis/vision.py:136

Dataclass fields

NameTypeDefault
codestr-
subjectstr-

DirectTargetRevealEvidence

class DirectTargetRevealEvidence

Bounded direct-reveal evidence for the requested canonical hero.

Source: src/gem/analysis/vision.py:149

Dataclass fields

NameTypeDefault
modifier_namestr-
target_namestr-
caster_namestr-
caster_teamint-
start_tickint-
end_tickint-
target_player_idint-

Properties

tick

Signature: def DirectTargetRevealEvidence.tick(self) -> int

Return the interval start using modifier-event terminology.

Source: src/gem/analysis/vision.py:174

PointVisionAssessment

class PointVisionAssessment

Evidence-aware modeled coverage assessment for one arbitrary point.

Source: src/gem/analysis/vision.py:180

Dataclass fields

NameTypeDefault
teamint-
tickint-
xfloat-
yfloat-
statusPointVisionStatus-
sourceslist[PointVisionSource]-
gapslist[PointVisionGap]-
direct_target_revealslist[DirectTargetRevealEvidence]-
target_player_idint | None-
authoritative_applicablebool-
authoritative_visibilityVisibilityState-
max_position_age_ticksint-

Module gem.analysis.formatting

Formatting helpers for parsed Dota names.

Source: src/gem/analysis/formatting.py

Top-level functions

format_npc_name

def format_npc_name(name: str) -> str

Convert an NPC name to a human-readable label.

Source: src/gem/analysis/formatting.py:6

Module gem.analysis.farming

Evidence-first neutral-camp route reconstruction.

Source: src/gem/analysis/farming.py

Top-level functions

build_farming_routes

def build_farming_routes(match: ParsedMatch, *, config: FarmingRouteConfig = DEFAULT_FARMING_ROUTE_CONFIG) -> list[FarmingRoute]

Build deterministic camp-local route evidence for every parsed player.

Source: src/gem/analysis/farming.py:633

Top-level classes

FarmingEvidenceStrength

class FarmingEvidenceStrength(str, Enum)

Conservative support level for a camp-local route segment.

Source: src/gem/analysis/farming.py:25

FarmingBoundaryReason

class FarmingBoundaryReason(str, Enum)

Observed reason a route segment started or ended.

Source: src/gem/analysis/farming.py:33

FarmingRouteConfig

class FarmingRouteConfig

Inspectable thresholds for farming-route reconstruction.

Source: src/gem/analysis/farming.py:45

Dataclass fields

NameTypeDefault
max_sample_gap_ticksint10 * _TICKS_PER_SECOND
max_contiguous_speedfloat900.0
merge_gap_ticksint5 * _TICKS_PER_SECOND
min_weak_dwell_ticksint5 * _TICKS_PER_SECOND
resource_max_age_ticksint2 * _TICKS_PER_SECOND

FarmingCampZone

class FarmingCampZone

One calibrated neutral-camp zone from the bundled catalog.

Source: src/gem/analysis/farming.py:71

Dataclass fields

NameTypeDefault
camp_idint-
camp_typestr-
center_xfloat-
center_yfloat-
shapestr-
radius_xfloat-
radius_yfloat-
rotation_degreesfloat-
polygon_pointstuple[tuple[float, float], ...]()
enter_marginfloat0.0
exit_marginfloat0.0
owner_teamint | NoneNone
laneLiteral['top', 'mid', 'bot', 'none', 'unknown']'unknown'
areastr'unknown'

FarmingRoutePoint

class FarmingRoutePoint

One sampled route point and its selected camp membership.

Source: src/gem/analysis/farming.py:91

Dataclass fields

NameTypeDefault
tickint-
xfloat-
yfloat-
camp_idint | None-
camp_typestr | None-
inside_base_zonebool-
boundary_beforeFarmingBoundaryReason | NoneNone

FarmingRouteSegment

class FarmingRouteSegment

One camp-local sampled route segment with factual support evidence.

Source: src/gem/analysis/farming.py:104

Dataclass fields

NameTypeDefault
segment_indexint-
player_idint-
hero_namestr-
teamint-
camp_idint-
camp_typestr-
start_tickint-
end_tickint-
duration_secondsfloat-
start_reasonFarmingBoundaryReason-
end_reasonFarmingBoundaryReason-
sample_countint-
in_zone_sample_countint-
position_coveragefloat | None-
max_sample_gap_ticksint | None-
micro_exit_mergedbool-
neutral_killsint-
neutral_damageint-
window_xp_deltaint | None-
window_total_earned_gold_deltaint | None-
resource_start_sample_tickint | None-
resource_end_sample_tickint | None-
evidence_strengthFarmingEvidenceStrength-
evidence_reasonslist[str]field(...)
evidence_gapslist[str]field(...)
pointslist[FarmingRoutePoint]field(...)
distance_travelledfloat | NoneNone
camp_owner_teamint | NoneNone
camp_lanestr'unknown'
camp_areastr'unknown'
camp_catalog_versionint | NoneNone
camp_map_patchstr | NoneNone
camp_topology_patchstr | NoneNone

FarmingRoute

class FarmingRoute

Evidence-first farming route for one parsed player.

Source: src/gem/analysis/farming.py:143

Dataclass fields

NameTypeDefault
player_idint-
hero_namestr-
teamint-
camp_catalog_versionint | None-
camp_map_patchstr | None-
statusLiteral['complete', 'partial', 'unavailable']-
status_reasonslist[str]field(...)
pointslist[FarmingRoutePoint]field(...)
segmentslist[FarmingRouteSegment]field(...)
camp_topology_patchstr | NoneNone

Module gem.analysis.roshan

Post-parse Roshan records: each kill, its Aegis lifecycle and the window that followed.

Source: src/gem/analysis/roshan.py

Top-level functions

build_rosh_conversions

def build_rosh_conversions(match: ParsedMatch) -> list[RoshConversion]

Summarise each Roshan kill, its Aegis lifecycle and the window that followed.

Source: src/gem/analysis/roshan.py:916

Top-level classes

AegisFateSource

class AegisFateSource(str, Enum)

Evidence or boundary used to classify an Aegis lifecycle.

Source: src/gem/analysis/roshan.py:42

RoshTeamAttributionSource

class RoshTeamAttributionSource(str, Enum)

Provenance of a team attribution used by Roshan analysis.

Source: src/gem/analysis/roshan.py:55

RoshFightRelation

class RoshFightRelation(str, Enum)

Temporal relationship between a fight and the conversion window.

Source: src/gem/analysis/roshan.py:67

RoshFightEvidence

class RoshFightEvidence

Engagement-aware evidence for one fight associated with a Roshan window.

Source: src/gem/analysis/roshan.py:77

Dataclass fields

NameTypeDefault
fight_indexint-
relationRoshFightRelation-
engagement_start_tickint-
engagement_start_sourceEngagementStartSource-
first_death_tickint-
end_tickint-
winnerstr-
deathsint-
conversion_participant_idstuple[int, ...]-
opponent_participant_idstuple[int, ...]-
unknown_participant_idstuple[int, ...]-

RoshTimelineEvent

class RoshTimelineEvent

One notable event inside a Roshan conversion sequence.

Source: src/gem/analysis/roshan.py:128

Dataclass fields

NameTypeDefault
tickint-
kindLiteral['roshan', 'aegis_pickup', 'aegis_denied', 'fight_win', 'fight_loss', 'fight_draw', 'tower', 'tower_lost', 'tower_unknown', 'barracks', 'barracks_lost', 'barracks_unknown', 'buyback', 'own_buyback', 'tormentor', 'opponent_tormentor', 'tormentor_unknown', 'banner', 'opponent_banner', 'aegis_end', 'game_end']-
labelstr-
fight_indexint | NoneNone

RoshDifferentialProfile

class RoshDifferentialProfile

Evidence-first conversion-team profile over one hardened Rosh window.

Source: src/gem/analysis/roshan.py:160

Dataclass fields

NameTypeDefault
conversion_teamint | NoneNone
opponent_teamint | NoneNone
window_start_tickint | NoneNone
window_end_tickint | NoneNone
conversion_fights_wonint | NoneNone
opponent_fights_wonint | NoneNone
fights_drawnint | NoneNone
fight_differentialint | NoneNone
conversion_towersint | NoneNone
opponent_towersint | NoneNone
conversion_barracksint | NoneNone
opponent_barracksint | NoneNone
unattributed_towersint0
unattributed_barracksint0
conversion_structure_valueint | NoneNone
opponent_structure_valueint | NoneNone
structure_deltaint | NoneNone
net_worth_advantage_startint | NoneNone
net_worth_advantage_endint | NoneNone
net_worth_swingint | NoneNone
net_worth_swing_per_minutefloat | NoneNone
xp_advantage_startint | NoneNone
xp_advantage_endint | NoneNone
xp_swingint | NoneNone
xp_swing_per_minutefloat | NoneNone
conversion_forward_wardsint | NoneNone
opponent_forward_wardsint | NoneNone
forward_ward_deltaint | NoneNone
conversion_tormentorsint | NoneNone
opponent_tormentorsint | NoneNone
unattributed_tormentorsint0
tormentor_deltaint | NoneNone
statusLiteral['complete', 'partial', 'unavailable']'unavailable'
status_reasonslist[str]field(...)

RoshConversion

class RoshConversion

Derived summary for one Roshan kill and the advantage window that followed.

Source: src/gem/analysis/roshan.py:242

Dataclass fields

NameTypeDefault
rosh_numberint-
rosh_tickint-
killer_namestr-
holder_teamint | None-
holder_player_idint | None-
holder_namestr-
aegis_pickup_tickint | None-
immediate_end_tickint-
aegis_end_tickint-
aegis_eval_end_tickint-
extended_end_tickint-
aegis_fateLiteral['consumed', 'expired', 'denied', 'game_end', 'unknown']-
first_fight_tickint | None-
first_objective_tickint | None-
fight_countint-
fights_wonint-
fights_lostint-
fights_drawnint-
towers_takenint-
barracks_takenint-
enemy_buybacks_forcedint-
enemy_half_observer_deltaint-
timeline_eventslist[RoshTimelineEvent]field(...)
dropslist[str]field(...)
had_high_value_dropboolFalse
banner_plantedboolFalse
banner_rax_conversionboolFalse
banner_rax_lanestr | NoneNone
roshan_teamint | NoneNone
conversion_teamint | NoneNone
roshan_team_sourceRoshTeamAttributionSourceRoshTeamAttributionSource.UNKNOWN
conversion_team_sourceRoshTeamAttributionSourceRoshTeamAttributionSource.UNKNOWN
aegis_fate_sourceAegisFateSourceAegisFateSource.MISSING_EVENT
aegis_fate_inferredboolFalse
first_engagement_tickint | NoneNone
fight_evidencelist[RoshFightEvidence]field(...)
analysis_statusLiteral['complete', 'partial', 'unavailable']'unavailable'
analysis_status_reasonslist[str]field(...)
differential_profileRoshDifferentialProfilefield(...)

Module gem.analysis.smoke

Evidence-first post-parse Smoke of Deceit lifecycle analysis.

Source: src/gem/analysis/smoke.py

Top-level functions

build_smoke_analysis

def build_smoke_analysis(match: ParsedMatch) -> list[SmokeAnalysis]

Build factual lifecycle summaries for every smoke item use.

Source: src/gem/analysis/smoke.py:149

Top-level classes

SmokeLifecycleStatus

class SmokeLifecycleStatus(str, Enum)

Observed lifecycle classification for one smoke participant.

Source: src/gem/analysis/smoke.py:35

SmokeGroupStatus

class SmokeGroupStatus(str, Enum)

Evidence-based aggregate state for one smoke activation.

Source: src/gem/analysis/smoke.py:52

SmokeMemberAnalysis

class SmokeMemberAnalysis

Evidence summary for one hero in a smoke activation.

Source: src/gem/analysis/smoke.py:72

Dataclass fields

NameTypeDefault
hero_namestr-
player_idint | None-
applied_tickint-
removed_tickint | None-
lifecycle_statusSmokeLifecycleStatus-
visibility_at_applyVisibilityState-
visibility_at_removeVisibilityState-
first_visible_tickint | NoneNone
nearest_enemy_herostr | NoneNone
nearest_enemy_player_idint | NoneNone
nearest_enemy_distancefloat | NoneNone
same_tick_actionslist[CombatLogEntry]field(...)
same_tick_deathslist[CombatLogEntry]field(...)
evidence_gapslist[str]field(...)

SmokeAnalysis

class SmokeAnalysis

Evidence summary for one Smoke of Deceit item use.

Source: src/gem/analysis/smoke.py:115

Dataclass fields

NameTypeDefault
activation_tickint-
activatorstr-
teamint-
statusSmokeGroupStatus-
activation_xfloat | None-
activation_yfloat | None-
member_centroid_xfloat | None-
member_centroid_yfloat | None-
memberslist[SmokeMemberAnalysis]field(...)
first_fightFight | NoneNone
evidence_gapslist[str]field(...)

Module gem.analysis.fight_positioning

Evidence-aware positioning snapshots for detected fights.

Source: src/gem/analysis/fight_positioning.py

Top-level functions

build_fight_positioning

def build_fight_positioning(match: ParsedMatch, *, pre_engagement_ticks: int = 300, max_position_age_ticks: int = 60, nearby_radius: float = 3000.0) -> list[FightPositioning]

Build evidence-aware positioning records for detected fights.

Source: src/gem/analysis/fight_positioning.py:218

Top-level classes

SnapshotKind

class SnapshotKind(str, Enum)

Logical moment represented by a positioning snapshot.

Source: src/gem/analysis/fight_positioning.py:23

EngagementStartSource

class EngagementStartSource(str, Enum)

Provenance for the engagement-start tick.

Source: src/gem/analysis/fight_positioning.py:43

EvidenceCompleteness

class EvidenceCompleteness(str, Enum)

Position-evidence completeness for one team at one snapshot.

Source: src/gem/analysis/fight_positioning.py:62

HeroPositionEvidence

class HeroPositionEvidence

Position and contextual evidence for one canonical player hero.

Source: src/gem/analysis/fight_positioning.py:79

Dataclass fields

NameTypeDefault
player_idint-
hero_namestr-
player_namestr-
teamint-
active_participantbool-
near_fightbool | None-
xfloat | None-
yfloat | None-
sample_tickint | None-
sample_age_ticksint | None-
visibilityVisibilityState-
distance_to_team_centroidfloat | None-
nearest_ally_distancefloat | None-
nearest_enemy_distancefloat | None-
active_smoke_activation_tickint | None-
active_reveal_modifierstuple[str, ...]-
evidence_gapstuple[str, ...]-

TeamPositionSummary

class TeamPositionSummary

Fresh-position geometry and completeness for one team.

Source: src/gem/analysis/fight_positioning.py:127

Dataclass fields

NameTypeDefault
teamint-
expected_countint-
positioned_countint-
unpositioned_countint-
completenessEvidenceCompleteness-
centroid_xfloat | None-
centroid_yfloat | None-
rms_spreadfloat | None-

FightPositionSnapshot

class FightPositionSnapshot

All canonical hero evidence at one logical fight moment.

Source: src/gem/analysis/fight_positioning.py:153

Dataclass fields

NameTypeDefault
kindSnapshotKind-
tickint-
heroestuple[HeroPositionEvidence, ...]-
radiantTeamPositionSummary-
direTeamPositionSummary-
centroid_distancefloat | None-
active_participant_centroid_xfloat | None-
active_participant_centroid_yfloat | None-

FightPositioning

class FightPositioning

Four evidence-aware positioning snapshots for one detected fight.

Source: src/gem/analysis/fight_positioning.py:180

Dataclass fields

NameTypeDefault
fight_indexint-
start_tickint-
engagement_start_tickint-
first_death_tickint-
end_tickint-
engagement_start_sourceEngagementStartSource-
snapshotstuple[FightPositionSnapshot, ...]-

Module gem.analysis.fight_timeline

A fight's combat log as typed records: casts and what they hit, damage, modifiers, deaths.

Source: src/gem/analysis/fight_timeline.py

Top-level functions

modifier_matches_ability

def modifier_matches_ability(modifier: str, ability: str) -> bool

Whether a modifier name looks like it came from an ability or item.

Source: src/gem/analysis/fight_timeline.py:290

build_fight_timeline

def build_fight_timeline(match: ParsedMatch, start_tick: int, end_tick: int, *, hit_window_ticks: int = 90, recap_ticks: int = 300, burst_ticks: int = 15) -> FightTimeline

Turn the combat log between two ticks into a fight's typed records.

Source: src/gem/analysis/fight_timeline.py:314

Top-level classes

CastHit

class CastHit

What one cast did to one hero.

Source: src/gem/analysis/fight_timeline.py:46

Dataclass fields

NameTypeDefault
herostr-
damageint0
damage_typestr''
stun_sfloat0.0
modifierstuple[str, ...]()

TimelineCast

class TimelineCast

One ability or item use by a hero, with the heroes it hit.

Source: src/gem/analysis/fight_timeline.py:67

Dataclass fields

NameTypeDefault
tickint-
casterstr-
abilitystr-
is_itembool-
levelint0
targetstr | NoneNone
target_is_heroboolFalse
hitstuple[CastHit, ...]()
self_effectCastHit | NoneNone

Properties

damage

Signature: def TimelineCast.damage(self) -> int

Total damage the cast dealt to other heroes.

Source: src/gem/analysis/fight_timeline.py:99

DamageBurst

class DamageBurst

Damage from one unit to one hero, with one source and type, in a short burst.

Source: src/gem/analysis/fight_timeline.py:105

Dataclass fields

NameTypeDefault
start_tickint-
end_tickint-
attackerstr-
attacker_is_illusionbool-
attacker_herostr | None-
targetstr-
sourcestr-
damage_typestr-
damageint-
hitsint-

ModifierWindow

class ModifierWindow

A modifier on a hero, from MODIFIER_ADD to its MODIFIER_REMOVE.

Source: src/gem/analysis/fight_timeline.py:137

Dataclass fields

NameTypeDefault
targetstr-
modifierstr-
sourcestr-
source_herostr | None-
start_tickint | None-
end_tickint | None-
duration_sfloat | NoneNone
stun_sfloat0.0
aurabool | NoneNone

DamageTaken

class DamageTaken

Damage a hero took from one unit, source and type before dying.

Source: src/gem/analysis/fight_timeline.py:168

Dataclass fields

NameTypeDefault
attackerstr-
attacker_is_illusionbool-
attacker_herostr | None-
sourcestr-
damage_typestr-
damageint-

TimelineDeath

class TimelineDeath

A hero death.

Source: src/gem/analysis/fight_timeline.py:189

Dataclass fields

NameTypeDefault
tickint-
victimstr-
killerstr-
killer_herostr | None-
reincarnatedbool-
gold_lostint0
recent_damagetuple[DamageTaken, ...]()

KillRewards

class KillRewards

The kill gold and XP paid on a tick with hero deaths.

Source: src/gem/analysis/fight_timeline.py:215

Dataclass fields

NameTypeDefault
tickint-
victimstuple[str, ...]-
golddict[str, int]field(...)
xpdict[str, int]field(...)

TimelineBuyback

class TimelineBuyback

A buyback.

Source: src/gem/analysis/fight_timeline.py:235

Dataclass fields

NameTypeDefault
tickint-
herostr-
costint-
cost_exactbool-

FightTimeline

class FightTimeline

A window's combat log as typed records, each list in tick order.

Source: src/gem/analysis/fight_timeline.py:253

Dataclass fields

NameTypeDefault
start_tickint-
end_tickint-
player_idsdict[str, int]-
caststuple[TimelineCast, ...]()
damagetuple[DamageBurst, ...]()
modifierstuple[ModifierWindow, ...]()
deathstuple[TimelineDeath, ...]()
rewardstuple[KillRewards, ...]()
buybackstuple[TimelineBuyback, ...]()

Properties

disables

Signature: def FightTimeline.disables(self) -> tuple[ModifierWindow, ...]

The modifiers that stunned (stun_s > 0).

Source: src/gem/analysis/fight_timeline.py:281

Methods

rewards_at

Signature: def FightTimeline.rewards_at(self, tick: int) -> KillRewards | None

The kill rewards paid on tick, or None.

Source: src/gem/analysis/fight_timeline.py:285

Module gem.analysis.bundle

One-call bundle of gem's evidence-first post-parse analyses.

Source: src/gem/analysis/bundle.py

Top-level functions

analyze

def analyze(match: ParsedMatch) -> MatchAnalysis

Run every default post-parse analysis on a parsed match.

Source: src/gem/analysis/bundle.py:51

Top-level classes

MatchAnalysis

class MatchAnalysis

Results of every default post-parse analysis for one match.

Source: src/gem/analysis/bundle.py:31

Dataclass fields

NameTypeDefault
smokelist[SmokeAnalysis]field(...)
roshan_conversionslist[RoshConversion]field(...)
farming_routeslist[FarmingRoute]field(...)
fight_positioninglist[FightPositioning]field(...)