File: Dice.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,064 kB
  • sloc: haskell: 45,636; makefile: 223
file content (261 lines) | stat: -rw-r--r-- 9,726 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | Representation of dice scaled with current level depth.
module Game.LambdaHack.Core.Dice
  ( -- * Frequency distribution for casting dice scaled with level depth
    Dice, AbsDepth(..), castDice, d, dL, z, zL, intToDice, minDice, maxDice
  , infsupDice, supDice, infDice, meanDice, reduceDice
    -- * Dice for rolling a pair of integer parameters representing coordinates.
  , DiceXY(..), supDiceXY, infDiceXY
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import Data.Binary

-- | Multiple dice rolls, some scaled with current level depth, in which case
-- the sum of all rolls is scaled in proportion to current depth
-- divided by maximal dungeon depth.
--
-- The simple dice should have positive number of rolls and number of sides.
--
-- The @Num@ instance doesn't have @abs@ nor @signum@ defined,
-- because the functions for computing infimum, supremum and mean dice
-- results would be too costly.
data Dice =
    DiceI Int
  | DiceD Int Int
  | DiceDL Int Int
  | DiceZ Int Int
  | DiceZL Int Int
  | DicePlus Dice Dice
  | DiceTimes Dice Dice
  | DiceNegate Dice
  | DiceMin Dice Dice
  | DiceMax Dice Dice
  deriving Eq

instance Show Dice where
  show = stripOuterParens . showDiceWithParens

stripOuterParens :: String -> String
stripOuterParens s@('(' : rest) = case uncons $ reverse rest of
  Just (')', middle) -> reverse middle
  _ -> s
stripOuterParens s = s

showDiceWithParens :: Dice -> String
showDiceWithParens = sh
 where
  sh dice1 = case dice1 of
    DiceI k -> show k
    DiceD n k -> show n ++ "d" ++ show k
    DiceDL n k -> show n ++ "dL" ++ show k
    DiceZ n k -> show n ++ "z" ++ show k
    DiceZL n k -> show n ++ "zL" ++ show k
    DicePlus d1 (DiceNegate d2) -> wrapInParens $ sh d1 ++ "-" ++ sh d2
    DicePlus (DiceNegate d1) d2 -> wrapInParens $ "-" ++ sh d1 ++ "+" ++ sh d2
    DicePlus d1 (DicePlus d2 d3) -> sh $ DicePlus (DicePlus d1 d2) d3
    DicePlus (DicePlus d1 d2) d3 ->
      wrapInParens $ stripOuterParens (sh $ DicePlus d1 d2) ++ "+" ++ sh d3
    DicePlus d1 d2 -> wrapInParens $ sh d1 ++ "+" ++ sh d2
    DiceTimes d1 d2 -> wrapInParens $ sh d1 ++ "*" ++ sh d2
    DiceNegate d1 -> wrapInParens $ "-" ++ sh d1
    DiceMin d1 d2 -> wrapInParens $ "min" ++ sh d1 ++ sh d2
    DiceMax d1 d2 -> wrapInParens $ "max" ++ sh d1 ++ sh d2

wrapInParens :: String -> String
wrapInParens "" = ""
wrapInParens t = "(" <> t <> ")"

instance Num Dice where
  d1 + d2 = DicePlus d1 d2
  d1 * d2 = DiceTimes d1 d2
  d1 - d2 = d1 + DiceNegate d2
  negate = DiceNegate
  abs = undefined  -- very costly to compute mean exactly
  signum = undefined  -- very costly to compute mean exactly
  fromInteger n = DiceI (fromInteger n)

-- | Absolute depth in the dungeon. When used for the maximum depth
-- of the whole dungeon, this can be different than dungeon size,
-- e.g., when the dungeon is branched, and it can even be different
-- than the length of the longest branch, if levels at some depths are missing.
newtype AbsDepth = AbsDepth Int
  deriving (Show, Eq, Ord, Binary)

-- | Cast dice scaled with current level depth. When scaling, we round up,
-- so that the value of @1 `dL` 1@ is @1@ even at the lowest level,
-- but so is the value of @1 `dL` depth@.
--
-- The implementation calls RNG as many times as there are dice rolls,
-- which is costly, so content should prefer to cast fewer dice
-- and then multiply them by a constant. If rounded results are not desired
-- (often they are, to limit the number of distinct item varieties
-- in inventory), another dice may be added to the result.
--
-- A different possible implementation, with dice represented as @Frequency@,
-- makes only one RNG call per dice, but due to lists lengths proportional
-- to the maximal value of the dice, it's is intractable for 1000d1000
-- and problematic already for 100d100.
castDice :: forall m. Monad m
         => ((Int, Int) -> m Int)
         -> AbsDepth -> AbsDepth -> Dice -> m Int
{-# INLINE castDice #-}
castDice randomR (AbsDepth lvlDepth) (AbsDepth maxDepth) dice = do
  let !_A = assert (lvlDepth >= 0 && lvlDepth <= maxDepth
                    `blame` "invalid depth for dice rolls"
                    `swith` (lvlDepth, maxDepth)) ()
      castNK n start k = if start == k then return $! n * k else do
          let f !acc 0 = return acc
              f acc count = do
                r <- randomR (start, k)
                f (acc + r) (count - 1)
          f 0 n
      scaleL k = (k * max 1 lvlDepth) `divUp` max 1 maxDepth
      castD :: Dice -> m Int
      castD dice1 = case dice1 of
        DiceI k -> return k
        DiceD n k -> castNK n 1 k
        DiceDL n k -> scaleL <$> castNK n 1 k
        DiceZ n k -> castNK n 0 (k - 1)
        DiceZL n k -> scaleL <$> castNK n 0 (k - 1)
        DicePlus d1 d2 -> do
          k1 <- castD d1
          k2 <- castD d2
          return $! k1 + k2
        DiceTimes d1 d2 -> do
          k1 <- castD d1
          k2 <- castD d2
          return $! k1 * k2
        DiceNegate d1 -> do
          k <- castD d1
          return $! negate k
        DiceMin d1 d2 -> do
          k1 <- castD d1
          k2 <- castD d2
          return $! min k1 k2
        DiceMax d1 d2 -> do
          k1 <- castD d1
          k2 <- castD d2
          return $! max k1 k2
  castD dice

-- | A die, rolled the given number of times. E.g., @1 `d` 2@ rolls 2-sided
-- die one time.
d :: Int -> Int -> Dice
d n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k))
        $ DiceD n k

-- | A die rolled the given number of times,
-- with the result scaled with dungeon level depth.
dL :: Int -> Int -> Dice
dL n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k))
         $ DiceDL n k

-- | A die, starting from zero, ending at one less than second argument,
-- rolled the given number of times. E.g., @1 `z` 1@ always rolls zero.
z :: Int -> Int -> Dice
z n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k))
        $ DiceZ n k

-- | A die, starting from zero, ending at one less than second argument,
-- rolled the given number of times,
-- with the result scaled with dungeon level depth.
zL :: Int -> Int -> Dice
zL n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k))
         $ DiceZL n k

intToDice :: Int -> Dice
intToDice = DiceI

minDice :: Dice -> Dice -> Dice
minDice = DiceMin

maxDice :: Dice -> Dice -> Dice
maxDice = DiceMax

-- | Minimal and maximal possible value of the dice.
--
-- @divUp@ in the implementation corresponds to @ceiling@,
-- applied to results of @meanDice@ elsewhere in the code,
-- and prevents treating 1d1-power effects (on shallow levels) as null effects.
infsupDice :: Dice -> (Int, Int)
infsupDice dice1 = case dice1 of
  DiceI k -> (k, k)
  DiceD n k -> (n, n * k)
  DiceDL n k -> (1, n * k)  -- bottom and top level considered
  DiceZ n k -> (0, n * (k - 1))
  DiceZL n k -> (0, n * (k - 1))  -- bottom and top level considered
  DicePlus d1 d2 ->
    let (infD1, supD1) = infsupDice d1
        (infD2, supD2) = infsupDice d2
    in (infD1 + infD2, supD1 + supD2)
  DiceTimes (DiceI k) d2 ->
    let (infD2, supD2) = infsupDice d2
    in if k >= 0 then (k * infD2, k * supD2) else (k * supD2, k * infD2)
  DiceTimes d1 (DiceI k) ->
    let (infD1, supD1) = infsupDice d1
    in if k >= 0 then (infD1 * k, supD1 * k) else (supD1 * k, infD1 * k)
  -- Multiplication other than the two cases above is unlikely, but here it is.
  DiceTimes d1 d2 ->
    let (infD1, supD1) = infsupDice d1
        (infD2, supD2) = infsupDice d2
        options = [infD1 * infD2, infD1 * supD2, supD1 * supD2, supD1 * infD2]
    in (minimum options, maximum options)
  DiceNegate d1 ->
    let (infD1, supD1) = infsupDice d1
    in (negate supD1, negate infD1)
  DiceMin d1 d2 ->
    let (infD1, supD1) = infsupDice d1
        (infD2, supD2) = infsupDice d2
    in (min infD1 infD2, min supD1 supD2)
  DiceMax d1 d2 ->
    let (infD1, supD1) = infsupDice d1
        (infD2, supD2) = infsupDice d2
    in (max infD1 infD2, max supD1 supD2)

-- | Maximal value of dice. The scaled part taken assuming median level.
supDice :: Dice -> Int
supDice = snd . infsupDice

-- | Minimal value of dice. The scaled part taken assuming median level.
infDice :: Dice -> Int
infDice = fst . infsupDice

-- | Mean value of dice. The scaled part taken assuming median level,
-- but not taking into account rounding up, and so too low, especially
-- for dice small compared to depth. To fix this, depth would need
-- to be taken as argument.
meanDice :: Dice -> Double
meanDice dice1 = case dice1 of
  DiceI k -> intToDouble k
  DiceD n k -> intToDouble (n * (k + 1)) / 2
  DiceDL n k -> intToDouble (n * (k + 1)) / 4
  DiceZ n k -> intToDouble (n * k) / 2
  DiceZL n k -> intToDouble (n * k) / 4
  DicePlus d1 d2 -> meanDice d1 + meanDice d2
  DiceTimes d1 d2 -> meanDice d1 * meanDice d2  -- I hope this is that simple
  DiceNegate d1 -> negate $ meanDice d1
  DiceMin d1 d2 -> min (meanDice d1) (meanDice d2)
    -- crude approximation, only exact if the distributions disjoint
  DiceMax d1 d2 -> max (meanDice d1) (meanDice d2)  -- crude approximation

reduceDice :: Dice -> Maybe Int
reduceDice d1 =
  let (infD1, supD1) = infsupDice d1
  in if infD1 == supD1 then Just infD1 else Nothing

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

-- | Maximal value of DiceXY.
supDiceXY :: DiceXY -> (Int, Int)
supDiceXY (DiceXY x y) = (supDice x, supDice y)

-- | Minimal value of DiceXY.
infDiceXY :: DiceXY -> (Int, Int)
infDiceXY (DiceXY x y) = (infDice x, infDice y)