```-- | Representation of probabilities and random computations.
module Game.LambdaHack.Random
( -- * The @Rng@ monad
Rnd
-- * Random operations
, randomR, oneOf, frequency, roll
-- * Rolling dice
, RollDice(..), rollDice, maxDice, minDice, meanDice
-- * Rolling 2D coordinates
, RollDiceXY(..), rollDiceXY
-- * Rolling dependent on depth
, RollDeep, rollDeep, chanceDeep, intToDeep, maxDeep
-- * Fractional chance
, Chance, chance
) where

import qualified Data.Binary as Binary
import Data.Ratio
import qualified System.Random as R
import qualified Data.List as L

import Game.LambdaHack.Utils.Assert
import Game.LambdaHack.Utils.Frequency

-- | The monad of computations with random generator state.
type Rnd a = MState.State R.StdGen a

-- | Get a random object within a range with a uniform distribution.
randomR :: (R.Random a) => (a, a) -> Rnd a
randomR range = MState.state \$ R.randomR range

-- | Get any element of a list with equal probability.
oneOf :: [a] -> Rnd a
oneOf xs = do
r <- randomR (0, length xs - 1)
return (xs !! r)

-- | Gen an element according to a frequency distribution.
frequency :: Show a => Frequency a -> Rnd a
frequency fr = MState.state \$ rollFreq fr

-- | Roll a single die.
roll :: Int -> Rnd Int
roll x = if x <= 0 then return 0 else randomR (1, x)

-- | Dice: 1d7, 3d3, 1d0, etc.
-- @RollDice a b@ represents @a@ rolls of @b@-sided die.
data RollDice = RollDice Binary.Word8 Binary.Word8
deriving (Eq, Ord)

instance Show RollDice where
show (RollDice a b) = show a ++ "d" ++ show b

let (a, db) = L.break (== 'd') s
in case db of
'd' : b -> [ (RollDice av bv, rest) | (bv, rest) <- readsPrec d b ]
_ -> []

-- | Roll dice and sum the results.
rollDice :: RollDice -> Rnd Int
rollDice (RollDice a' 1)  = return \$ fromEnum a'  -- optimization
rollDice (RollDice a' b') =
in liftM sum (replicateM a (roll b))

-- | Maximal value of dice.
maxDice :: RollDice -> Int
maxDice (RollDice a' b') =
in a * b

-- | Minimal value of dice.
minDice :: RollDice -> Int
minDice (RollDice a' b') =
in if b == 0 then 0 else a

-- | Mean value of dice.
meanDice :: RollDice -> Rational
meanDice (RollDice a' b') =
let (a, b) = (fromIntegral a', fromIntegral b')
in if b' == 0 then 0 else a * (b + 1) % 2

-- | Dice for rolling a pair of integer parameters pertaining to,
-- respectively, the X and Y cartesian 2D coordinates.
data RollDiceXY = RollDiceXY (RollDice, RollDice)
deriving Show

-- | Roll the two sets of dice.
rollDiceXY :: RollDiceXY -> Rnd (Int, Int)
rollDiceXY (RollDiceXY (xd, yd)) = do
x <- rollDice xd
y <- rollDice yd
return (x, y)

-- | Dice for parameters scaled with current level depth.
-- To the result of rolling the first set of dice we add the second,
-- scaled in proportion to current depth divided by maximal dungeon depth.
type RollDeep = (RollDice, RollDice)

-- | Roll dice scaled with current level depth.
-- Note that at the first level, the scaled dice are always ignored.
rollDeep :: Int -> Int -> RollDeep -> Rnd Int
rollDeep n depth (d1, d2) =
assert (n > 0 && n <= depth `blame` (n, depth)) \$ do
r1 <- rollDice d1
r2 <- rollDice d2
return \$ r1 + ((n - 1) * r2) `div` max 1 (depth - 1)

-- | Roll dice scaled with current level depth and return @True@
-- if the results if greater than 50.
chanceDeep :: Int -> Int -> RollDeep -> Rnd Bool
chanceDeep n depth deep = do
c <- rollDeep n depth deep
return \$ c > 50

-- | Generate a @RollDeep@ that always gives a constant integer.
intToDeep :: Int -> RollDeep
intToDeep 0  = (RollDice 0 0, RollDice 0 0)
intToDeep n' = let n = toEnum n'
in if n > maxBound || n < minBound
then assert `failure` n'
else (RollDice n 1, RollDice 0 0)

-- | Maximal value of scaled dice.
maxDeep :: RollDeep -> Int
maxDeep (d1, d2) = maxDice d1 + maxDice d2

-- | Fractional chance.
type Chance = Rational

-- | Give @True@, with probability determined by the fraction.
chance :: Chance -> Rnd Bool
chance r = do
let n = numerator r
d = denominator r
k <- randomR (1, d)
return (k <= n)
```