Published 26 June 2026 pythondata-analysisgaming

Reverse-engineering jungle pathing from Riot's timeline API

The Riot API gives player positions every 60 seconds. To reconstruct a jungler's actual path, you need interpolation that respects map geometry — and A* pathfinding on the game's navgrid.

League of Legends junglers make hundreds of micro-decisions per game: which camp to start, which direction to clear, when to break the route for a gank. The difference between a Challenger and a Diamond jungler often comes down to tempo — how efficiently they convert camps into gold and experience relative to the game clock.

I wanted to measure that. The Riot Games API provides match timeline data, and in theory you can reconstruct a jungler’s path from it. In practice, there is a problem.

The 60-second gap

The timeline API returns participant positions in participantFrames — one snapshot every 60 seconds. Between those snapshots, you get positions only when events fire: a kill, a ward placement, an objective take. For a jungler farming camps alone in the early game, that can mean a single data point per minute.

Sixty seconds is an eternity on Summoner’s Rift. A jungler clears two or three camps in that window. If you want to know which camps were cleared and when, you need to fill the gaps.

Naive interpolation walks through walls

The obvious approach is linear interpolation: draw a straight line between two known positions and sample points along it at regular intervals. This works for laners moving along predictable paths, but it falls apart in the jungle.

The jungle is a maze of walls, camp pits, and narrow corridors. A straight line from Gromp to Wolves cuts directly through the rock wall separating them. The actual walking path curves around it, adding roughly 20% to the distance. That extra distance matters — it affects time estimates, and wrong time estimates mean wrong tempo calculations.

Position interpolation from Riot's 60-second timeline snapshots.

Show code
In [1]:
import numpy as np

known_positions = [
    {"t": 90_000,  "x": 3850, "y": 8000},   # Blue buff (1:30)
    {"t": 150_000, "x": 2100, "y": 8400},   # Gromp    (2:30)
    {"t": 210_000, "x": 3750, "y": 6400},   # Wolves   (3:30)
]

def linear_interpolate(p1, p2, target_t):
    t = (target_t - p1["t"]) / (p2["t"] - p1["t"])
    return {
        "t": target_t,
        "x": p1["x"] + t * (p2["x"] - p1["x"]),
        "y": p1["y"] + t * (p2["y"] - p1["y"]),
    }

interpolated = []
for i in range(len(known_positions) - 1):
    p1, p2 = known_positions[i], known_positions[i + 1]
    for t in range(p1["t"], p2["t"], 10_000):
        interpolated.append(linear_interpolate(p1, p2, t))
interpolated.append(known_positions[-1])

print(f"{len(known_positions)} known positions -> {len(interpolated)} interpolated points")
for p in interpolated:
    mins, secs = divmod(p['t'] // 1000, 60)
    print(f"  {mins}:{secs:02d}  ({p['x']:.0f}, {p['y']:.0f})")
3 known positions -> 13 interpolated points
  1:30  (3850, 8000)
  1:40  (3558, 8067)
  1:50  (3267, 8133)
  2:00  (2975, 8200)
  2:10  (2683, 8267)
  2:20  (2392, 8333)
  2:30  (2100, 8400)
  2:40  (2375, 8067)
  2:50  (2650, 7733)
  3:00  (2925, 7400)
  3:10  (3200, 7067)
  3:20  (3475, 6733)
  3:30  (3750, 6400)

The path from Gromp to Wolves cuts straight through the jungle wall between them. A* pathfinding on the navgrid routes around it.

Show code
In [2]:
from analytics.pathfinding import NavGrid, AStarPathfinder

navgrid = NavGrid()
pathfinder = AStarPathfinder(navgrid)

start = navgrid.game_to_grid(2100, 8400)  # Gromp
end   = navgrid.game_to_grid(3750, 6400)  # Wolves

path = pathfinder.find_path(start, end)
linear_dist = np.sqrt((3750 - 2100)**2 + (6400 - 8400)**2)
path_dist = sum(
    np.sqrt((path[i+1].x - path[i].x)**2 + (path[i+1].y - path[i].y)**2)
    for i in range(len(path) - 1)
) * 50  # grid cells -> game units

print(f"Linear distance:     {linear_dist:.0f} game units")
print(f"A* path distance:    {path_dist:.0f} game units ({len(path)} waypoints)")
print(f"Detour factor:       {path_dist / linear_dist:.2f}x")
Linear distance:     2561 game units
A* path distance:    3074 game units (52 waypoints)
Detour factor:       1.20x

Using the interpolated positions and clear event timestamps, we can compare actual clear speed to par time.

Show code
In [3]:
import json

with open("data/par_times.json") as f:
    par_times = {k: v for k, v in json.load(f).items()
                 if isinstance(v, (int, float))}

clear_path = "Blue-Gromp-Wolves-Raptors-Red-Krugs"
actual_time = 272  # seconds, from a real Viego game
par = par_times[clear_path]

print(f"Path:        {clear_path}")
print(f"Par time:    {par // 60}:{par % 60:02d} ({par}s)")
print(f"Actual time: {actual_time // 60}:{actual_time % 60:02d} ({actual_time}s)")
print(f"Tempo loss:  +{actual_time - par}s")
Path:        Blue-Gromp-Wolves-Raptors-Red-Krugs
Par time:    4:20 (260s)
Actual time: 4:32 (272s)
Tempo loss:  +12s

A* on the navgrid

The solution is pathfinding. Summoner’s Rift has a navigation grid — a 295 by 296 cell grid where each cell represents a 50-unit square and is marked either walkable or wall. The project implements A* search on this grid with octile distance as the heuristic (allowing 8-directional movement, with diagonal steps costing the square root of two).

The algorithm:

  1. Convert game coordinates to grid cells
  2. Run A* from the start cell to the goal cell, respecting walls
  3. Convert the grid path back to game coordinates
  4. Distribute interpolated positions along the path at the requested time interval

Diagonal movement is only allowed when both adjacent cardinal cells are walkable, which prevents the path from cutting corners through walls. The pathfinder also snaps start and end points to the nearest walkable cell if they land inside a wall (which happens occasionally with imprecise API coordinates).

Paths are cached by grid-cell pair, so repeated queries between the same two camps are instant after the first computation.

Jungle tempo

With accurate position reconstruction, the next step is measuring tempo. The system detects camp clears by correlating a player’s interpolated position with known camp locations and gold-change events from the timeline. Each clear gets a timestamp, and the sequence of clears forms a clear path — for example, “Blue-Gromp-Wolves-Raptors-Red-Krugs” for a standard blue-side full clear.

The project maintains par times for common clear paths: the time a competent jungler should finish each route, accounting for leash quality and champion clear speed. These are calibrated from high-elo VOD reviews and practice tool runs.

Par times as percentage of full clear (260s = 100%)

Comparing a player’s actual clear time to par reveals tempo loss — seconds wasted to mechanical errors, suboptimal pathing, or getting collapsed on. A jungler finishing their full clear at 4:32 against a 4:20 par has lost 12 seconds of tempo. At the highest level, that is the difference between arriving at scuttle crab first or second, between making a gank window or missing it.

What is next

This is part of a larger personal project analysing high-elo jungle play. The position interpolation and tempo system are working; the next steps are aggregating tempo data across many games to build champion-specific par time baselines, and correlating early tempo with win rate to quantify how much those lost seconds actually cost.

The pathfinding approach generalises beyond jungle analysis — any question about player movement on the Rift (roam timing, rotation speed, vision control patterns) benefits from wall-aware position reconstruction rather than naive straight-line interpolation.