# For Warframe Players
[Secondary Enervate](https://wiki.warframe.com/w/Secondary_Enervate)
is amazing and has become a very interesting option for many exalted secondaries since the rework.
But have you ever wondered what average crit chance it actually yields when theory crafting?
We can answer this with math!
# The Math
Guns in the looter-shooter game Warframe can score [critical hits](https://wiki.warframe.com/w/Critical_Hit)
of different tiers, with tier $0$ representing no critical hit.
To this end, each weapon has a crit chance $C\in\mathbb{R}_+$.
The natural number $T = \lfloor C\rfloor$ defines the guaranteed tier of the crit, i.e. $C < 1$ means that no crit is guaranteed, and the number $C - T$ is the chance of scoring a crit of tier $T + 1$.
There are weapon modifications called "arcanes" and the arcane named [Secondary Enervate](https://wiki.warframe.com/w/Secondary_Enervate) gives the weapon the following properties:
- A bonus $A$ is added to the effective value of $C$, i.e. $C+A$ is used instead of $C$ to determine the crit tier of all hits.
- After each hit (regardless of crit tier), the value $A$ is permanently increased by $\tfrac1{10}$.
- If 6 critical hits of tier 2 or higher are scored, the arcane resets to $A=0$.
We are now asking: Given $C$, what is the average value of $P = C + A$ as the number of hits approaches infinity?
# Disclaimer
Firstly, I am primarily interested in [Titania](https://wiki.warframe.com/w/Titania/)'s
[Dex Pixia](https://wiki.warframe.com/w/Dex_Pixia), i.e. I will usually assume $C = \tfrac{1}{10}$.
However, all code and theory presented here can be used to do this for any other secondary.
We will also work with integers by setting $c := 100 \cdot C$, i.e. $c$ corresponds to the integer percentage value shown in-game. Similarly, we call $a := 100 \cdot A$.
# Monte Carlo Method
My first approach to this was to run a Monte Carlo experiment:
In other words, emulate sustained gunfire up to 10 million hits, and use a pseudo-random number generator to simulate the crits. I wrote this code:
import random
cs = 10
ch = 0
n = 10_000_000
total = 0
for _ in range(n):
total += cs
if cs > 100:
limit = (cs - 100) / 100
if random.random() < limit:
ch += 1
if ch >= 6:
ch = 0
cs = 10
continue
cs += 10
print(total / n)
This works fine and gives us roughly 107.86
.
However, it isn't super fast and also and mathematically dissatisfying because it is empirical in nature:
I would prefer an exact answer to this question.
In fact, since all probabilities are rational here, I have reason to believe that the final answer is also rational,
and there is something deeply satisfying about providing an exact fraction as the answer rather than a decimal approximation.
# Markov Chains
For a Markov chain, we have to define the possible "states" that our experiment can have.
Our states are defined by two values:
- The current value of $p=a+c$.
- The number $b$ of tier 2 critical hits that have already been scored since the arcane was last reset.
Notably, $c$ is considered a constant here.
We then have to specify the probabilities for transitioning from each state $(a+c, b)$ to another one.
For example, assuming $c=10$:
- The state $(10,0)$ has a probability of $1$ to transition to $(10,0)$.
- The state $(200,0)$ has a probability of $1$ to transition to $(210,1)$.
- The state $(150,2)$ has probability $\tfrac12$ to transition to $(160,3)$ and equal probability $\tfrac12$ to transition to $(160,2)$.
- The state $(200,5)$ has probability $1$ to transition back to $(10,0)$.
We can generate all possible states like so:
from typing import NamedTuple
class State(NamedTuple):
crit_chance: int
big_hit_count: int
def all_states(state_cc: int = 10, big_hits: int = 0):
if big_hits >= 6:
return
yield State(state_cc, big_hits)
cc = state_cc + 10
if state_cc > 100:
yield from all_states(cc, big_hits + 1)
if state_cc < 200:
yield from all_states(cc, big_hits + 0)
The code is a simple recursive design to determine all the states that we can even possibly have:
Starting at a crit chance percentage of $200$, for example, it is impossible not to increase the tier 2 crit counter
while at crit chance percentages up to $100$ it is impossible to increase it at all.
Next, we want to compute the matrix $M$ that represents the state transition.
The entries $M_{ij}$ of $M$ represent the probability to transition from state $j$ to state $i$.
In other words, the $j$-th column of $M$ contains a vector that assigns to each state $i$ the probability that we transition from $j$ to $i$.
The Monte Carlo method I implemented earlier corresponds to computing $w = M^{10000000}e_1$, where $e_1$ is the vector that assigns $1$ to the initial state $(10,0)$ and $0$ to all other states.
Each component $w_{(p,b)}$ of the vector $w$ then can be thought of as the probability to be in state $(p,b)$ for a random hit,
and the average crit chance can be computed as
$\sum w_{(p,b)} \cdot p$.
There is an underlying assumption here: Namely that the sequence $M^n e_1$ converges to a vector $v$ as $n\to\infty$,
and this is indeed true.
Now this vector $v$ is what we are actually after: It represents the true limit state of this experiment.
The vector also has a very notable property, namely we must have $Mv = v$, i.e. $v$ is an eigenvector or eigenvalue 1.
Luckily, this is easy to compute using [sympy](https://www.sympy.org/en/index.html):
from sympy import Rational, Matrix
def create_state_matrix(base_crit_chance: int) -> tuple[list[State], Matrix]:
states = sorted(set(all_states(base_crit_chance)))
matrix = []
for prev_cc, prev_bh in states:
column = []
is_tier2_hit = min(1, max(0, Rational(prev_cc - 100, 100)))
if prev_bh == 5:
column.append(is_tier2_hit)
else:
column.append(0)
for next_cc, next_bh in states[1:]:
if next_cc != prev_cc + 10:
column.append(0)
elif prev_bh == next_bh:
column.append(1 - is_tier2_hit)
elif prev_bh + 1 == next_bh:
column.append(is_tier2_hit)
else:
column.append(0)
matrix.append(column)
return states, Matrix(matrix).transpose()
states, matrix = create_state_matrix(10)
for val, mult, vects in matrix.eigenvects():
if val != 1 or mult != 1:
continue
for v in vects:
u = v / sum(v)
c = sum([s.crit_chance * k for s, k in zip(states, u)])
print(F'Average Critical Chance with Secondary Enervate: {c}')
This prints:
Average Critical Chance with Secondary Enervate: 27630690850/256146139
Notably, $\tfrac{27630690850}{256146139}\approx 107.87$ is pretty close to our Monte Carlo simulation.
I'll leave it for you to decide whether that is awesome or disappointing:
On the one hand, we now have solid theoretical proof and an exact number, but from a pragmatic perspective the Monte Carlo result was probably good enough and arguably easier to obtain. ¯\\_(ツ)_/¯