-- | A module containing various definitions of dice as random events, and a few
-- associated helper functions.  See 'DieRoll', which is really a synonym for 'EventM'
-- Int.
module Numeric.Probability.Game.Dice (
  -- * Main die roll type
  DieRoll, roll,
  -- * Dice definitions
  d4, d6, d8, z10, d10, d12, d20, d100, z100, d, z,
  -- * Dice helper functions
  rerollOn) where

import Numeric.Probability.Game.Event (EventM, enact, makeEvent, makeEventProb, outcomes)

-- | A type synonym for events with an integer outcome (i.e. all standard die rolls).
--
-- The 'Num' instance for @EventM Int@ allows you to add the results of two die
-- rolls, or subtract them (if it helps, @(+) = liftA2 (+)@).
--
-- Multiplication works as follows.  @d * e@ evaluates the first die roll,
-- then sums that many rolls of the second.  So @2 * d6@ rolls two d6 and adds
-- the outcomes.  However, this definition means that @d6 * 2@ rolls one d6,
-- then effectively scales the result by 2.  And @d6 * d4@ rolls one d6, then
-- rolls that number of @d4@, adding their results together.  The simple rule
-- when one of the terms is a constant is: use the constant on the left-hand
-- side to get more dice, and use the constant on the right-hand side to scale
-- the result.
type DieRoll = EventM Int


-- | A die with an equal chance of rolling 1, 2, 3 or 4.
d4 :: DieRoll
d4 = d 4

-- | A die with an equal chance of rolling 1, 2, 3, 4, 5 or 6.
d6 :: DieRoll
d6 = d 6

-- | A die with an equal chance of rolling 1, 2, 3, 4, 5, 6, 7 or 8.
d8 :: DieRoll
d8 = d 8

-- | A die with an equal chance of rolling 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9.
z10 :: DieRoll
z10 = z 10

-- | A die with an equal chance of rolling 1, 2, 3, 4, 5, 6, 7, 8, 9 or 10.
d10 :: DieRoll
d10 = d 10

-- | A die with an equal chance of rolling 1 to 12 inclusive.
d12 :: DieRoll
d12 = d 12

-- | A die with an equal chance of rolling 1 to 20 inclusive.
d20 :: DieRoll
d20 = d 20

-- | A die with an equal chance of rolling 1 to 100 inclusive.
d100 :: DieRoll
d100 = d 100

-- | A die with an equal chance of rolling 0 to 99 inclusive.
z100 :: DieRoll
z100 = z 100

-- | Makes a die that has an equal chance of achieving the numbers 1 through the
-- number given.  @d 4@ has an equal chance of producing the outcomes 1, 2, 3 and
-- 4, @d 1@ is equivalent to @return 1@ (a certain result of 1), and
-- @d@ is undefined for any number below 1.  For convenience, all the standard dice are
-- provided, e.g. @d6 = d 6@.
d :: Int -> DieRoll
d n = makeEvent [1..n]

-- | Makes a die that has an equal chance of achieving the numbers 0 through
-- the one less than the number given.  @z 4@ has an equal chance of producing
-- the outcomes 0, 1, 2 and 3, while @z 1@ is equivalent to @return 0@ (a
-- certain result of 0), and @z@ is undefined for any number below 1.  For
-- convenience, several standard dice that can be interpreted with a lower
-- result of 0 are provided, e.g. @z10 = z 10@.
z :: Int -> DieRoll
z n = makeEvent [0 .. (n - 1)]

-- | Rerolls the die when the specified outcome(s) occur.  This has the effect
-- of removing the outcomes from the set of outcomes and rescaling all the other
-- probabilities linearly to sum to 1.  For example:
--
-- > d6 `rerollOn` [5,6] == d4
-- > chancePred (== 12) ((2*d6) `rerollOn` [7]) == 1/30
--
-- With the latter example, the standard chance of 12 on 2d6 is 1\/36, which
-- is rescaled by 36\/30, the reciprocal of the chance of /not/ hitting
-- a 7.
rerollOn :: DieRoll -> [Int] -> DieRoll
rerollOn dc ns = makeEventProb $ filter ((`notElem` ns) . fst) $ outcomes dc

-- | A nice synonym for 'enact': actually rolls the die and produces a single result according to the probabilities
-- in the @EventM a@ parameter.
roll :: DieRoll -> IO Int
roll = enact