Farming Patterns
Farming Patterns is an experimental interpretation layer that sits on top of parsed replay data and tries to answer a practical analyst question:
Where is a hero choosing to farm, and what does that route say about map safety, pressure, and objective state?
This feature currently appears in the HTML match report as the Farming tab.
IMPORTANT
This is not a ground-truth "which neutral camp was definitely cleared" feature.
It is a route-and-context feature. It is designed to surface farming intent and map usage patterns, with explicit support signals and explicit caveats.
Why this feature exists
A raw hero trail is not enough.
If you only plot position samples on the map, you can see where a hero moved, but you still cannot answer the questions analysts actually ask:
- Was this normal home-side farming or a forced contraction into safe camps?
- Was the carry taking enemy space or just crossing the river briefly?
- Did tower losses, ward coverage, or Aegis pressure change where the hero felt allowed to farm?
- Are we looking at a comfortable route, a cautious route, or a punishable invade?
The replay does not give a single built-in field for any of those interpretations. The feature has to derive them by combining multiple lower-level signals.
What the replay gives us directly
The feature is built from facts gem can already parse:
- hero position samples (
player.position_log) - camp metadata and calibrated camp zones
- tower death state
- Aegis pickup / deny events
- Roshan and Tormentor events
- observer ward activity
- net-worth and XP advantage snapshots
- enemy hero movement density by region
- neutral kills, neutral damage, and XP gained during visit windows
Those are real replay-derived facts. The Farming Patterns feature begins once gem starts combining them.
What the replay does not give us directly
The replay does not directly tell us:
- true per-player fog-of-war state at every moment
- real map control ownership as a clean scalar
- whether a hero's exact intention was "farm", "cut wave", "scout", or "move through"
- a built-in label like
safe_home_farmorhigh_risk_invade
So the feature is intentionally framed as a heuristic layer, not replay truth.
NOTE
The right way to evaluate this feature is not "is every label perfect?"
The right question is: "does this produce a defensible, readable first-pass explanation of the hero's farming route?"
Design goals
The current implementation is trying to optimize for these goals:
- Show farming routes, not just isolated camp dots.
- Keep the output readable for humans, especially in the HTML report.
- Use objective-aware context instead of treating every camp as equally safe.
- Avoid pretending we know more than the replay actually tells us.
- Make the heuristics inspectable: formulas, thresholds, and drivers are all visible.
High-level pipeline
The feature works in five stages.
1. Build camp-local geometry
Each neutral camp has calibrated map geometry in camp_zones.json.
Instead of saying "nearest camp center wins", gem asks whether a hero sample falls inside the zone geometry for a specific camp. That reduces context blindness and makes nearby camps easier to distinguish.
2. Build route segments from hero movement
Hero movement is read from player.position_log.
A route segment is created when a hero spends time moving through or around a camp zone. The feature no longer tries to draw a strict binary line between "actual farm" and "transit" because, for route analysis, both often express the same strategic intent: this is space the hero is using.
Support signals are still recorded when available:
- neutral kills
- neutral damage
- XP gained during the segment window
- in-zone sample count
These support signals increase confidence, but they are not strict requirements.
3. Build objective-aware map context buckets
The feature constructs coarse 30-second map-context buckets for each team perspective.
Each bucket includes:
- towers alive on both teams
- whether mid T1 is still alive
- current Aegis state
- last Roshan / Tormentor timing
- active observer ward counts
- team net-worth advantage
- team XP advantage
- enemy movement density in three coarse regions:
- own half
- enemy half
- river / border strip
This is the layer that tries to answer: "what did the map probably feel like around this time?"
4. Score each visit against the active bucket
For every route segment, gem computes three scores:
farm_safety_scorepressure_scoreexpected_value_score
These are not learned model outputs. They are explicit formulas in code.
5. Assign an analyst-facing label and drivers
The final label is chosen from a small readable set:
Safe Home FarmCautious Home FarmForced Home FarmSafe InvadeContested InvadeHigh-Risk Invade
The label is accompanied by drivers such as:
lost_t1_midenemy_aegis_activeenemy_presence_high_own_halfvision_deficitmap_control_deficitborder_zone_farminvading_enemy_halfhigh_farm_value
That combination is what makes the feature inspectable instead of opaque.
Exact formulas
The current formulas come directly from src/gem/map_context.py.
clamp
clamp(x) means: force the result into the range [0.0, 1.0].
Examples:
clamp(1.18) = 1.0clamp(-0.12) = 0.0clamp(0.44) = 0.44
Conceptually:
clamp(x) = max(0.0, min(1.0, x))Safety
Safety = clamp(
0.55
+ 0.25 * tower_diff
+ 0.20 * ward_diff
- 0.45 * enemy_own_half
- 0.20 * enemy_aegis
- 0.15 * invading
- 0.08 * border_zone
)Interpretation:
- starts from a neutral baseline of
0.55 - goes up if your team owns more towers or has more observer coverage
- goes down if enemy presence on your side is high
- goes down when the enemy has Aegis
- goes down when you are farming enemy territory
- goes down slightly for border-zone camps near the map diagonal
Pressure
Pressure = clamp(
0.30
+ 0.40 * enemy_own_half
+ 0.20 * enemy_river
+ 0.20 * max(0, -tower_diff)
+ 0.20 * enemy_aegis
+ invade_bonus
+ 0.08 * border_zone
)Interpretation:
- starts from a lower baseline of
0.30 - rises when recent enemy movement density is high
- rises when your team is down towers
- rises when the enemy has Aegis
- rises more if you are farming enemy territory
- rises slightly for camps near the border strip
Invade bonus
Invade Bonus = 0.15 + 0.15 * enemy_enemy_half when invading
Invade Bonus = 0 otherwiseInterpretation:
Invading already makes the route riskier. It becomes even riskier if recent enemy presence is also high on their own side.
Value
Value = clamp(0.5 * camp_value + 0.5 * evidence)Interpretation:
The route is considered more valuable when:
- the camp itself is economically valuable
- the visit window also contains stronger support signals
Camp value by camp type
small -> 0.35
medium -> 0.45
large -> 0.55
ancient -> 0.70
flooded_small -> 0.40
flooded_medium -> 0.50
fallback -> 0.45Interpretation:
This is a deliberately coarse economic prior. Ancients should push the score upward more than a small camp even before we look at supporting evidence.
Evidence
evidence is a support score for meaningful interaction during the route segment.
It is built from three capped components:
neutral_kill_component = min(neutral_kills / 4.0, 1.0) * 0.35
neutral_damage_component = min(neutral_damage / 3000.0, 1.0) * 0.30
xp_component = min(xp_gain / 600.0, 1.0) * 0.35
evidence = neutral_kill_component + neutral_damage_component + xp_componentInterpretation:
- more neutral kills support the idea that this was a meaningful farming segment
- more neutral damage supports the same
- XP gained also supports that interpretation
TIP
evidence is not proof of a full neutral clear.
It is a support score used to make route segments more informative. The feature is intentionally route-first, evidence-second.
Derived terms
tower_diff
tower_diff = (own_towers - enemy_towers) / 11Why divide by 11?
Because each team has 11 lane towers on a standard Dota map:
- top lane: T1, T2, T3, T4A, T4B = 5
- mid lane: T1, T2, T3 = 3
- bottom lane: T1, T2, T3 = 3
Total = 11
This normalization keeps tower advantage on a compact scale before it is mixed into the safety and pressure formulas.
ward_diff
ward_diff = (own_observers - enemy_observers) / 6Interpretation:
This is a coarse observer-coverage differential. Positive means your team currently has more active observer wards than the enemy.
winning
winning = net_worth_advantage >= 3500 or xp_advantage >= 4500Interpretation:
This is a shortcut for "clearly ahead enough that own-side farming should not automatically be treated as forced".
losing
losing = net_worth_advantage <= -3500 or xp_advantage <= -4500Interpretation:
This is a shortcut for "clearly behind enough that the same route may deserve a more defensive reading".
structural_deficit
structural_deficit = tower_diff < -0.25 or (lost mid T1 and ward_diff < -0.20)Interpretation:
This tries to capture the moment where own-side farm stops looking like ordinary routing and starts looking structurally constrained.
border_zone
border_zone = camp center falls in the diagonal strip abs(x - y) <= 1200Interpretation:
These camps sit near the central map boundary. They are treated as slightly less safe and slightly more pressured, but they do not receive a dedicated public label.
Category rules
These are the current public labels.
Safe Home Farm
Meaning:
Own-side farm with good cover and relatively low contest risk.
Rule:
home side and safety >= 0.68 and pressure <= 0.40 and not losingCautious Home Farm
Meaning:
Own-side farm with contest risk, but not enough stress to say the map is clearly forcing the hero inward.
Rule:
fallback non-invade label when the visit is not safe and not forcedForced Home Farm
Meaning:
Own-side farm that looks reactive or constrained by map state.
Rule:
(losing and pressure >= 0.52)
or (pressure >= 0.70 and not winning)
or (enemy Aegis and pressure >= 0.55 and not winning)
or (structural_deficit and pressure >= 0.45 and not winning)Safe Invade
Meaning:
Enemy-side farm taken while sufficiently ahead or stable to own that area.
Rule:
invading
and safety >= 0.52
and pressure <= 0.48
and tower_diff >= -0.05
and ward_diff >= -0.10
and no enemy Aegis
and winningContested Invade
Meaning:
Enemy-side farm with some risk, but not yet the most punishable invade state.
Rule:
fallback invade label when the visit is not safe invade and not high-risk invadeHigh-Risk Invade
Meaning:
Enemy-side farm that looks especially punishable.
Rule:
invading and (
pressure >= 0.70
or (enemy Aegis and (enemy presence in enemy half >= 0.35 or losing))
)Driver definitions
Drivers explain why a label was assigned.
lost_t1_mid
Trigger:
own mid T1 is deadMeaning:
Your side of the map is more open through the central entrance than in a stable early-game state.
enemy_aegis_active
Trigger:
Aegis is active and the holder team is the enemy teamMeaning:
This is short-lived but important objective pressure. It widens the window where aggressive enemy map occupation is plausible.
enemy_presence_high_own_half
Trigger:
enemy_own_half >= 0.45Meaning:
Recent enemy movement density on your side of the map is high enough to materially affect route interpretation.
enemy_presence_high_river
Trigger:
enemy_river >= 0.45Meaning:
Recent enemy movement density is high near the central diagonal / border region.
vision_deficit
Trigger:
ward_diff < -0.15Meaning:
The enemy currently has better observer coverage than your team.
map_control_deficit
Trigger:
tower_diff < -0.15Meaning:
Your team has lost enough towers that your side of the map is materially less secure.
border_zone_farm
Trigger:
camp center falls in the diagonal border stripMeaning:
The route is using a camp in an area where ownership is naturally less stable, even if we no longer expose this as its own category.
invading_enemy_half
Trigger:
camp is on the enemy half of the mapMeaning:
The route is taking enemy-side space.
high_farm_value
Trigger:
value >= 0.70Meaning:
The segment is either inherently valuable because of camp type, strongly supported by neutral / XP evidence, or both.
How enemy presence is measured
Enemy presence is one of the most important parts of the model, so it is worth calling out explicitly.
gem looks at enemy hero position samples inside a recent time window and buckets them into three coarse regions:
riverradiant_halfdire_half
Recent samples are weighted more heavily than older samples using exponential decay.
This is not a real influence map. It is a compact, practical pressure proxy.
Current bucket settings
- context bucket width:
900ticks = 30 seconds - enemy-presence window:
2700ticks = 90 seconds - exponential decay tau:
900ticks = about 30 seconds
Interpretation:
- the feature wants pressure to feel recent, not permanent
- but it also should not disappear instantly after one enemy pathing sample
Why the feature is route-first, not clear-first
A common temptation is to demand a strict distinction between:
actual camp farmtransit
That sounds cleaner than it really is.
In practice, for analyst-facing route interpretation, the more useful question is often:
Was this space part of the hero's farming pattern?
A carry can pass through a camp area very quickly, partially hit it, stack it, posture near it, or clip it while moving to the next camp. Those behaviors still matter for understanding map usage.
That is why the feature now treats camp-path segments as valid route segments and shows support signals instead of pretending every segment is a confirmed full neutral clear.
Why there is no separate border-camp label
Earlier versions exposed a dedicated border / river-edge category.
That turned out to be too noisy and not very intuitive. The better approach is:
- keep border-zone geography in the scoring model
- expose it as a driver (
border_zone_farm) - let the public label still resolve to home-side or invade-oriented language
That gives the user the right contextual hint without bloating the label system.
Why this feature is useful despite being heuristic
The feature is still valuable because it helps answer questions that are otherwise awkward to reconstruct manually:
- Is the carry repeatedly retreating into ancients and triangle camps?
- Are they looping own-side camps or stepping across the map boundary?
- Does a Roshan / Aegis window coincide with safer or more constrained routes?
- Is a winning core taking enemy space, or only threatening to do so?
The labels are not perfect truth. They are compact analyst summaries of route posture.
Known limitations
IMPORTANT
The current feature should be treated as a strong first-pass visualization, not a final tactical judge.
Current limits include:
- no true per-player fog-of-war reconstruction
- no terrain-aware line-of-sight model
- no exact camp-clear confirmation model
- threshold tuning is still match-dependent
- border-region and pressure heuristics can still overstate danger in some games
- short camp touches can introduce noise
- long routes can visually clump without careful playback use
How to read the report correctly
The HTML report is easiest to interpret if you read it in this order:
- scrub or play the route on the map
- check the context label for the active segment
- inspect the support signals
- read the drivers
- use the label as a summary, not as an unquestionable verdict
This is especially important for borderline cases like:
- short cross-river clips
- ancient-triangle loops during tense midgame windows
- losing teams that still briefly step into enemy space
- winning teams farming own side for efficiency rather than necessity
Future tuning directions
Likely next improvements include:
- better smoothing and time-window fading for long routes
- region-aware thresholds by camp family instead of one global set
- more stable enemy-presence modeling
- optional higher-level grouping of micro-visits into larger farming stints
- better tie-in with tower-state changes and Roshan windows
- richer report interactions and explanations
Code locations
If you want to inspect or tune the implementation directly:
src/gem/map_context.pyexamples/report/html_sections.pyexamples/report/style.pysrc/gem/data/neutral_camps.jsonsrc/gem/data/camp_zones.json