Rift Logic Reference
Rift Logic Reference
Open the editor

Rift Logic Reference

Overview

Rift Logic is a small C++-style language for making claims about League of Legends that can be checked against the game's own data. It has three kinds of answer:

  • Calculations: damage, healing, stats, cooldowns, projectile travel times, from the formulas the game ships with.
  • Simulations: timed fights between champions or team comps, with runes, item passives, heals and shields.
  • Arguments: rules that argue for or against a claim like beats(Fizz, Syndra), where the strongest argument wins.

Every result shows its working and lists every assumption the model made. A result is only as good as those assumptions, which is why they're always visible.

Running programs

In the editor, results update as you type (Ctrl/⌘ + Enter re-runs). Click a row in the results to see its working, assumptions and tables. The Inspect tab shows any champion's or item's data, including every named formula you can reach.

From the command line, the same engine runs a file:

node tools/rl.js program.rl           # full working for every result
node tools/rl.js program.rl --brief   # one line per result
echo 'print(Syndra(11).Q.damage);' | node tools/rl.js -

The exit code is 0 when every assert and prove held, and 1 when anything was refuted or errored, so programs can be used as checks.

Imports (command line only): import "../lib.rl"; at the top of a file pulls in another program's functions, rules, variables and data corrections. Paths are relative to the importing file, each file is imported once, and the imported file's own assert/print/prove output is not shown. The reasoning workspace (reasoning/) uses this to share scenario comps and rules between questions.

A first program

ItemSet burst = {Ludens, Sorc};
Champion a = Syndra(11).with(burst, Electrocute);
Champion tank = Garen(11).with(Plated, Kaenic);

assert(a.Q.damage(vs: tank) > 150);          // Proved or Refuted, with the formula
print(a.ap, a.Q.cd, a.combo(Q, E, R, vs: tank));
assert(canKill(a, tank, 5), "Syndra kills Garen in 5 seconds");

Syntax

C++ you'd sketch on a whiteboard. Statements end with ;. Comments are // … and /* … */. Strings use double quotes. There are no pointers, headers, classes or templates.

FormMeaning
Champion a = Syndra(11);Declare with a type. Several at once: Champion a = …, b = …;
auto x = a.ap * 2;auto works anywhere a type does.
Champion a(Syndra, 11);Constructor-style declaration.
ItemSet s = {Ludens, Sorc};Brace lists become ItemSets, TeamComps or lists, depending on the type (or contents, with auto).
+ - * / %Arithmetic. All numbers are decimal: 2 / 5 is 0.4. int truncates only when stored.
30%A percent literal: 0.3.
== != < <= > >=Comparisons. Champions compare equal when same champion, level, items, runes and ranks.
&& || !Logic, short-circuiting.
= += -= *= /= ++ --Assignment. Objects are copied on assignment and when passed to functions.
"text" + xString concatenation with any value.
f(x, vs: t)Named arguments follow the positional ones.

Types and values

TypeWhat it holds
int double floatNumbers.
bool stringtrue/false; text.
ChampionA champion at a level, with items, runes, ability ranks and fight settings.
AbilityOne of a champion's abilities: a.Q. Keeps a reference to its champion.
Item ItemSetOne shop item; a set of items.
RuneA keystone, minor rune or stat shard.
SummonerA summoner spell: Flash, Ghost, Heal, Barrier, Ignite, Exhaust, Cleanse (Summoner spells).
TeamCompA list of champions.
FightThe result of fight(…).
DummyThe practice-tool target dummy, Dummy(hp: 3000, resists: 50). It is also a Champion, so it goes anywhere a target does (Practice-tool dummy).
autoWhatever the value is.

Assignment and parameter passing copy champions, sets and comps. To change the original inside a loop or function, use a reference: for (Champion& c : team) c.target = x;.

Names: champions, items, runes

A capitalised name that isn't a variable is looked up in this order: classes (Mage), summoner spells (Flash, Ignite, …), champions, exact rune names, items, then rune name prefixes. Lower-case names are variables or ability tags (stun, dash).

  • Champions are written without spaces or punctuation: KSante, MissFortune, Wukong. Syndra alone is Syndra at defaultLevel with no items; Syndra(11) sets the level.
  • Items accept any unique start of the name: Ludens, Rabadon, Sorc, Plated. An ambiguous prefix is an error that lists the matches.
  • Runes by name, without spaces: Electrocute, SuddenImpact, AdaptiveForce, HealthScaling.
  • Ability slots P Q W E R are used in combo(Q, E, R).
  • Globals: defaultLevel (11) and gameMinute (20) can be assigned.

Functions and control flow

double burstOn(Champion c, Champion t) {
    return c.combo(Q, E, vs: t) + c.proc(Ludens, vs: t);
}
bool oneShots(Champion c, Champion t) { return burstOn(c, t) >= t.hp; }

for (Champion c : blue) if (c.is(Mage)) mages++;
for (int i = 0; i < 5; i++) { … }
while (x < 10) { … }             // break and continue work as usual

Functions can be declared anywhere at the top level and called before their declaration. Parameters need types; a function with a return type other than void must return. Programs are stopped after about three million steps, which catches endless loops.

assert, print, prove

StatementResult
assert(a.Q.damage > b.Q.damage);Proved or Refuted. When the condition is a comparison, both sides are shown with their working and the percentage difference.
assert(cond, "title");The same, with your own title.
print(a, b, "text");Each value with how it was calculated.
prove beats(Fizz, Syndra);Runs the rules for a claim: Proved, Refuted, Contested or No rule applies, with the argument tree.
prove forall (Champion c : list) beats(c, x);The same claim for every champion in a list, grouped by verdict.
prove expr;With a non-claim expression, behaves like assert.

Every result lists its assumptions: ranks chosen for you, facts you asserted, item passives not modelled, simulator simplifications.

Champion

Making and changing

Syndra(11)A champion at level 11 (1–18).
a.with(Ludens, burst, Electrocute)A copy with more items, item sets or runes.
a.at(16)A copy at another level.
a.items.add(x) / a.items.remove(x)Change items in place.
a.runes.add(Conqueror) / a.runes.remove(x)Change runes in place.
a.level = 13; a.Q.rank = 3;Settable. Unset ranks are the highest possible at the level (basic abilities ⌈level/2⌉ up to 5; ultimate at 6, 11, 16).

Stats

ap ad bonusad baseadAbility power, total, bonus and base attack damage.
hp bonushp basehp manaHealth and mana.
armor bonusarmor basearmor mr bonusmr basemrResistances.
ehpphysical ehpmagicEffective health: hp × (1 + resist ÷ 100).
haste ms bonusms as bonusas crit critdmg rangeAbility haste, move speed, attack speed, crit chance and damage, attack range.
lethality armorpen armorpenpct magicpen magicpenpctPenetration.
lifesteal omnivamp tenacity healpower healIn shieldInSustain; outgoing heal & shield power; extra healing and shielding received.
aa dps gold level nameOne auto attack; auto DPS with expected crit; item cost; level; name.
attack defense magic difficulty melee hp6 hp11 hp18 …Riot's 1–10 ratings; melee (bool); stats at fixed levels regardless of the champion's level.

Methods

a.combo(Q, E, R, vs: t)Each listed ability's damage once; after t's resistances with vs. Abilities without a damage formula count 0 (noted).
a.proc(Ludens, vs: t)An item's main damage formula with a's stats, in its own damage type (Luden's at all six echoes, Terminus on-hit, Kraken's third hit before the missing-health bonus…).
a.stacks = 225; a.stacksThe champion's own permanent stacks: Syndra's Splinters of Wrath (default: 5 per skill point after level 1, up to 85, as leveling in the practice tool gives; 40 = two Dark Sphere charges, 60 = Force of Will true damage, 100 = Unleashed Power execute, 120 = +15% AP), Smolder's Dragon Practice (default 0; tier 1/2/3 at 25/125/225, bonus magic damage on Q, W and E). Reading it gives the count in use.
a.evolved = {Q, E}; a.evolvedEvolved or augmented abilities: Kai'Sa's Second Skin (default from her stats: Q at 100 AD from items and growth, W at 100 AP, E at 100% attack speed from items and growth), Viktor's Hex Core augments (default none: they cost Hex Fragments from kills). Reading it gives the abilities in use, e.g. "QE".
a.stacks(Heartsteel, 400)A copy of a with stacks earned before the fight: Heartsteel health, Mejai's / Dark Seal Glory, Hubris, Yun Tal, Rod of Ages, Gluttonous Greaves / Immortal Path, Manaflow mana. Runes too: a.stacks(LegendAlacrity, 10), Legend: Haste / Bloodline, Ultimate Hunter, Overgrowth, DarkHarvest (souls), ManaflowBand (bonus mana). Without it an item counts as freshly bought and a rune as a fresh game (0 stacks), as in the practice tool; the assumptions say which.
a.is(Mage) a.has(stun)Class and ability-tag checks.
a.cdOf(tag) a.cdmaxOf(tag) a.rangeOf(tag)Rank-1 cooldown, max-rank cooldown or range of whichever ability has the tag (shortest cooldown, longest range).
a.threatRangeLongest reach of any damaging ability or attack.
a.gapClose a.closeGap(d)Total dash and blink distance; seconds to cover d units using dashes then walking.
a.hitboxThe champion's gameplay radius, used in dodge checks: the game files' character record (overrideGameplayCollisionRadius), else 65. Most champions 65; large ones 80 (Cho'Gath, Malphite, Sion, …), small ones 55 (Teemo, Fizz, Lulu, …), Ivern 70, Warwick 74.75 (65 + his innate 15% size). Size effects (Cho'Gath's Feast, Nasus R, Zac's health, …) aren't simulated; the assumptions mention them.
a.summoners a.Flash a.summonerHasteThe champion's summoner spells (a list), one of them bound to the champion (so its numbers use the champion's level and summoner haste), and summoner spell haste from items and Cosmic Insight. See Summoner spells.

Fight settings

a.target = b;Focus b in fights. Otherwise attacks go to the lowest-health enemy.
a.healPolicy = "save";Who a heals and shields: "lowest" (default), "save" (whoever dies soonest at the current damage rate), "self", or a Champion.
a.passive = true;Only heals and shields; never attacks.
a.rotation = "QEWR";Ability priority (default R, Q, E, W).
a.souls = 10;Dark Harvest souls.
a.role = "peel";How a fights (see Crowd control and positions): "kite" (default for ranged champions), "dive" (default for melee), "peel", "engage" or "fight".
a.tenacity a.slowresistTenacity and slow resist from items, elixirs and the rune shard, stacking multiplicatively: Mercury's Treads + Sterak's Gage = 1 − 0.7 × 0.8 = 44%.

Ability

a.Q.damage a.Q.damage(vs: t)The tooltip's damage. Values the tooltip joins with “plus” are summed, each reduced by its own damage type. Percent-of-health formulas use the target's max health with vs. When the tooltip deals one formula twice as different damage types, both passes count (Ahri Q: magic out, true back; Yone W and R: physical and magic).
a.Q.damage(passes: 1)Only the first pass of those abilities (Ahri Q's orb misses on the way back).
a.R.damage(spheres: 5)For abilities whose damage is per unit with a guaranteed minimum count, .damage is that minimum (Syndra R: 3 spheres); name the count to use more (Syndra R: 3–7 spheres; count: also works).
a.Q.damage(snips: 3) a.R.damage(needles: 5)Champion kits (champion audit): hits, recasts and empowered parts the tooltip gives in words. .damage is what perfect play lands, and the working says so: Gwen Q 5 snips in the centre (snips: 1–5, center:), R 9 needles (needles:); Viktor Q with Discharge (discharge:), R 6 storm ticks (ticks:); Dr. Mundo W 12 ticks + recast (ticks:, recast:), Q never below its minimum; Kai'Sa Q all missiles on one target (missiles:); Smolder E all bolts (bolts:), R in the centre (sweetspot:); Samira W 2 slashes (slashes:), R 10 shots (shots:), Q as the blade slash (melee:); Zed Q shurikens: 1–3; Akali E and R with the recast (recast:); Yasuo E stacks: 0–4. Yasuo and Yone Q (and Yone W) cooldowns come from bonus attack speed; Yasuo and Yone crit chance is doubled. perform() and fight() use what actually happens instead: Snippy stacks from attacks, live shadows and Dark Spheres, Plasma, Headshot counts, Style, Gathering Storm, marks (Zed R, Yone E, Ezreal W) and missing health.
a.Q.value("name", vs: t) a.Q.name_of_formulaAny named formula or value of the ability (the Inspect tab lists them).
a.W.heal a.W.heal(target: x)Heal amount with heal & shield power, and what x receives. Targets the tooltip can't reach are an error.
a.E.shield(target: x)The same for shields.
a.W.healTarget a.E.shieldTarget“caster only”, “another ally”, “an ally or self”, “self and an ally”, “every ally” or “none”.
a.Q.cd a.Q.cdbase a.Q.cost a.Q.range a.Q.rank a.Q.nameCooldown after haste; before haste; mana; cast range; rank; ability name.
a.Q.speed a.Q.width a.Q.radius a.Q.castTime a.Q.delay a.Q.kind a.Q.reach a.Q.delivery a.Q.length a.Q.originPhysics: projectile speed, full width, area radius, cast time, appear delay, kind (line, area, cone, targeted, self), how far it can hit, where it comes from (skillshot, lobbed, placed, vector, remote, unit, self, cone; see Physics), a vector's length, and a remote ability's distance from its object to the target.
a.Q.arrival(distance: d)Seconds until the ability lands at d units.
a.E.ccDuration("root") a.E.ccDuration("root", vs: t)The crowd control's duration at the ability's rank; with vs, after t's tenacity (floor 0.3 s; airborne, suppression and drowsy are unaffected). 0 if the ability doesn't apply that type. The working says where the number comes from (game data value or the wiki).
a.R.ccTypes a.R.knockback a.W.slowAmountThe crowd control types the ability applies in fights; its knockback or pull distance (Janna R knocks back to 875 units from her); its slow (0.8 = 80%).
a.E.has(untargetable)Tag check on this ability.
Syndra.R.addTag(targeted); removeTag(pull);Correct the tags (see Correcting the data).
Fizz.E.setCooldown(10); Syndra.Q.setPhysics(delay: 0.6);Override numbers. setPhysics takes speed, width, radius, castTime, delay, dashSpeed, dashRange, kind, delivery, origin, originRange, length, reveal, reach.

Item and ItemSet

Ludens.gold Ludens.nameTotal cost; display name.
Ludens.ap Ludens.haste …Any stat (0 if the item doesn't have it).
Ludens.basedamageNamed passive values from the game data.
ItemSet s = {Ludens, Sorc};A set. s.add(x), s.remove(x), s.contains(x), s.size(), s + x. A set that breaks an item limit (two boots, Void Staff with Cryptbloom) is an error.
s.gold s.apTotals across the set.
items()Every finished item, for loops.

Rune

Runes go into with() or a.runes.add(). Shards can be repeated: .with(AdaptiveForce, AdaptiveForce, HealthScaling). Conqueror.description returns the rune's text, which is where its numbers come from. runes() lists every rune. See Modelled runes for which ones change stats and which act in fights.

Summoner spells

Flash, Ghost, Heal, Barrier, Ignite, Exhaust and Cleanse are values of type Summoner. Give a champion two with x.with(Flash, Ignite) or x.summoners = {Flash, Ignite};. A champion without any set is assumed to have whichever one you use (the result says so).

Champion o = Orianna(11).with(Flash, Ignite);
print(o.Flash.cooldown, o.Ignite.damage);           // 300, 300 at level 11
prove canDodge(o, Syndra(11).Q, distance: 800, using: o.Flash);
Combo c = {Flash, Q, W, Ignite};
print(Annie(11).perform(c, vs: Dummy(hp: 3000)).damage);
spellnumbers (game files; checked against the wiki)cooldown
Flash.distance 400-unit blink, no cast time300 s
Ghost.speed 24% → 48% bonus move speed over levels 1–18, .duration 10 s, no ramp-up240 s
Heal.heal 80 → 318 over levels 1–18; .speed +30% move speed for 1 s240 s
Barrier.shield 100 → 460 over levels 1–18, .duration 2.5 s180 s
Ignite.damage 70 true damage, +20 per level to 5, +25 per level from 6 (475 at 18), over 5 s; 40% grievous wounds180 s
Exhaust.damageReduction 35% less damage dealt, .slow 40%, .duration 3 s240 s
Cleanse.tenacity 75% for 3 s; removes disables240 s

.cooldown uses summoner spell haste: base × 100 ÷ (100 + haste), from Ionian Boots of Lucidity (10), Crimson Lucidity (20) and Cosmic Insight (18). Numbers that scale with level use the champion's level (x.Ignite.damage) or defaultLevel (Ignite.damage).

  • Dodging: using: Flash, using: Ghost, using: Heal (its speed), or combined with abilities: using: {o.W, Flash}. See Physics and dodging.
  • Combos: Combo c = {Flash, R, Ignite};. Ignite adds its true damage (5 ticks, one every 1.056 s) and grievous wounds; Exhaust cuts the target's damage by 35% for 3 s; Heal and Barrier heal and shield the user. A summoner the champion didn't take is skipped; one on cooldown is waited for, like abilities.
  • Fights: a champion with .summoners set uses Ignite and Exhaust as soon as it fights (Exhaust on the enemy with the most AD + AP), and Heal and Barrier below 30% health. Flash, Ghost and Cleanse don't change fight numbers.

TeamComp and lists

TeamComp blue = {Ornn(11), Vi(11), a};A team.
blue[0] blue.size() blue.add(x) blue.contains(x)Index, size, add a copy, membership by champion.
for (Champion c : blue) …Loop over copies; Champion& c to change members.
champions() champions(Assassin)Every champion (of a class) at defaultLevel.

Fight

Fight f = fight(blue, red, 10);Both sides fight for up to 10 seconds (0–120). A side is a Champion or TeamComp.
fight(blue, red, 20, start: 1000)The two front lines start 1000 units apart (default 0: in contact). Also formation: false (everyone on the front line instead of ranged units at their attack range behind it), kite: false (nobody kites: role "kite", whether default or set with x.role, becomes "fight" for this fight), room: 600 (a champion can back off at most 600 units from where it started; default: unlimited). See Crowd control and positions.
f.ccTime(x) f.blocks(x) f.position(x) f.distance(x, y)Seconds x spent unable to act (stunned, airborne, suppressed, asleep, charmed, feared, taunted); abilities x's spell shields blocked; x's position on the line at the end (side 1 faces +, side 2 faces −); the distance between two champions at the end.
f.winner f.duration f.log1 or 2 if only that side has anyone alive, else 0; end time; every event as text.
f.alive(x) f.dead(x) f.hp(x) f.hpPercent(x) f.deathTime(x)How one champion ended up (deathTime is infinite if alive).
f.dealt(x) f.taken(x) f.healed(x) f.shielded(x) f.received(x)Damage dealt and taken, healing and shielding done, healing received.
f.survivors(1) f.deaths(blue) f.totalHealing(2)Per side, by number or by the TeamComp you passed in.
canKill(a, t, 3)Can a kill t within 3 seconds? t doesn't hit back unless fightBack: true.
canKill(a, t, 5, healers: soraka)Healers join t's side and only heal and shield.
timeToKill(a, t, healers: …, fightBack: …, within: 60)Seconds until t dies, or infinity if it survives within seconds.

Combo

A combo is an exact sequence of abilities and auto attacks, run through the fight simulator against one target, so every proc applies: spellblade after abilities, Electrocute on the third separate hit, Luden's, on-hit items, crits, burns, and the target's shields, Zhonya's and healers.

Combo burst = {R, E, Q, AA};               // AA = one auto attack
Combo full = burst + W + Q;                // or Combo(Q, AA) + E, or c.add(W)
ComboResult r = zed.perform(full, vs: lux);
print(r.damage, r.time, r.killed, r.steps);
a.perform(c, vs: t)Runs the combo on t and returns a ComboResult. An inline list works too: a.perform({Q, AA, E}, vs: t).
wait: falseSkip an ability that's on cooldown instead of waiting for it (default: wait).
healers: s fightBack: true within: 30Healers on t's side; t attacks back; time limit in seconds.
distance: 750Units between the two champions' centres, for effects that depend on it (Arcane Comet +0–100% over 0–750 units, Aftershock's 350 radius). The steps still ignore range.
a.combo(c, vs: t)Shorthand for a.perform(c, vs: t).damage. (a.combo(Q, E, R, vs: t) with plain slots is the instant formula sum, without procs or timing.)
r.damage r.time r.dpsTotal damage (including burns after the last step); time of the last step; damage per second.
r.bySource("Kraken Slayer")Total damage after resistances from one source (an item, rune or ability), like an item's "damage dealt" counter. r.floatingText lists every source's total.
r.killed r.killTime r.hpLeft r.hpPercentWhether and when the target died; what it has left.
r.steps r.lingering r.logDamage per step (skipped steps say why); damage after the last step; every event.
r.healed r.shieldedHealing and shielding done by the performer (lifesteal, Sundered Sky, Eclipse, item actives…).
{ProfaneHydra, Q, AA}An item with an active can be a step: it is pressed at that point (Tiamat and the Hydras, Stridebreaker, Gunblade, Rocketbelt, Zhonya's, Redemption, Locket, Mikael's, Actualizer, potions…). Damaging actives are only used when listed; in fight() fighters press them whenever they're ready.
c.size() c.add(W) c.stepsCombo length, extending it, the sequence as text.
{Q, E, P, W, E, P}P applies the passive's damage formula once (e.g. Katarina picking up a dagger). Its trigger is assumed to happen; the assumptions say so. Passives without a damage formula are skipped with a note.

Each step happens as soon as it can: an ability when it's off cooldown (casts lock the champion for 0.25 s), an auto attack when the attack timer allows. Every step hits; the assumptions list says so.

print(r.floatingText);Every damage instance in order: time, step, source (ability, attack, Luden's, Electrocute…), type, exact value, and the value floored and rounded, then the per-step totals. For comparing with the practice tool line by line.
r.stepDamage(i) r.hit(i) r.hitCountDamage of step i and of damage instance i (both count from 1); the number of instances. Burn ticks after their step are instances with step “tick”.

Rounding. The game shows whole numbers in floating combat text and on the dummy. Whether it rounds or truncates damage numbers is not documented: the wiki only says the health bar rounds up and the C panel rounds armor, MR, AD and AP to the nearest whole number (wiki “Health”). floatingText shows both until measurements settle it; the practice tests allow ±1 per displayed number.

Practice-tool dummy

Dummy(hp:, armor:, mr:) is the Practice Tool target dummy, usable wherever a target Champion is: x.Q.damage(vs: d), x.proc(Item, vs: d), x.perform(combo, vs: d), canKill, timeToKill, fight. All settings are optional.

Dummy d = Dummy();                           // 1000 health, 0 armor, 0 MR
Dummy big = Dummy(hp: 3000, resists: 50);    // 20 × Add 100 Max HP, 5 × Add 10 Resistances
print(big);                                  // Target Dummy (practice tool: 3000 health, 50 armor, 50 magic resist)
auto r = Syndra(11).perform({Q, W, E}, vs: big);
print(r.floatingText);
Defaults1000 health, 0 armor, 0 magic resist, level 1, no stat growth, move speed 370, attack range 175 (melee), gameplay radius 65. Game file Characters/PracticeTool_TargetDummy (patch 16.19: baseHP 1000, baseArmor 0, no base MR, unit tag “Champion”) and the wiki's Practice Tool page agree.
hp:“Add 100 Max HP” gives 100 bonus health; “Remove” only takes away what was added. So the tool offers 1000, 1100, 1200… and everything above 1000 is bonus health (it matters for Lord Dominik's Regards and other effects that read bonus health).
resists: armor: mr:“Add 10 Resistances” gives 10 bonus armor and 10 bonus MR, so the tool offers 0, 10, 20… with armor = MR. resists: 50 sets both. armor: and mr: can be set apart for theory; the output then says the practice tool can't do it.
Counts as a champion“It stands in as a champion for all intents and purposes” (wiki); the game file tags it Champion. Champion-only effects (Electrocute, Kraken Slayer, Conqueror, Lord Dominik's, …) apply.
Can't dieIts health stops at 1; it takes at most (max health − 1). r.killed/r.killTime, canKill and timeToKill report when damage taken reached its max health, the moment a champion with these stats would die. After that, current-health effects see a 1-health target, as in game. Only Urgot's R can actually kill it (wiki).
Resets after 3 sAfter 3 seconds without damage it restores to full health and resets its counters (wiki). A combo with a gap of 3 s or more starts over on a full dummy, in the engine too.
Doesn't actIt never attacks, moves or casts; fightBack: does nothing. canDodge(d, …) is an error. It is immune to Grievous Wounds (it doesn't heal anyway).
d.hp d.armor d.mr d.bonushp d.hitboxIts settings. d.hitbox is 65 and grows with bonus health up to 130 at 10,000 health (wiki); straight-line growth in between is an assumption and is labelled as one.
CountersSince patch 25.14 the dummy's “last hit” counter adds up every damage instance from one hit (an attack plus its on-hit procs) (wiki Practice Tool, patch history). r.floatingText prints per-step totals for that.

Measurements from practice-tool sessions go in tests/practice/ (format in its README); tools/practice_to_tests.py turns them into assertions that the regression gate runs.

Built-in functions

min max abs floor ceil round sqrt powMaths.
champions(Class?) items() runes()Lists for loops.
fight canKill timeToKillSee Fight.
canDodge(b, a.Q, distance: d, using: b.E, reaction: 0.25)See Physics and dodging.
Champion(Syndra, 11) ItemSet(…) TeamComp(…)Constructors.

Rules and claims

rule dive(Champion x, Champion y) -> beats(x, y) [1] {
    return x.is(Assassin) && x.has(mobile) && !y.has(mobile);
}
rule stunTheDive(Champion x, Champion y) -> !beats(x, y) [2] {
    return x.has(mobile) && y.has(stun);
}
prove beats(Fizz, Syndra);
  • A rule is an argument that the claim is true (-> beats(x, y)) or false (-> !beats(x, y)) whenever its body returns true. [n] is its strength, default 1.
  • For a claim, every rule with that name and number of arguments is tried. The strongest argument that fires wins. If the strongest for and against are equally strong, the claim is contested. If none fire, no rule applies.
  • A body written as return c1 && c2 && c3; is shown condition by condition, with the evidence for each (the tooltip phrase behind a tag, the formula behind a number). Other bodies are shown as a whole.
  • Claims can be used inside functions and other rules as booleans: beats(a, b). A claim is true only when it's proved. Sub-proofs are shown nested.
  • Rule arguments are the parameter names in the claim, in order: -> counters(y, x) is allowed.

Correcting the data

Tags are read from tooltip text, and a few physics values are missing from the game files. Correct them in your program. Every result that uses a correction lists it under Holds only if you accept.

Syndra.R.addTag(targeted);Add a tag to an ability.
Syndra.W.removeTag(pull);Remove a wrong one.
Fizz.E.setCooldown(10);Override a cooldown.
Syndra.Q.setPhysics(speed: 0, delay: 0.6);Override physics.
Fizz.addClass(Mage); Fizz.removeClass(Fighter);Change classes.
Katarina.P.setDamageType("magic");Set an ability's damage type when the tooltip doesn't say and the engine's guess (physical if it scales with AD, else magic) is wrong.

Corrections apply to that champion everywhere in the program, from the line where they're made.

Physics and dodging

canDodge(defender, ability, distance: d, using: defender.X, reaction: r, preBuff: bool, hitbox: units) checks whether the defender can avoid an ability aimed perfectly at them from d units away.

Where it comes from. Range doesn't always mean the ability travels from the champion to the target. Every ability has a delivery (a.Q.delivery), read from the game files' targeting type and corrected by hand where they mislead (each correction cites its source):

deliverywhat happenstime to land at d unitsreach
skillshotFired from the caster in a direction (Ezreal Q, Lux Q).cast + d ÷ speed + delayrange
lobbedThrown from the caster to a chosen point; hits an area where it lands (Lux E, Ziggs Q, Gragas Q).cast + d ÷ speed + delayrange + radius
placedAppears at a chosen point within range (Syndra Q, Xerath W, Karthus Q, Veigar W). Distance doesn't matter.cast + delayrange + radius
vectorThe caster picks a start point anywhere within range, then it runs its length in a chosen direction (Viktor E, Rumble R). Aimed perfectly the start sits on the target.cast + delay + (d − range, if beyond range) ÷ speedrange + length
remoteComes from another object: Orianna's ball, Azir's soldiers, Ornn's elemental, Xayah's feathers. The object starts on the target (perfect play) unless you say otherwise with setPhysics(origin: units).cast + origin ÷ speed + delaythe object's range + radius
unitPoint-and-click, including homing bolts and empowered attacks.cast + d ÷ speedrange
selfCentred on the caster (Amumu R, Alistar Q).cast (+ d ÷ speed if it expands)radius
coneA cone from the caster.cast + d ÷ speedcone length
  • Arrival time (a.Q.arrival(distance: d)) is measured from the start of the cast, by the delivery's formula above.
  • Time to respond = arrival time − the moment it becomes visible − reaction time (default 0, perfect play). It becomes visible when the cast ends (the missile or ground marker appears then; during the cast time the target point can't be seen), unless its data says it shows later (Rumble R: nothing is on the ground until the first rockets land). So Syndra Q gives 0.6 s to respond at any distance.
  • Hitbox: the defender's gameplay radius (b.hitbox: 65 for most champions, 80 for large ones, 55 for small ones). Test a grown champion with hitbox: 100.
  • Out of reach: beyond the reach above plus the defender's hitbox, the defender is safe standing still.
  • Walking: the defender must move half the width (lines), the radius (areas) or the cone's half-width at that distance, plus their hitbox, before it lands. Move speed includes boots and other items, runes and League's soft caps (b.ms). For an area centred on the caster, they walk out of it instead. The line prints the move speed that would be needed.
  • Point-and-click abilities can't be walked out of.
  • using: an ability that makes the defender untargetable or invulnerable dodges anything if its cast time is shorter than the time to respond. A dash or blink adds its distance (after its cast and travel time), then the defender keeps walking. A move-speed boost (Orianna W, Hecarim E, Singed R, Ghost, Heal, …) starts after its cast: the defender stands still while casting, then walks at the boosted speed (with its decay, ramp or field, and League's soft caps above 415). Perfect play: the defender uses whatever moves it farthest, so a boost whose cast costs more than it gains is skipped and plain walking counts.
  • preBuff: true: the move-speed boosts in using: were cast before the ability appeared, so they're already running (fresh) when the time to respond starts, and cost no cast time.
  • Several at once: using: {o.W, Flash} tries every combination of the listed abilities and summoner spells (boost casts first, then dashes and blinks, then walking) and uses the best. using: Flash adds a 400-unit blink with no cast time.

setPhysics also takes delivery: "placed" (or any of the above), origin: (units from the remote object to the target), length: (a vector's length), reveal: (seconds into the cast when it becomes visible) and reach:.

Speeds, widths, radii and cast times come from the game files; where only the wiki has a value (some radii and dash speeds), the result lists it as an assumption. Dash speeds and appear delays that neither source has default to 1200 units/s and 0, also listed as assumptions and settable with setPhysics.

How fights are simulated

  • Time moves in 0.05-second steps. Within a step everyone acts at once, on the state at the start of the step: first items, summoner spells, heals and shields, then everything else. Damage between the sides (and the healing it causes), deaths, walking, blinks, new crowd control (and the interruptions it causes), stasis and knockbacks take effect at the end of the step. So the order of the champions, and which side is side 1, doesn't change the result: a mirror match ends even, and fight(a, b) is fight(b, a) with the sides swapped (tests/fight_symmetry.rl). Two champions can kill each other in the same step. The line is centred on 0 (side 1's front at −start/2) so mirrored positions are exact.
  • Each champion, when free, first uses a heal or shield if an ally needs one (healing ultimates are saved for allies below 40% health), then peels (below), then the first ability in its rotation that is off cooldown and in reach, otherwise an auto attack if the target is in attack range, otherwise it moves. Casting locks the champion for 0.25 seconds. See Crowd control and positions.
  • Damage goes through amplifiers, resistances after penetration, Bone Plating and shields, then health. Lifesteal (attacks), omnivamp and Conqueror heal the attacker; grievous wounds cut healing received by 40%.
  • Heals go to whoever the healer's policy picks; team heals reach everyone; Moonstone chains 30% to a second ally.
  • Zhonya's (below 30% health) puts a champion in 2.5 seconds of stasis; Guardian Angel revives once after 4 seconds.
  • Summoner spells (only for champions with .summoners set, or as combo steps): Ignite and Exhaust on first contact, Heal and Barrier below 30% health.
  • The fight ends when one side has nobody left, or at the time limit.
Positions are on one line; every ability in reach hits (perfect aim; no dodging inside fights), abilities land when cast (dashes when they arrive), mana is ignored, and special mechanics beyond an ability's tooltip formula aren't simulated. Use canDodge and your own rules to reason about the rest.

Crowd control and positions

Positions

  • One line, centred on 0. Side 1's front is at −start/2 and faces +; side 2's front is at +start/2 and faces − (start: default 0). Each champion stands max(0, attack range − 175) behind its front, so ranged champions start at their attack range from where the enemy front will be (formation: false: everyone on the front line).
  • Reach (wiki "Range"): basic attacks use edge range (attack range + both gameplay radii); point-and-click abilities centred range; other abilities their reach (.reach) + the target's radius. Placed and lobbed areas hit every enemy within their radius of the target; self-centred areas every enemy within the radius of the caster; lines and cones every enemy within reach (on one line everything is in the path).
  • Walking uses the current move speed: (base + flat) × (1 + % bonuses) × (1 − the strongest slow × (1 − slow resist)), then the soft caps (wiki "Movement speed": only the strongest slow counts).
  • Dashes and blinks (abilities tagged dash or blink) use the dash range and speed from the game data (dashInfo; 1200 units/s when the data has none). A dash ends touching its target or at its range; its hits land when it arrives. A blink moves at once.
  • Knockbacks move the target away from the caster by the data distance over the airborne time (Janna R: to 875 units from her; Gragas R 900; Alistar W 700; Vayne E 475); pulls move it toward the caster. A placed knockback (Gragas R) is aimed away from the caster's team, or toward it for role "engage". Knockbacks whose distance neither source gives (Gragas E, Draven E, Ornn Q) only make the target airborne.

Roles (x.role)

"kite"Default for ranged champions. Targets the lowest-health enemy it can reach (else the nearest). Backs off from any shorter-ranged enemy inside its attack range between attacks, uses dashes and blinks to escape such an enemy, and uses hard crowd control first on enemies diving an ally (peel). Knockbacks are held for peel.
"dive"Default for melee champions. Walks or dashes to the lowest-health enemy and uses everything on it, except knockbacks, which it holds to peel an enemy diving an ally.
"engage"Like dive, but also uses knockbacks offensively at once (placed ones push the target into its team).
"peel"Holds every hard crowd control ability for enemies that reach an ally they are outranged by (or itself), guards the rearmost ranged ally when nobody is diving.
"fight"Targets the lowest-health enemy it can reach, walks to attack range, no kiting. Every "kite" champion, default or set with x.role, uses it under fight(…, kite: false).

An enemy "dives" ally A when it is within its own attack range (+ 50) of A and A's attack range is longer. Spell shields, Morgana E and cleanses react perfectly: Sivir E and Nocturne W are raised as a hostile ability lands; Morgana E is cast on the ally a hard crowd control is about to land on (if in range); Quicksilver Sash and Mercurial Scimitar cleanse as soon as a cleansable crowd control lands; Mikael's Blessing is used on a crowd-controlled ally.

Crowd control types

stun, suppression, sleep, airborne (knock-up, knockback, pull)No moving, attacking or casting. Interrupts channels.
charm, fear (flee), taunt, berserkNo control: charm and taunt walk to the caster, fear walks away, taunt and berserk attack the caster when in range.
rootNo moving or dashing; attacks and casts allowed.
silenceNo casting. polymorph: no attacking or casting (walking allowed). disarm: no attacking. ground: no dashes. blind: attacks miss.
slowLower move speed (strongest slow only; decaying slows fade linearly).

Source: wiki "Types of Crowd Control" (summary table). Tenacity (wiki "Tenacity") shortens every type except airborne, suppression and drowsy, to no less than 0.3 s; items, elixirs and the rune shard stack multiplicatively (1 − (1 − a)(1 − b)…). Quicksilver removes everything except airborne (suppression included); Mikael's Blessing removes stuns, roots, silences, polymorphs, forced actions, sleep and slows, not suppression or airborne. Spell shields (Banshee's Veil, Edge of Night, Verdant Barrier, Sivir E, Nocturne W) block one whole ability, crowd control included.

Where the numbers come from

src/cc_export.py reads which crowd control each ability applies from the wiki ability text ({{tip|stun|…}} for {{fd|1}} seconds, knock back 900 units, slowed by 80%) and takes the value from the game data (StunDuration, RootDuration, KnockupDuration, KnockbackDistance, SlowAmount, …) when one exists and agrees with the wiki at every rank; otherwise the wiki's value, and the working says so. Where they disagree the wiki value is used and the game data value is shown (python src/cc_export.py lists them: KSante Q, Rammus Q, Urgot E, Hwei R, Lissandra R, Maokai Q…). Durations that grow with distance travelled (Xerath E 0.75–2.25 s, Braum R) are assumed linear up to the ability's reach; charge-dependent ones (Janna Q, K'Sante W, Sion Q) use the uncharged value, since fights cast at once. Kennen's Mark of the Storm is modelled: every ability hit marks, the third mark stuns 1.25 s (0.5 s if stunned by it within 6 s; game data), Slicing Maelstrom marks each target up to 3 times. Effects the wiki makes conditional on terrain (Vayne E stun, Poppy E stun, Ornn E) are left out, as are effects with no known duration (the assumptions list each one). Braum R: the first target's knock-up grows from 0.6 s with distance, assumed linear up to the ability's reach.

Damage, healing, stats

  • Stats at a level: base + growth × (level − 1) × (0.7025 + 0.0175 × (level − 1)), the game's formula.
  • Ability formulas are evaluated from the game's calculation trees: per-rank values, stat ratios (total, bonus or base), level curves, sums, products and clamps. Stack-based parts count as 0 stacks (noted).
  • Resistances: percent penetration first, then flat (lethality is flat armor penetration); damage × 100 ÷ (100 + resist), or 2 − 100 ÷ (100 − resist) below zero.
  • Adaptive runes give AD when bonus AD exceeds AP and AP when AP exceeds bonus AD (1 adaptive = 0.6 AD or 1 AP). The AP compared includes Rabadon's +30%, which then also amplifies adaptive AP. A tie (e.g. no items) goes to the champion's adaptive type from the game files (Zed: AD, Syndra: AP). Adaptive damage is physical or magic by the same test.
  • Abilities that apply on-hit effects (the wiki's "Attack effects" list: Katarina's Shunpo, Sinister Steel and Death Lotus, Ezreal Q, Fiora Q, Gangplank Q, Irelia Q, Yasuo Q, Yone Q…) trigger on-hit items at the listed effectiveness (Death Lotus 25/30/35%, Master Yi Q 18.75%, Urgot W 50%…). Channels like Death Lotus are separate hits (15 daggers per target). Ones that also trigger on-attack effects count as basic attacks for Kraken Slayer, Terminus and Hullbreaker stacks. On-hit-only ones stack and consume Kraken's Bring It Down, per the wiki's Kraken Slayer page, but build no Terminus stacks (assumed).
  • Critical strikes deal 200% (base; Infinity Edge +30% = 230%). Ashe's crits deal no bonus damage.
  • Attack speed is capped at 3.003 (one attack per 0.333 s); Bel'Veth has no cap. Cap exceptions that only last during an ability (Jinx, Kennen, Varus, Sion, Zeri) aren't modelled.
  • Move speed = (base + flat) × (1 + % bonuses), then soft caps: above 415 → × 0.8 + 83, above 490 → × 0.5 + 230.
  • Item limits from the game's item data: one pair of boots, one Blight item (Void Staff, Cryptbloom, Terminus…), one Hydra, one Spellblade, no two copies of a legendary, and so on. Breaking one is an error.
  • Cooldowns: base × 100 ÷ (100 + ability haste). Haste that one ability gives another (Syndra R: Dark Sphere +10 per R rank) is included.
  • Heals: formula × (1 + heal & shield power) × (1 + healing received), capped at missing health.

Modelled runes

Values the rune text gives as a range “based on level” are spread evenly from level 1 to 18 unless the wiki gives another curve (Fleet Footwork follows the champion-growth curve). Stacks earned before the fight (Legend runes, Ultimate Hunter, Overgrowth, Dark Harvest souls, Manaflow Band mana) start at 0, as in a fresh practice-tool game; set them with a.stacks(LegendAlacrity, 10). Gathering Storm, Conditioning and Biscuit Delivery follow gameMinute. Every rune's status, source and approximation is in the runes audit (outputs/runes_audit/RUNES.md), and every choice appears in the assumptions.

Modelled item passives

Every item's stats are always included. These items' passives and actives are also modelled in fights and combos, using the values in the game data (the few values the data lacks come from the item text):

These items have nothing a fight can see (movement, vision, mana, cleanses, gold); a build that contains one says so in the assumptions:

Stacks earned before a fight (Heartsteel, Mejai's, Hubris, Yun Tal, Rod of Ages…) start at 0, as when you buy the item in the practice tool; set them with a.stacks(Item, n). Every approximation (for example Energized items recharging only from attacks, because nobody moves) is listed in the assumptions of any result that uses the item.

Ability tags

Tags are read from each ability's tooltip text. mobile means dash or blink; hardcc means any hard crowd control; targeted means point-and-click. The Inspect tab shows the phrase behind every tag.

Data sources

  • Riot Data Dragon (patch ): champion stats, ability tooltips, cooldowns, ranges, item names and prices.
  • CommunityDragon game files: ability calculation trees, per-rank values, projectile speeds, widths, cast times; item stats and passive values.
  • Client rune data: rune descriptions, whose numbers the rune model uses.

The data is rebuilt for a new patch with python src/fetch_static.py, python src/calc_export.py and python src/kb_export.py.

Limits

  • A proof holds for the model, not the live game. The assumptions list says exactly where the two can differ.
  • Tooltip-derived tags and damage types can be wrong; check the evidence and correct with addTag / removeTag.
  • Champion-specific mechanics (Zed's shadows, stored damage, stacking passives, empowered recasts) are only as good as the tooltip formula.
  • Fights are one-dimensional: no terrain, walls, flanks or body blocking, and area abilities catch everyone within their radius on the line (2-D spacing would catch fewer). Mana is ignored. Physics checks assume perfect aim and a straight-line dodge.