game-probability (empty) → 1.0
raw patch · 6 files changed
+374/−0 lines, 6 filesdep +basedep +probabilitydep +randomsetup-changed
Dependencies added: base, probability, random
Files
- LICENSE +30/−0
- Numeric/Probability/Game/Dice.hs +101/−0
- Numeric/Probability/Game/Event.hs +136/−0
- Numeric/Probability/Game/Query.hs +33/−0
- Setup.hs +2/−0
- game-probability.cabal +72/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c)2010, Neil Brown++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Neil Brown nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Numeric/Probability/Game/Dice.hs view
@@ -0,0 +1,101 @@++-- | 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+
+ Numeric/Probability/Game/Event.hs view
@@ -0,0 +1,136 @@+-- | A module containing the central type of the library, 'EventM', and various+-- related helper functions.+module Numeric.Probability.Game.Event (EventM, makeEvent, makeEventProb, outcomes, enact,+ coinToss, subst) where++import Control.Applicative (Applicative(..), (<$>), liftA2)+import Control.Arrow (second)+import Control.Monad (ap, replicateM)+import Data.Ratio (denominator)+import Numeric.Probability.Distribution (T(..), certainly, decons, fromFreqs, norm, selectP, uniform)+import System.Random (randomRIO)++-- | A probabilistic event with an outcome of type 'a'. See the 'enact' function+-- to actually run the event and randomly pick an outcome. +--+-- For an explanation of the 'Num' instance, see the DieRoll type in the "Numeric.Probability.Game.Dice"+-- module.+--+-- The 'Eq' instance compares the two distributions to see if they are equal.+-- This looks at all the outcomes and sees if their probabilities are equal on+-- the left-hand side and the right-hand side. For example,+-- @coinToss == fmap (>= 4) d6@, but @d12 /= d6 + d6@.+--+-- The 'Show' instance will display a horizontal bar-chart of relative outcome+-- probability. Note: this really is a relative probability -- common factors+-- are cancelled, and is not a count of the different outcomes. If you wish to+-- show the raw numbers, use @show . outcomes@ instead.+-- +-- The 'Functor' instance allows you to modify the outcome values without changing+-- their associated probabilities. For example, @fmap show d6@ changes the outcomes+-- into their String representations.+--+-- The 'Applicative' instance allows you to join together the results of two events+-- in a predetermined manner. For example, @makeEvent [id, (* 2)] \<*\> d6@ allows+-- you to roll a d6 that has a 50% chance of being doubled. Note that @pure+-- 6@ is an event that is certain to produce the outcome 6.+--+-- The 'Monad' instance allows you to base the choice of the next event on the+-- result of the previous event. For example, @coinToss >>= \x -> if x then d6+-- else d4@ will roll a d4 50% of the time and a d6 the other 50%. Note that @return+-- 6@ is an event that is certain to produce the outcome 6.+newtype EventM a = EventM (T Rational a)+ deriving (Monad, Functor)++instance Ord a => Eq (EventM a) where+-- Eq not defined properly for T prob a, work-around for now:+--(===) :: Ord a => EventM a -> EventM a -> Bool+ (==) (EventM a) (EventM b) = decons (norm a) == decons (norm b)++normEventM :: Ord a => EventM a -> EventM a+normEventM (EventM dc) = EventM (norm dc)++instance Num (EventM Int) where+ (+) x y = normEventM $ liftA2 (+) x y+ (-) x y = normEventM $ liftA2 (-) x y+ negate = fmap negate+ abs = normEventM . fmap abs+ signum = normEventM . fmap signum+ (*) = lotsOf+ where+ lotsOf :: EventM Int -> EventM Int -> EventM Int+ lotsOf x y = do n <- x+ sum <$> replicateM n y+ fromInteger = EventM . certainly . fromInteger++instance (Show a, Ord a) => Show (EventM a) where+ show (EventM dc) = showBars (decons (norm dc))++showBars :: Show a => [(a, Rational)] -> String+showBars xs = unlines (map (makeBar . scale) xs)+ where+ den = fromIntegral $ foldr lcm 1 (map (denominator . snd) xs)+ scale (x, r) = (x, floor (r * den)) -- should be integral anyway++ width = maximum (map (length . show . fst) xs)++ makeBar (x, n) = pad (show x) ++ ": " ++ replicate n '#'+ where+ pad s = s ++ replicate (width - length s) ' '++instance Applicative EventM where+ pure = return+ (<*>) = ap++-- | Gets a list of all the outcomes of the event and their associated probability.+-- You can be sure that the probabilities will all sum to 1, and that there will+-- only be one item in the list per outcome. It is possible that some of the outcomes+-- in the list will have zero probability. +outcomes :: Ord a => EventM a -> [(a, Rational)]+outcomes (EventM dc) = decons (norm dc)++-- | Makes an event that has an equal chance of taking on the value of each+-- entry in the list. Note that duplicates in the list are permitted and do+-- have an effect: @makeEvent [True, False]@ has a 50% chance of giving a True+-- result, but @makeEvent [True, True, False, False, False]@ only has a 40%+-- chance of giving a True result. If you do not want this behaviour, use+-- @makeEvent . nub@ to remove duplicates. +--+-- The result of passing the empty list is undefined.+makeEvent :: [a] -> EventM a+makeEvent = EventM . uniform++-- | Given a list of events and their associated probabilities, forms a+-- corresponding event. The probabilities must be non-negative. If the+-- probabilities do not sum to one, they are all scaled linearly so that their+-- sum is one. Duplicate items will have their probabilities added.+--+-- The result of passing the empty list, a list containing negative probabilities,+-- or a list where all the probabilities are zero is undefined.+makeEventProb :: (Ord a, Real prob) => [(a, prob)] -> EventM a+makeEventProb = EventM . norm . fromFreqs . map (second toRational)++-- | An event with a 50% chance of giving True, and a 50% chance of giving False.+coinToss :: EventM Bool+coinToss = makeEvent [True, False]++-- | Actually enacts the event and produces a single result according to the probabilities+-- in the @EventM a@ parameter.+enact :: EventM a -> IO a+enact (EventM dc) = selectP (toDouble dc) <$> randomRIO (0, 1)+ where+ toDouble :: T Rational a -> T Double a+ toDouble = Cons . map (second fromRational) . decons++-- | If the @EventM a@ parameter returns a result equal to the first parameter,+-- it is changed to be the second parameter; otherwise it is left untouched. For+-- example @replace 4 8 d4@ has an equal chance of producing the outcomes 1, 2,+-- 3 and 8, @replace 10 0 d10 == z10@, and @replace 10 20 d6 == d6@.+subst :: Eq a => a -> a -> EventM a -> EventM a+subst x y = fmap (\n -> if x == n then y else n)+++ -- a `lotsOf` b = [(bx,ap*bp) | (ax,ap)<-a,(bx,bp)<- sum (replicate ax b)]+ -- b `lotsOf` c = [(cx,bp*cp) | (bx,bp)<-b,(cx,cp)<- sum (replicate bx c)]+ -- a `lotsOf` (b `lotsOf` c)+ -- = [(bcx,ap*bcp) | (ax,ap)<-a,(bcx,bcp)<- sum (replicate ax [(cx,bp*cp) | (bx,bp)<-b,(cx,cp)<- sum (replicate bx c)])]
+ Numeric/Probability/Game/Query.hs view
@@ -0,0 +1,33 @@+-- | A module with functions for querying the probabilities of various outcomes.+module Numeric.Probability.Game.Query (chancePred, chanceRel, chanceTrue) where++import Control.Applicative ((<$>), liftA2)+import Data.Maybe (fromMaybe)++import Numeric.Probability.Game.Event (EventM, outcomes)++-- | Gets the probability that the outcome will satisfy the given predicate. For+-- example:+--+-- > chancePred (<= 2) d6 == 1/3 -- The chance of getting 2 or less on a d6+-- > chancePred even d6 == 1/2 -- The chance of rolling an event number on a d6+chancePred :: (a -> Bool) -> EventM a -> Rational+chancePred f e = fromMaybe 0 $ lookup True (outcomes (f <$> e))++-- | Gets the probability that the given relation will hold between the two events.+-- For example:+--+-- > chanceRel (==) d6 d6 == 1/6 -- The chance of rolling doubles on d6+-- > chanceRel (>) (2*d6) d12 -- The chance of beating a d12 with two d6+chanceRel :: (a -> a -> Bool) -> EventM a -> EventM a -> Rational+chanceRel f a b = chanceTrue (liftA2 f a b)++-- | Gets the probability that the given boolean-outcome event will give a True+-- outcome. For example:+--+-- > chanceTrue coinToss == 1/2+-- > chanceTrue ((== 3) <$> d6) == 1/6+--+-- (For the latter example, 'chancePred' is more concise.)+chanceTrue :: EventM Bool -> Rational+chanceTrue = chancePred id
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ game-probability.cabal view
@@ -0,0 +1,72 @@+Name: game-probability+Version: 1.0+Synopsis: Simple probability library for dice rolls and similar simple games of chance+-- A longer description of the package.+Description: A library designed to aid in easily calculating the+ probability of various outcomes of dice rolls. The+ central types are held in the+ "Numeric.Probability.Game.Event" module, the dice are+ defined in the "Numeric.Probability.Game.Dice" module,+ and the functions for calculating probabilities are in+ the "Numeric.Probability.Game.Query" module. Various+ examples are scattered throughout the library, but here+ are some more:+ .+ Evalulates the chance of a coin toss turning up heads:+ .+ > chanceTrue coinToss+ .+ Shows the chances of each outcome of rolling two six-sided dice, as a textual bar chart:+ .+ > show (2*d6)+ .+ The chance of getting an 18 when rolling 3 six-sided dice and rerolling on any total less than 8:+ .+ > chancePred (== 18) ((3*d6) `rerollOn` [3..7])+ .+ As a more complex example, this implements my memory/understanding of the original+ World of Darkness dice system+ (<http://en.wikipedia.org/wiki/Storytelling_System>).+ You roll a given number of 10-sided dice, rolling one+ extra for every 10 you score if you are specialised. The number of 1s on the+ original roll are subtracted from the total number of+ dice that equal or exceed the difficulty target:+ .+ > successes :: Int -> Int -> Bool -> EventM Int+ > successes target dice specialised+ > = do initial <- replicateM dice d10+ > extra <- if specialised+ > then replicateM (count (== 10) initial) d10+ > else return []+ > return (count (>= target) (initial ++ extra) - count (== 1) initial)+ > where+ > count f xs = length (filter f xs)+ .+ If only all RPGs specified their rules in Haskell!+ .+ See also the blog post on the design of the library: <http://chplib.wordpress.com/2010/08/13/nice-dice-in-haskell/>+License: BSD3+License-file: LICENSE+Author: Neil Brown+Maintainer: neil@twistedsquare.com+-- Copyright: ++Category: Math+Build-type: Simple++Cabal-version: >=1.6+++Library+ -- Modules exported by the library.+ Exposed-modules: Numeric.Probability.Game.Event+ Numeric.Probability.Game.Dice+ Numeric.Probability.Game.Query+ + -- Packages needed in order to build this package.+ Build-depends: base >= 4 && < 5, probability == 0.2.*, random == 1.0.*++ Extensions: GeneralizedNewtypeDeriving FlexibleInstances++ GHC-Options: -Wall+