Fixed lookup tests to, um, run.
[zzz-pokedex.git] / pokedex / formulae.py
index 2059db7..29ab514 100644 (file)
@@ -1,5 +1,22 @@
 # encoding: utf8
 """Faithful translations of calculations the games make."""
+from __future__ import division
+
+from itertools import izip
+
+def nCr(n, r):
+    """n-choose-r.
+
+    Thanks for the "compact" solution go to:
+    http://stackoverflow.com/questions/2096573/counting-combinations-and-permutations-efficiently
+    """
+
+    return reduce(
+        lambda x, y: x * y[0] / y[1],
+        izip(xrange(n - r + 1, n + 1),
+             xrange(1, r + 1)),
+        1)
+
 
 def calculated_stat(base_stat, level, iv, effort):
     """Returns the calculated stat -- i.e. the value actually shown in the game
@@ -19,3 +36,75 @@ def calculated_hp(base_hp, level, iv, effort):
         return 1
 
     return (base_hp * 2 + iv + effort // 4) * level // 100 + 10 + level
+
+def earned_exp(base_exp, level):
+    """Returns the amount of EXP earned when defeating a Pokémon at the given
+    level.
+    """
+
+    return base_exp * level // 7
+
+def capture_chance(percent_hp, capture_rate,
+                   ball_bonus=1, status_bonus=1, heavy_modifier=0):
+    """Calculates the chance that a Pokémon will be caught, given its capture
+    rate and the percentage of HP it has remaining.
+
+    Returns five values: the chance of a capture, then the chance of the ball
+    shaking three, two, one, or zero times.  Each of these is a float such that
+    0.0 <= n <= 1.0.  Feel free to ignore all but the first.
+    """
+
+    if heavy_modifier:
+        # Only used by Heavy Ball.  Changes the target's capture rate outright
+        capture_rate += heavy_modifier
+        if capture_rate <= 1:
+            capture_rate = 1
+
+    # This should really be integer math, right?  But the formula uses FOURTH
+    # ROOTS in a moment, so it can't possibly be.  It probably doesn't matter
+    # either way, so whatever; use regular ol' division.  ball_bonus and
+    # status_bonus can be 1.5, anyway.
+
+    # A slight math note:
+    # The formula is originally: (3 max - 2 curr) rate bonus / (3 max)
+    # I have reduced this to: (1 - 2/3 * pct) rate bonus
+    # My rationale is that this cannot possibly be integer math, so rounding is
+    # not a problem and commutation won't make a difference.  It also
+    # simplifies the input considerably.
+    base_chance = (1 - 2/3 * percent_hp) * capture_rate \
+                * ball_bonus * status_bonus
+
+    shake_index = (base_chance / 255) ** 0.25 * (2**16 - 1)
+
+    # Iff base_chance < 255, then shake_index < 65535.
+    # The game now picks four random uwords.  However many of them are <=
+    # shake_index is the number of times the ball will shake.  If all four are
+    # <= shake_index, the Pokémon is caught.
+
+    # If shake_index >= 65535, all four randoms must be <= it, and the Pokémon
+    # will be caught.  Skip hard math
+    if shake_index >= 65535:
+        return (1.0, 0.0, 0.0, 0.0, 0.0)
+
+    # This brings up an interesting invariant: sum(return_value) == 1.0.
+    # Something is guaranteed to happen.
+
+    # Alrighty.  Here's some probability.
+    # The chance that a single random number will be <= shake_index is:
+    p = (shake_index + 1) / 65536
+    # Now, the chance that two random numbers will be <= shake_index is p**2.
+    # And the chance that neither will be is (1 - p)**2.
+    # With me so far?
+    # The chance that one will be and one will NOT be is p * (1 - p) * 2.
+    # The 2 is because they can go in any order: the first could be less, or
+    # the second could be less.  That 2 is actually nCr(2, 1); the number of
+    # ways of picking one item in any order from a group of two.
+    # Try it yourself add up those three values and you'll get 1.
+
+    # Right.  Hopefully, the following now makes sense.
+    # There are five cases: four randoms are <= shake_index (which means
+    # capture), or three are, etc.
+    return [
+        p**i * (1 - p)**(4 - i) * nCr(4, i)
+        for i in reversed(range(5))
+    ]