From 9165ef5e4a998e40f7317d3c83857ba2bd3beb66 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 6 Dec 2021 11:22:14 +0000 Subject: [PATCH 01/58] start of some DiceStats stuff --- src/Tablebot/Plugin/Dice.hs | 20 +++- src/Tablebot/Plugin/DiceStats.hs | 167 +++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/Tablebot/Plugin/DiceStats.hs diff --git a/src/Tablebot/Plugin/Dice.hs b/src/Tablebot/Plugin/Dice.hs index 1f28370c..8d2cf5be 100644 --- a/src/Tablebot/Plugin/Dice.hs +++ b/src/Tablebot/Plugin/Dice.hs @@ -8,7 +8,25 @@ -- -- This plugin contains the neccessary parsers and stucture to get the AST for an -- expression that contains dice, as well as evaluate that expression. -module Tablebot.Plugin.Dice (evalExpr, Expr, PrettyShow (..), supportedFunctionsList, defaultRoll) where +module Tablebot.Plugin.Dice + ( evalExpr, + Expr, + PrettyShow (..), + supportedFunctions, + supportedFunctionsList, + defaultRoll, + Expr (..), + Term (..), + Func (..), + Negation (..), + Expo (..), + NumBase (..), + Base (..), + Die (..), + Dice (..), + DieOpRecur (..), + ) +where import Control.Monad (when) import Control.Monad.Exception (MonadException) diff --git a/src/Tablebot/Plugin/DiceStats.hs b/src/Tablebot/Plugin/DiceStats.hs new file mode 100644 index 00000000..027cd253 --- /dev/null +++ b/src/Tablebot/Plugin/DiceStats.hs @@ -0,0 +1,167 @@ +{-# OPTIONS_GHC -Wno-unused-imports #-} + +-- | +-- Module : Tablebot.Plugin.DiceStats +-- Description : Get statistics on particular expressions. +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- This plugin generates statistics based on the values of dice in given expressions +module Tablebot.Plugin.DiceStats where + +import Data.Map as M ((!)) +import Data.Maybe (fromMaybe) +import Data.Set as S (Set, fromList, singleton, toList, unions) +import Safe.Foldable (maximumMay, minimumMay) +import Tablebot.Plugin.Dice + +-- {- +--- Finding the range of an expression. + +-- TODO: make range return all possible values, repeated the number of times they would be +-- present (so it can be used for statistics) + +-- | Type class to find the range and bounds of a given value. +class Range a where + range :: a -> [Integer] + range = toList . range' + range' :: a -> Set Integer + maxVal :: a -> Integer + minVal :: a -> Integer + +instance Range Expr where + range' (Add t e) = S.fromList $ ((+) <$> range t) <*> range e + range' (Sub t e) = S.fromList $ ((-) <$> range t) <*> range e + range' (NoExpr t) = range' t + maxVal (Add t e) = maxVal t + maxVal e + maxVal (Sub t e) = maxVal t - minVal e + maxVal (NoExpr t) = maxVal t + minVal (Add t e) = minVal t + minVal e + minVal (Sub t e) = minVal t - maxVal e + minVal (NoExpr t) = minVal t + +instance Range Term where + range' (Multi f t) = S.fromList $ ((*) <$> range f) <*> range t + range' (Div f t) = S.fromList $ (div <$> range f) <*> filter (/= 0) (range t) + range' (NoTerm f) = range' f + maxVal (Multi f t) = maxVal f * maxVal t + maxVal (Div f t) = maxVal f `div` minVal t + maxVal (NoTerm f) = maxVal f + minVal (Multi f t) = minVal f * minVal t + minVal (Div f t) = minVal f `div` maxVal t + minVal (NoTerm f) = minVal f + +-- NOTE: this is unsafe since the function requested may not be defined +-- if using the dice parser functions, it'll be safe, but for all other uses, beware +instance Range Func where + range' (Func s n) = S.fromList $ (supportedFunctions M.! s) <$> range n + maxVal (Func "id" n) = maxVal n + maxVal f = maximum (range f) + minVal (Func "id" n) = minVal n + minVal f = minimum (range f) + +instance Range Negation where + range' (Neg expo) = S.fromList $ negate <$> range expo + range' (NoNeg expo) = range' expo + maxVal (NoNeg expo) = maxVal expo + maxVal (Neg expo) = negate $ minVal expo + minVal (NoNeg expo) = minVal expo + minVal (Neg expo) = negate $ maxVal expo + +instance Range Expo where + range' (NoExpo b) = range' b + range' (Expo b expo) = S.fromList $ ((^) <$> range b) <*> range expo + maxVal (NoExpo b) = maxVal b + maxVal (Expo b expo) = maxVal b ^ maxVal expo + minVal (NoExpo b) = minVal b + minVal (Expo b expo) = minVal b ^ minVal expo + +instance Range NumBase where + range' (Value i) = singleton i + range' (Paren e) = range' e + maxVal (Value i) = i + maxVal (Paren e) = maxVal e + minVal (Value i) = i + minVal (Paren e) = minVal e + +instance Range Base where + range' (NBase nb) = range' nb + range' (DiceBase dop) = range' dop + maxVal (NBase nb) = maxVal nb + maxVal (DiceBase dop) = maxVal dop + minVal (NBase nb) = minVal nb + minVal (DiceBase dop) = minVal dop + +instance Range Die where + -- range' (CustomDie is) = S.fromList is + range' (Die b) = S.fromList [1 .. (maxVal b)] + + -- maxVal (CustomDie is) = maximum is + maxVal (Die b) = maxVal b + + -- minVal (CustomDie is) = minimum is + minVal (Die _) = 1 + +-- TODO: check this more +instance Range Dice + +{- + range' d = S.unions $ fmap foldF counts + where + (counts, dr) = diceVals d + foldF' i js + | i < 1 = [] + | i == 1 = js + | otherwise = ((+) <$> dr) <*> foldF' (i - 1) js + foldF i = S.fromList $ foldF' i dr + maxVal d + | mxdr < 0 = fromMaybe 0 (minimumMay counts) * mxdr + | otherwise = fromMaybe 0 (maximumMay counts) * mxdr + where + (counts, dr) = diceVals d + mxdr = fromMaybe 0 $ maximumMay dr + minVal d + | mndr < 0 = fromMaybe 0 (maximumMay counts) * mndr + | otherwise = fromMaybe 0 (minimumMay counts) * mndr + where + (counts, dr) = diceVals d + mndr = fromMaybe 0 $ minimumMay dr + +type DieRange = [Integer] + +-- the tuple is the range of the number of dice, the current die range, and the total die range possible with the dice being used + +-- | Applies a given die operation to the current die ranges. The tuple given and returned +-- represents the number of dice, the current range of the die, and the base die range. +applyDieOpVal :: DieOpOption -> ([Integer], DieRange, DieRange) -> ([Integer], DieRange, DieRange) +applyDieOpVal (Reroll ro c l) t@(is, cdr, dr) + | any boolF cdr = (is, applyBoolF dr, dr) + | otherwise = t + where + boolF i' = compare i' l == c + applyBoolF = if ro then id else filter (not . boolF) +applyDieOpVal (DieOpOptionKD kd (Where o i)) (is, cdr, dr) + | any boolF cdr = ([0 .. maximum is], filter boolF cdr, dr) + | otherwise = (is, cdr, dr) + where + boolF i' = (if kd == Keep then id else not) $ compare i' i == o +applyDieOpVal (DieOpOptionKD kd lh) (is, cdr, dr) = (f (getValueLowHigh lh) <$> is, cdr, dr) + where + f (Just i) i' = if kd == Keep then min i i' else max 0 (i' - i) + f Nothing i' = i' + +-- | Get the number of dice and the die range of a given set of dice. +diceVals :: Dice -> ([Integer], DieRange) +diceVals (Dice b d mdor) = (filter (>= 0) counts, dr) + where + (counts, dr, _) = diceVals' mdor (filter (>= 0) (range b), dieVals, dieVals) + dieVals = range d + +-- | Helper function to iterate through all the `DieOpOption`s for a give set of dice. +diceVals' :: Maybe DieOpRecur -> ([Integer], DieRange, DieRange) -> ([Integer], DieRange, DieRange) +diceVals' Nothing t = t +diceVals' (Just (DieOpRecur doo mdor)) t = diceVals' mdor (applyDieOpVal doo t) + +-- -} From 1d63187064283710e430b5f970d5cd3e2f46e8c8 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 3 Jan 2022 21:45:49 +0000 Subject: [PATCH 02/58] covered the basic of the distributions, going to work on the graphing next --- package.yaml | 7 + src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 2 +- .../Plugins/Roll/{ => Dice}/DiceStats.hs | 133 +++++++++++++++--- src/Tablebot/Plugins/Roll/Plugin.hs | 11 +- stack.yaml | 11 ++ 5 files changed, 145 insertions(+), 19 deletions(-) rename src/Tablebot/Plugins/Roll/{ => Dice}/DiceStats.hs (57%) diff --git a/package.yaml b/package.yaml index fab30c5a..27968670 100644 --- a/package.yaml +++ b/package.yaml @@ -57,6 +57,13 @@ dependencies: - safe - edit-distance - unliftio +- Chart-diagrams +- diagrams-core +- diagrams-lib +- diagrams-postscript +- diagrams-svg +- SVGFonts +- graphviz library: source-dirs: src diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index cd708f90..3545496b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -8,7 +8,7 @@ -- -- Functions, type classes, and other utilities to evaluate dice values and -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), eval) where +module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), eval, evaluationException) where import Control.Monad (when) import Control.Monad.Exception (MonadException) diff --git a/src/Tablebot/Plugins/Roll/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs similarity index 57% rename from src/Tablebot/Plugins/Roll/DiceStats.hs rename to src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 027cd253..e19620ff 100644 --- a/src/Tablebot/Plugins/Roll/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -1,7 +1,5 @@ -{-# OPTIONS_GHC -Wno-unused-imports #-} - -- | --- Module : Tablebot.Plugin.DiceStats +-- Module : Tablebot.Plugins.Roll.Dice.DiceStats -- Description : Get statistics on particular expressions. -- License : MIT -- Maintainer : tagarople@gmail.com @@ -9,15 +7,115 @@ -- Portability : POSIX -- -- This plugin generates statistics based on the values of dice in given expressions -module Tablebot.Plugin.DiceStats where +module Tablebot.Plugins.Roll.Dice.DiceStats where + +import Control.Monad.Exception (MonadException) +import Data.List (genericTake) +import Data.Map as M +import Tablebot.Plugins.Roll.Dice.DiceData +import Tablebot.Plugins.Roll.Dice.DiceEval +import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) +import Tablebot.Plugins.Roll.Dice.DiceFunctions +import Tablebot.Plugins.Roll.Dice.DiceStatsBase + +-- combineDistributionsBinOp' :: (Monad m) => (Integer -> Integer -> Integer) -> m Distribution -> m Distribution -> m Distribution +-- combineDistributionsBinOp' f m m' = do +-- d <- m +-- d' <- m' +-- return $ combineDistributionsBinOp f d d' + +combineRangesBinOp :: (MonadException m, Range a, Range b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution +combineRangesBinOp f a b = do + d <- range a + d' <- range b + return $ combineDistributionsBinOp f d d' + +class Range a where + range :: MonadException m => a -> m Distribution + +-- maxValue :: MonadException m => a -> m Integer +-- minValue :: MonadException m => a -> m Integer + +instance Range Expr where + range (NoExpr t) = range t + range (Add t e) = combineRangesBinOp (+) t e + range (Sub t e) = combineRangesBinOp (-) t e + +instance Range Term where + range (NoTerm t) = range t + range (Multi t e) = combineRangesBinOp (*) t e + range (Div t e) = do + d <- range t + d' <- range e + return $ combineDistributionsBinOp div d (dropWhereDistribution (== 0) d') + +instance Range Negation where + range (Neg t) = do + d <- range t + return $ mapOverValue negate d + range (NoNeg t) = range t + +instance Range Expo where + range (NoExpo t) = range t + range (Expo t e) = do + d <- range t + d' <- range e + return $ combineDistributionsBinOp (^) d (dropWhereDistribution (>= 0) d') + +instance Range Func where + range (NoFunc t) = range t + range f@(Func _ _) = evaluationException "tried to find range of function" [prettyShow f] + +instance Range NumBase where + range (Value i) = return $ toDistribution [(i, 1)] + range (NBParen (Paren e)) = range e + +instance Range Base where + range (NBase nb) = range nb + range (DiceBase d) = range d -import Data.Map as M ((!)) -import Data.Maybe (fromMaybe) -import Data.Set as S (Set, fromList, singleton, toList, unions) -import Safe.Foldable (maximumMay, minimumMay) -import Tablebot.Plugin.Dice +instance Range Die where + range (LazyDie d) = range d + range (Die nb) = do + nbr <- range nb + let vcs = (\(hv, p) -> (toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]), p)) <$> fromDistribution nbr + return $ mergeWeightedDistributions vcs + range (CustomDie (LVBList es)) = do + exprs <- mapM range es + let l = fromIntegral $ length es + return $ mergeWeightedDistributions ((,1 / l) <$> exprs) + range cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] + +instance Range Dice where + range dice@(Dice b d mdor) = rangeDieOp dice + +rangeDieOp :: (MonadException m) => Dice -> m Distribution +rangeDieOp (Dice b d Nothing) = do + bDis <- range b + dDis <- range d + let endDises = do + (i, p) <- fromDistribution bDis + if i < 1 + then [] + else do + let v = Prelude.foldr1 (combineDistributionsBinOp (+)) (genericTake i (repeat dDis)) + [(v, p)] + return $ mergeWeightedDistributions endDises +rangeDieOp d = evaluationException "die modifiers are unimplemented" [prettyShow d] + +-- rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> (Distribution , Distribution ) -> m (Distribution , Distribution ) +-- rangeDieOpHelpKD kd (Where cmp i) ds@(counts,values) = do +-- iDis <- range i +-- let ds = do +-- (i,r) <- fromDistribution iDis +-- if doesActivate i then return () else r +-- r +-- where +-- vs = fromDistribution values +-- doesActivate i' = any (\(i'',_) -> applyCompare cmp i'' i') vs + +{- --- {- --- Finding the range of an expression. -- TODO: make range return all possible values, repeated the number of times they would be @@ -55,12 +153,12 @@ instance Range Term where -- NOTE: this is unsafe since the function requested may not be defined -- if using the dice parser functions, it'll be safe, but for all other uses, beware -instance Range Func where - range' (Func s n) = S.fromList $ (supportedFunctions M.! s) <$> range n - maxVal (Func "id" n) = maxVal n - maxVal f = maximum (range f) - minVal (Func "id" n) = minVal n - minVal f = minimum (range f) +-- instance Range Func where +-- range' (Func s n) = S.fromList $ (supportedFunctions M.! s) <$> range n +-- maxVal (Func "id" n) = maxVal n +-- maxVal f = maximum (range f) +-- minVal (Func "id" n) = minVal n +-- minVal f = minimum (range f) instance Range Negation where range' (Neg expo) = S.fromList $ negate <$> range expo @@ -164,4 +262,5 @@ diceVals' :: Maybe DieOpRecur -> ([Integer], DieRange, DieRange) -> ([Integer], diceVals' Nothing t = t diceVals' (Just (DieOpRecur doo mdor)) t = diceVals' mdor (applyDieOpVal doo t) --- -} +-} +-} diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index c1b50988..b19a7f4b 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -17,6 +17,7 @@ import Discord.Types (Message (messageAuthor)) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions (ListInteger (LIInteger, LIList)) +import Tablebot.Plugins.Roll.Dice.DiceStats (Range (range)) import Tablebot.Utility import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMention) import Tablebot.Utility.Parser (inlineCommandHelper) @@ -63,7 +64,7 @@ rollDiceParser = choice (try <$> options) -- | Basic command for rolling dice. rollDice :: Command -rollDice = Command "roll" rollDiceParser [] +rollDice = Command "roll" rollDiceParser [statsCommand] -- | Rolling dice inline. rollDiceInline :: InlineCommand @@ -130,6 +131,14 @@ gencharHelp = [] None +statsCommand :: Command +statsCommand = Command "stats" (parseComm statsCommand') [] + where + statsCommand' :: Expr -> Message -> DatabaseDiscord () + statsCommand' e m = do + range' <- range e + sendMessage m (T.pack $ show range') + -- | @rollPlugin@ assembles the command into a plugin. rollPlugin :: Plugin rollPlugin = diff --git a/stack.yaml b/stack.yaml index 6806babc..63154b8e 100644 --- a/stack.yaml +++ b/stack.yaml @@ -51,6 +51,17 @@ extra-deps: - duckling-0.2.0.0 - dependent-sum-0.7.1.0 - constraints-extras-0.3.1.0 +- Chart-diagrams-1.9.3 +- SVGFonts-1.7.0.1 +- diagrams-core-1.5.0 +- diagrams-lib-1.4.5.1 +- diagrams-postscript-1.5.1 +- diagrams-svg-1.4.3.1 +- svg-builder-0.1.1 +- active-0.2.0.15 +- dual-tree-0.2.3.0 +- monoid-extras-0.6.1 +- statestack-0.3 # Override default flag values for local packages and extra-deps # flags: {} From 8675355a6d03d6de01d61afd86d0ece858e1e477 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 3 Jan 2022 21:47:00 +0000 Subject: [PATCH 03/58] added base file for stats --- .../Plugins/Roll/Dice/DiceStatsBase.hs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs new file mode 100644 index 00000000..4b993dce --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -0,0 +1,72 @@ +-- | +-- Module : Tablebot.Plugins.Roll.Dice.DiceStatsBase +-- Description : The basics for dice stats +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- The basics for dice stats +module Tablebot.Plugins.Roll.Dice.DiceStatsBase + ( Distribution, + toDistribution, + fromDistribution, + combineDistributionsBinOp, + mergeDistributions, + mergeWeightedDistributions, + dropWhereDistribution, + mapOverValue, + -- getCount, + ) +where + +import Data.Map as M +import Diagrams +import Diagrams.Backend.SVG +import Diagrams.Backend.SVG.CmdLine +import Diagrams.Core +import Diagrams.Core.Names +import Diagrams.Prelude +import Diagrams.TwoD.GraphViz +import Graphics.SVGFonts + +newtype Distribution = Distribution (Map Integer Rational) + deriving (Show) + +normaliseDistribution :: Distribution -> Distribution +normaliseDistribution (Distribution m) = Distribution $ M.map (/ total) m + where + total = M.foldr (+) 0 m + +toDistribution :: [(Integer, Rational)] -> Distribution +toDistribution xs = normaliseDistribution $ Distribution $ fromListWith (+) xs + +fromDistribution :: Distribution -> [(Integer, Rational)] +fromDistribution (Distribution m) = toList m + +combineDistributionsBinOp :: (Integer -> Integer -> Integer) -> Distribution -> Distribution -> Distribution +combineDistributionsBinOp f (Distribution m) (Distribution m') = toDistribution $ combineFunc <$> d <*> d' + where + d = toList m + d' = toList m' + combineFunc (v, c) (v', c') = (f v v', c * c') + +mergeDistributions :: [Distribution] -> Distribution +mergeDistributions ds = normaliseDistribution $ Prelude.foldr helper (Distribution empty) ds + where + helper (Distribution d) (Distribution d') = Distribution $ unionWith (+) d d' + +mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution +mergeWeightedDistributions ds = mergeDistributions $ (\(Distribution m, p) -> Distribution $ M.map (* p) m) <$> ds + +dropWhereDistribution :: (Integer -> Bool) -> Distribution -> Distribution +dropWhereDistribution f (Distribution m) = normaliseDistribution $ Distribution $ M.filterWithKey (\k _ -> f k) m + +mapOverValue :: (Integer -> Integer) -> Distribution -> Distribution +mapOverValue f (Distribution m) = Distribution $ M.mapKeys f m + +-- mergeDistributions :: Distribution -> Distribution -> Distribution +-- mergeDistributions (Distribution d) (Distribution d') = normaliseDistribution $ Distribution $ unionWith (+) d d' + +toDiagrams :: Distribution -> Diagram B +toDiagrams = undefined From f370f8ddc3e511ec8c1b8a389ffd5b4969388d46 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 4 Jan 2022 22:19:44 +0000 Subject: [PATCH 04/58] fixed some formatting issues --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 2 +- src/Tablebot/Plugins/Roll/Plugin.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index e06bc013..deed072a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -28,7 +28,7 @@ where -- import Graphics.SVGFonts import Data.Bifunctor (Bifunctor (second)) -import Data.ByteString.Lazy qualified as B +import qualified Data.ByteString.Lazy as B import Data.ByteString.Char8 qualified as C import Data.Map as M import Data.Text qualified as T diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 594cb907..a0194d28 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,7 +15,7 @@ import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Lazy (toStrict) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) -import Data.Text qualified as T +import qualified Data.Text as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types (Channel (channelId), Message (messageAuthor, messageChannel)) @@ -160,7 +160,7 @@ statsCommand = Command "stats" (parseComm statsCommand') [] where se = prettyShow e sse = unpack se - msg d = let (modalOrder, mean, std) = getStats d in ("Here are the statistics for your dice (" <> sse <> ").\n Ten most common totals: " <> show (take 10 modalOrder) <> "\n Mean: " <> show mean <> "\n Standard deviation: " <> show std ) + msg d = let (modalOrder, mean, std) = getStats d in ("Here are the statistics for your dice (" <> sse <> ").\n Ten most common totals: " <> show (take 10 modalOrder) <> "\n Mean: " <> show mean <> "\n Standard deviation: " <> show std) -- sendMessage m (T.pack $ show range') From c689c57e64c94a040759f2fdd68a66b052242909 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 4 Jan 2022 22:28:05 +0000 Subject: [PATCH 05/58] formatting again --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index deed072a..49a27552 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -29,7 +29,7 @@ where import Data.Bifunctor (Bifunctor (second)) import qualified Data.ByteString.Lazy as B -import Data.ByteString.Char8 qualified as C +import qualified Data.ByteString.Char8 as C import Data.Map as M import Data.Text qualified as T import Diagrams (Diagram, dims2D, renderDia, mkWidth) From 70d9e9662aef2c53a231d0c8ba2fac0d977cef19 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 4 Jan 2022 22:31:46 +0000 Subject: [PATCH 06/58] cleaned up a bit --- .../Plugins/Roll/Dice/DiceStatsBase.hs | 18 +++--------------- src/Tablebot/Plugins/Roll/Plugin.hs | 3 +-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 49a27552..85daa63d 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -20,25 +20,14 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase ) where --- import Diagrams.Backend.SVG.CmdLine --- import Diagrams.Core --- import Diagrams.Core.Names --- import Diagrams.Prelude hiding (Renderable) --- import Diagrams.TwoD.GraphViz --- import Graphics.SVGFonts - -import Data.Bifunctor (Bifunctor (second)) +import Codec.Picture (PngSavable (encodePng)) import qualified Data.ByteString.Lazy as B -import qualified Data.ByteString.Char8 as C import Data.Map as M -import Data.Text qualified as T -import Diagrams (Diagram, dims2D, renderDia, mkWidth) --- import Diagrams.Backend.SVG +import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy -import Codec.Picture (PngSavable(encodePng)) newtype Distribution = Distribution (Map Integer Rational) deriving (Show) @@ -94,5 +83,4 @@ distributionRenderable t d = toRenderable $ do plot $ plotBars <$> bars ["values"] pts where pts :: [(Double, [Double])] - pts = (\(o, s) -> (fromInteger o, [ fromRational s])) <$> fromDistribution d - + pts = (\(o, s) -> (fromInteger o, [fromRational s])) <$> fromDistribution d diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index a0194d28..c4a2de81 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -9,7 +9,6 @@ -- A command that outputs the result of rolling the input dice. module Tablebot.Plugins.Roll.Plugin (rollPlugin) where -import Control.Exception (throwIO) import Control.Monad.Writer (MonadIO (liftIO), void) import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Lazy (toStrict) @@ -18,7 +17,7 @@ import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) -import Discord.Types (Channel (channelId), Message (messageAuthor, messageChannel)) +import Discord.Types (Message (messageAuthor, messageChannel)) import System.Timeout import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData From 16a8db67b93a9a6bc2ca4ab91ccf8263f22117c5 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 12:10:39 +0000 Subject: [PATCH 07/58] implemented rerolls and keep/drop dice operations --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 89 +++++++++++++++---- .../Plugins/Roll/Dice/DiceStatsBase.hs | 4 + 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index f0d93e92..268877cb 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} + -- | -- Module : Tablebot.Plugins.Roll.Dice.DiceStats -- Description : Get statistics on particular expressions. @@ -9,6 +11,7 @@ -- This plugin generates statistics based on the values of dice in given expressions module Tablebot.Plugins.Roll.Dice.DiceStats where +import Control.Monad (join) import Control.Monad.Exception (MonadException) import Data.List import Tablebot.Plugins.Roll.Dice.DiceData @@ -19,9 +22,10 @@ getStats :: Distribution -> ([Integer], Double, Double) getStats d = (modalOrder, fromRational mean, std) where vals = fromDistribution d - (mean, len) = Prelude.foldr (\(i, r) (a, c) -> (fromInteger i * r + a, c + 1)) (0, 0) vals + (mean, _, _) = Prelude.foldr (\(i, r) (a, c, nz) -> (fromInteger i * r + a, c + 1, nz + fromIntegral (fromEnum (r /= 0)))) (0, 0 :: Integer, 0 :: Integer) vals modalOrder = fst <$> sortBy (\(_, r) (_, r') -> compare r' r) vals - std = sqrt $ (1 / fromRational len) * sum ((\(i, r) -> fromRational (fromInteger i * r - mean) ** 2) <$> vals) + -- https://stats.stackexchange.com/a/295015 + std = sqrt $ fromRational $ sum ((\(x, w) -> w * (fromInteger x - mean) * (fromInteger x - mean)) <$> vals) combineRangesBinOp :: (MonadException m, Range a, Range b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution combineRangesBinOp f a b = do @@ -86,10 +90,12 @@ instance Range Dice where range (Dice b d mdor) = do b' <- range b d' <- range d - fromCountAndDie <$> rangeDieOp mdor (b', d') + mergeWeightedDistributions . (fromCountAndDie <$>) <$> rangeDieOp d' mdor [(b', d', 1)] + +type DiceCollection = (Distribution, Distribution, Rational) -fromCountAndDie :: (Distribution, Distribution) -> Distribution -fromCountAndDie (c, d) = mergeWeightedDistributions $ do +fromCountAndDie :: DiceCollection -> (Distribution, Rational) +fromCountAndDie (c, d, r) = (,r) . mergeWeightedDistributions $ do (i, p) <- fromDistribution c if i < 1 then [] @@ -97,15 +103,44 @@ fromCountAndDie (c, d) = mergeWeightedDistributions $ do let v = Prelude.foldr1 (combineDistributionsBinOp (+)) (genericTake i (repeat d)) [(v, p)] -rangeDieOp :: (MonadException m) => Maybe DieOpRecur -> (Distribution, Distribution) -> m (Distribution, Distribution) -rangeDieOp Nothing ds = return ds -rangeDieOp (Just (DieOpRecur doo mdor)) ds = rangeDieOp' doo ds >>= rangeDieOp mdor - -rangeDieOp' :: MonadException m => DieOpOption -> (Distribution, Distribution) -> m (Distribution, Distribution) -rangeDieOp' (DieOpOptionLazy o) ds = rangeDieOp' o ds +rangeDieOp :: (MonadException m) => Distribution -> Maybe DieOpRecur -> [DiceCollection] -> m [DiceCollection] +rangeDieOp _ Nothing ds = return ds +rangeDieOp die (Just (DieOpRecur doo mdor)) ds = rangeDieOp' die doo ds >>= rangeDieOp die mdor + +rangeDieOp' :: forall m. MonadException m => Distribution -> DieOpOption -> [DiceCollection] -> m [DiceCollection] +rangeDieOp' die (DieOpOptionLazy o) ds = rangeDieOp' die o ds +rangeDieOp' _ (DieOpOptionKD kd lhw) ds = rangeDieOpHelpKD kd lhw ds +rangeDieOp' die (Reroll rro cond lim) ds = do + limd <- range lim + join + <$> sequence + ( do + (v, p) <- fromDistribution limd + return + ( do + nd <- die' v + return + ( do + (c, d, cp) <- ds + let d' = + ( do + (dieV, dieP) <- fromDistribution d + if applyCompare cond dieV v + then [(nd, dieP)] + else [(toDistribution [(dieV, 1)], dieP)] + ) + return (c, mergeWeightedDistributions d', cp * p) + ) + ) + ) + where + die' :: forall m. (MonadException m) => Integer -> m Distribution + die' v + | rro = return die + | otherwise = let d = dropWhereDistribution (\i -> not $ applyCompare cond i v) die in if nullDistribution d then evaluationException "cannot reroll die infinitely; range is incorrect" [] else return d +-- if any (\i -> applyCompare cond v i . fst) -- rangeDieOp' (DieOpOptionKD kd lhw) ds = rangeDieOpHelpKD kd lhw ds --- rangeDieOp' (Reroll True cond lim) (c,d) = do -- rangeDieOp :: (MonadException m) => Dice -> m Distribution -- rangeDieOp (Dice b d Nothing) = do @@ -114,18 +149,36 @@ rangeDieOp' (DieOpOptionLazy o) ds = rangeDieOp' o ds -- return $ fromCountAndDie (bDis, dDis) -- rangeDieOp d = evaluationException "die modifiers are unimplemented" [prettyShow d] -rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> (Distribution, Distribution) -> m (Distribution, Distribution) --- rangeDieOpHelpKD _ (Where _ _) _ = evaluationException "keep/drop where is unsupported" [] -rangeDieOpHelpKD kd lhw (c, d) = do +rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> [DiceCollection] -> m [DiceCollection] +rangeDieOpHelpKD kd lhw ds = do let nb = getValueLowHigh lhw + repeatType = chooseType kd lhw case nb of Nothing -> evaluationException "keep/drop where is unsupported" [] Just nb' -> do nbd <- range nb' - return (kdFunc kd nbd, d) + return + ( do + (i, p) <- fromDistribution nbd + (c, d, dcp) <- ds + (ci, cp) <- fromDistribution c + let toKeep = getRemaining ci i + d' = repeatType (ci - toKeep) d + return (toDistribution [(toKeep, 1)], d', p * dcp * cp) + ) where - kdFunc Drop nbd = combineDistributionsBinOp (\a b -> max 0 (a - b)) c nbd - kdFunc Keep nbd = combineDistributionsBinOp min c nbd + getRemaining total value + | kd == Keep = min total value + | kd == Drop = max 0 (total - value) + repeatedM m i d + | i <= 0 = d + | otherwise = combineDistributionsBinOp m d (repeatedM m (i - 1) d) + repeatedMinimum = repeatedM min + repeatedMaximum = repeatedM max + chooseType Keep (High _) = repeatedMaximum + chooseType Keep (Low _) = repeatedMinimum + chooseType Drop (Low _) = repeatedMaximum + chooseType Drop (High _) = repeatedMaximum {- diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 85daa63d..6fd0ed6d 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -17,6 +17,7 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase dropWhereDistribution, mapOverValue, distributionByteString, + nullDistribution, ) where @@ -32,6 +33,9 @@ import Graphics.Rendering.Chart.Easy newtype Distribution = Distribution (Map Integer Rational) deriving (Show) +nullDistribution :: Distribution -> Bool +nullDistribution (Distribution m) = M.null m + normaliseDistribution :: Distribution -> Distribution normaliseDistribution (Distribution m) = Distribution $ M.map (/ total) m where From 8e28e57ad771d6e124fd01c8180504399d40bac2 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 12:16:46 +0000 Subject: [PATCH 08/58] filled some holes and removed some old stuff --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 179 +------------------- 1 file changed, 9 insertions(+), 170 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 268877cb..08b99c66 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-incomplete-patterns #-} - -- | -- Module : Tablebot.Plugins.Roll.Dice.DiceStats -- Description : Get statistics on particular expressions. @@ -139,23 +137,13 @@ rangeDieOp' die (Reroll rro cond lim) ds = do | rro = return die | otherwise = let d = dropWhereDistribution (\i -> not $ applyCompare cond i v) die in if nullDistribution d then evaluationException "cannot reroll die infinitely; range is incorrect" [] else return d --- if any (\i -> applyCompare cond v i . fst) --- rangeDieOp' (DieOpOptionKD kd lhw) ds = rangeDieOpHelpKD kd lhw ds - --- rangeDieOp :: (MonadException m) => Dice -> m Distribution --- rangeDieOp (Dice b d Nothing) = do --- bDis <- range b --- dDis <- range d --- return $ fromCountAndDie (bDis, dDis) --- rangeDieOp d = evaluationException "die modifiers are unimplemented" [prettyShow d] - rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> [DiceCollection] -> m [DiceCollection] rangeDieOpHelpKD kd lhw ds = do let nb = getValueLowHigh lhw - repeatType = chooseType kd lhw case nb of - Nothing -> evaluationException "keep/drop where is unsupported" [] + Nothing -> whereException Just nb' -> do + repeatType <- chooseType kd lhw nbd <- range nb' return ( do @@ -167,166 +155,17 @@ rangeDieOpHelpKD kd lhw ds = do return (toDistribution [(toKeep, 1)], d', p * dcp * cp) ) where + whereException = evaluationException "keep/drop where is unsupported" [] getRemaining total value | kd == Keep = min total value - | kd == Drop = max 0 (total - value) + | otherwise = max 0 (total - value) repeatedM m i d | i <= 0 = d | otherwise = combineDistributionsBinOp m d (repeatedM m (i - 1) d) repeatedMinimum = repeatedM min repeatedMaximum = repeatedM max - chooseType Keep (High _) = repeatedMaximum - chooseType Keep (Low _) = repeatedMinimum - chooseType Drop (Low _) = repeatedMaximum - chooseType Drop (High _) = repeatedMaximum - -{- - ---- Finding the range of an expression. - --- TODO: make range return all possible values, repeated the number of times they would be --- present (so it can be used for statistics) - --- | Type class to find the range and bounds of a given value. -class Range a where - range :: a -> [Integer] - range = toList . range' - range' :: a -> Set Integer - maxVal :: a -> Integer - minVal :: a -> Integer - -instance Range Expr where - range' (Add t e) = S.fromList $ ((+) <$> range t) <*> range e - range' (Sub t e) = S.fromList $ ((-) <$> range t) <*> range e - range' (NoExpr t) = range' t - maxVal (Add t e) = maxVal t + maxVal e - maxVal (Sub t e) = maxVal t - minVal e - maxVal (NoExpr t) = maxVal t - minVal (Add t e) = minVal t + minVal e - minVal (Sub t e) = minVal t - maxVal e - minVal (NoExpr t) = minVal t - -instance Range Term where - range' (Multi f t) = S.fromList $ ((*) <$> range f) <*> range t - range' (Div f t) = S.fromList $ (div <$> range f) <*> filter (/= 0) (range t) - range' (NoTerm f) = range' f - maxVal (Multi f t) = maxVal f * maxVal t - maxVal (Div f t) = maxVal f `div` minVal t - maxVal (NoTerm f) = maxVal f - minVal (Multi f t) = minVal f * minVal t - minVal (Div f t) = minVal f `div` maxVal t - minVal (NoTerm f) = minVal f - --- NOTE: this is unsafe since the function requested may not be defined --- if using the dice parser functions, it'll be safe, but for all other uses, beware --- instance Range Func where --- range' (Func s n) = S.fromList $ (supportedFunctions M.! s) <$> range n --- maxVal (Func "id" n) = maxVal n --- maxVal f = maximum (range f) --- minVal (Func "id" n) = minVal n --- minVal f = minimum (range f) - -instance Range Negation where - range' (Neg expo) = S.fromList $ negate <$> range expo - range' (NoNeg expo) = range' expo - maxVal (NoNeg expo) = maxVal expo - maxVal (Neg expo) = negate $ minVal expo - minVal (NoNeg expo) = minVal expo - minVal (Neg expo) = negate $ maxVal expo - -instance Range Expo where - range' (NoExpo b) = range' b - range' (Expo b expo) = S.fromList $ ((^) <$> range b) <*> range expo - maxVal (NoExpo b) = maxVal b - maxVal (Expo b expo) = maxVal b ^ maxVal expo - minVal (NoExpo b) = minVal b - minVal (Expo b expo) = minVal b ^ minVal expo - -instance Range NumBase where - range' (Value i) = singleton i - range' (Paren e) = range' e - maxVal (Value i) = i - maxVal (Paren e) = maxVal e - minVal (Value i) = i - minVal (Paren e) = minVal e - -instance Range Base where - range' (NBase nb) = range' nb - range' (DiceBase dop) = range' dop - maxVal (NBase nb) = maxVal nb - maxVal (DiceBase dop) = maxVal dop - minVal (NBase nb) = minVal nb - minVal (DiceBase dop) = minVal dop - -instance Range Die where - -- range' (CustomDie is) = S.fromList is - range' (Die b) = S.fromList [1 .. (maxVal b)] - - -- maxVal (CustomDie is) = maximum is - maxVal (Die b) = maxVal b - - -- minVal (CustomDie is) = minimum is - minVal (Die _) = 1 - --- TODO: check this more -instance Range Dice - -{- - range' d = S.unions $ fmap foldF counts - where - (counts, dr) = diceVals d - foldF' i js - | i < 1 = [] - | i == 1 = js - | otherwise = ((+) <$> dr) <*> foldF' (i - 1) js - foldF i = S.fromList $ foldF' i dr - maxVal d - | mxdr < 0 = fromMaybe 0 (minimumMay counts) * mxdr - | otherwise = fromMaybe 0 (maximumMay counts) * mxdr - where - (counts, dr) = diceVals d - mxdr = fromMaybe 0 $ maximumMay dr - minVal d - | mndr < 0 = fromMaybe 0 (maximumMay counts) * mndr - | otherwise = fromMaybe 0 (minimumMay counts) * mndr - where - (counts, dr) = diceVals d - mndr = fromMaybe 0 $ minimumMay dr - -type DieRange = [Integer] - --- the tuple is the range of the number of dice, the current die range, and the total die range possible with the dice being used - --- | Applies a given die operation to the current die ranges. The tuple given and returned --- represents the number of dice, the current range of the die, and the base die range. -applyDieOpVal :: DieOpOption -> ([Integer], DieRange, DieRange) -> ([Integer], DieRange, DieRange) -applyDieOpVal (Reroll ro c l) t@(is, cdr, dr) - | any boolF cdr = (is, applyBoolF dr, dr) - | otherwise = t - where - boolF i' = compare i' l == c - applyBoolF = if ro then id else filter (not . boolF) -applyDieOpVal (DieOpOptionKD kd (Where o i)) (is, cdr, dr) - | any boolF cdr = ([0 .. maximum is], filter boolF cdr, dr) - | otherwise = (is, cdr, dr) - where - boolF i' = (if kd == Keep then id else not) $ compare i' i == o -applyDieOpVal (DieOpOptionKD kd lh) (is, cdr, dr) = (f (getValueLowHigh lh) <$> is, cdr, dr) - where - f (Just i) i' = if kd == Keep then min i i' else max 0 (i' - i) - f Nothing i' = i' - --- | Get the number of dice and the die range of a given set of dice. -diceVals :: Dice -> ([Integer], DieRange) -diceVals (Dice b d mdor) = (filter (>= 0) counts, dr) - where - (counts, dr, _) = diceVals' mdor (filter (>= 0) (range b), dieVals, dieVals) - dieVals = range d - --- | Helper function to iterate through all the `DieOpOption`s for a give set of dice. -diceVals' :: Maybe DieOpRecur -> ([Integer], DieRange, DieRange) -> ([Integer], DieRange, DieRange) -diceVals' Nothing t = t -diceVals' (Just (DieOpRecur doo mdor)) t = diceVals' mdor (applyDieOpVal doo t) - --} --} + chooseType Keep (High _) = return repeatedMaximum + chooseType Keep (Low _) = return repeatedMinimum + chooseType Drop (Low _) = return repeatedMaximum + chooseType Drop (High _) = return repeatedMaximum + chooseType _ _ = whereException From d872e7abf6c91e0128a7dc98e0ae79dc1376da6f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 12:41:18 +0000 Subject: [PATCH 09/58] minor fixes and DiceStatsBase commenting --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 3 +- .../Plugins/Roll/Dice/DiceStatsBase.hs | 47 +++++++++++++++---- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 08b99c66..d58737ef 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -6,7 +6,8 @@ -- Stability : experimental -- Portability : POSIX -- --- This plugin generates statistics based on the values of dice in given expressions +-- This plugin generates statistics based on the values of dice in given +-- expressions. module Tablebot.Plugins.Roll.Dice.DiceStats where import Control.Monad (join) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 6fd0ed6d..9618ce1c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -6,13 +6,14 @@ -- Stability : experimental -- Portability : POSIX -- --- The basics for dice stats +-- The basics for dice stats. Functions for creating and manipulating +-- `Distribution`s. The constructor for `Distribution` is not exported to ensure +-- that a given `Distribution` is valid. module Tablebot.Plugins.Roll.Dice.DiceStatsBase ( Distribution, toDistribution, fromDistribution, combineDistributionsBinOp, - mergeDistributions, mergeWeightedDistributions, dropWhereDistribution, mapOverValue, @@ -23,56 +24,80 @@ where import Codec.Picture (PngSavable (encodePng)) import qualified Data.ByteString.Lazy as B -import Data.Map as M +import qualified Data.Map as M import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy -newtype Distribution = Distribution (Map Integer Rational) +-- | A wrapper type for mapping values to their probabilities. +-- +-- The constructor is not exported to ensure that the Distribution is always +-- valid. +newtype Distribution = Distribution (M.Map Integer Rational) deriving (Show) +-- | Check whether the distribution is empty. nullDistribution :: Distribution -> Bool nullDistribution (Distribution m) = M.null m +-- | Given a distribution, normalise the probabilities so that they sum to 1. normaliseDistribution :: Distribution -> Distribution -normaliseDistribution (Distribution m) = Distribution $ M.map (/ total) m +normaliseDistribution d@(Distribution m) = if M.null m then d else Distribution $ M.map (/ total) m where total = M.foldr (+) 0 m +-- | Turn a list of integer-rational tuples into a Distribution. Normalises so +-- that the Distribution is valid. toDistribution :: [(Integer, Rational)] -> Distribution -toDistribution xs = normaliseDistribution $ Distribution $ fromListWith (+) xs +toDistribution [(i, _)] = Distribution (M.singleton i 1) +toDistribution xs = normaliseDistribution $ Distribution $ M.fromListWith (+) xs +-- | Get the integer-rational tuples that represent a distribution. fromDistribution :: Distribution -> [(Integer, Rational)] -fromDistribution (Distribution m) = toList m +fromDistribution (Distribution m) = M.toList m +-- | Combine two distributions by applying the given function between every +-- element of each one, returning the resultant distribution. combineDistributionsBinOp :: (Integer -> Integer -> Integer) -> Distribution -> Distribution -> Distribution combineDistributionsBinOp f (Distribution m) (Distribution m') = toDistribution $ combineFunc <$> d <*> d' where - d = toList m - d' = toList m' + d = M.toList m + d' = M.toList m' combineFunc (v, c) (v', c') = (f v v', c * c') +-- | Merge all distributions by adding together the probabilities of any values +-- that are in multiple distributions, and normalising at the end. mergeDistributions :: [Distribution] -> Distribution mergeDistributions ds = normaliseDistribution $ Prelude.foldr helper (Distribution M.empty) ds where - helper (Distribution d) (Distribution d') = Distribution $ unionWith (+) d d' + helper (Distribution d) (Distribution d') = Distribution $ M.unionWith (+) d d' +-- | Merge all distributions according to a given weighting by multiplying the +-- probabilities in each distribution by the given weighting. Uses +-- `mergeDistributions`. mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution mergeWeightedDistributions ds = mergeDistributions $ (\(Distribution m, p) -> Distribution $ M.map (* p) m) <$> ds +-- | Drop all items in the distribution that fulfill the given function. dropWhereDistribution :: (Integer -> Bool) -> Distribution -> Distribution dropWhereDistribution f (Distribution m) = normaliseDistribution $ Distribution $ M.filterWithKey (\k _ -> f k) m +-- | Map over all the integer values, combining the probabilities that then map +-- to the same integer. mapOverValue :: (Integer -> Integer) -> Distribution -> Distribution mapOverValue f (Distribution m) = Distribution $ M.mapKeysWith (+) f m +-- | Get the ByteString representation of the given distribution, setting the +-- string as its title. distributionByteString :: String -> Distribution -> IO B.ByteString distributionByteString t d = encodePng . renderDia Rasterific opts <$> distributionDiagram t d where opts = RasterificOptions (dims2D 700 400) +-- | Get the Diagram representation of the given distribution, setting the +-- string as its title. distributionDiagram :: String -> Distribution -> IO (Diagram B) distributionDiagram t d = do defEnv <- defaultEnv (AlignmentFns id id) 700 400 @@ -80,6 +105,8 @@ distributionDiagram t d = do where r = distributionRenderable t d +-- | Get the Renderable representation of the given distribution, setting the +-- string as its title. distributionRenderable :: String -> Distribution -> Renderable () distributionRenderable t d = toRenderable $ do layout_title .= t From bb29c1743807cedc7b50db8eabd28ea1dd6c3031 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 13:27:55 +0000 Subject: [PATCH 10/58] more comments, and changed distribution functions so that they can error, requiring a rework of DiceStats --- package.yaml | 6 +- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 92 ++++++++++++------- .../Plugins/Roll/Dice/DiceStatsBase.hs | 24 +++-- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/package.yaml b/package.yaml index 50d2d6e2..9a6042f6 100644 --- a/package.yaml +++ b/package.yaml @@ -72,8 +72,8 @@ dependencies: library: source-dirs: src default-extensions: - - OverloadedStrings - ImportQualifiedPost + - OverloadedStrings - LambdaCase - EmptyDataDecls - FlexibleContexts @@ -105,6 +105,8 @@ executables: tablebot-exe: main: Main.hs source-dirs: app + default-extensions: + - ImportQualifiedPost ghc-options: - -threaded - -rtsopts @@ -116,6 +118,8 @@ tests: tablebot-test: main: Spec.hs source-dirs: test + default-extensions: + - ImportQualifiedPost ghc-options: - -threaded - -rtsopts diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index d58737ef..2c41171f 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -10,13 +10,16 @@ -- expressions. module Tablebot.Plugins.Roll.Dice.DiceStats where -import Control.Monad (join) +import Control.Monad import Control.Monad.Exception (MonadException) +import Data.Functor ((<&>)) import Data.List import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval import Tablebot.Plugins.Roll.Dice.DiceStatsBase +-- | Get the most common values, the mean, and the standard deviation of a given +-- distribution. getStats :: Distribution -> ([Integer], Double, Double) getStats d = (modalOrder, fromRational mean, std) where @@ -26,13 +29,23 @@ getStats d = (modalOrder, fromRational mean, std) -- https://stats.stackexchange.com/a/295015 std = sqrt $ fromRational $ sum ((\(x, w) -> w * (fromInteger x - mean) * (fromInteger x - mean)) <$> vals) +-- | Convenience wrapper for +-- `Tablebot.Plugins.Roll.Dice.DiceStatsBase.combineDistributionsBinOp`, which +-- gets the range of the given values then applies the function to the resultant +-- distributions. combineRangesBinOp :: (MonadException m, Range a, Range b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution combineRangesBinOp f a b = do d <- range a d' <- range b - return $ combineDistributionsBinOp f d d' + combineDistributionsBinOp f d d' +-- | Type class to get the overall range of a value. +-- +-- A `Tablebot.Plugins.Roll.Dice.DiceStatsBase.Distribution` is a map of values +-- to probabilities, and has a variety of functions that operate on them. class Range a where + -- | Try and get the `Distribution` of the given value, throwing a + -- `MonadException` on failure. range :: MonadException m => a -> m Distribution instance Range Expr where @@ -46,7 +59,9 @@ instance Range Term where range (Div t e) = do d <- range t d' <- range e - return $ combineDistributionsBinOp div d (dropWhereDistribution (== 0) d') + -- having 0 as a denominator is bad + d'' <- dropWhereDistribution (== 0) d' + combineDistributionsBinOp div d d'' instance Range Negation where range (Neg t) = do @@ -59,14 +74,15 @@ instance Range Expo where range (Expo t e) = do d <- range t d' <- range e - return $ combineDistributionsBinOp (^) d (dropWhereDistribution (>= 0) d') + d'' <- dropWhereDistribution (>= 0) d' + combineDistributionsBinOp (^) d d'' instance Range Func where range (NoFunc t) = range t range f@(Func _ _) = evaluationException "tried to find range of function" [prettyShow f] instance Range NumBase where - range (Value i) = return $ toDistribution [(i, 1)] + range (Value i) = toDistribution [(i, 1)] range (NBParen (Paren e)) = range e instance Range Base where @@ -77,30 +93,34 @@ instance Range Die where range (LazyDie d) = range d range (Die nb) = do nbr <- range nb - let vcs = (\(hv, p) -> (toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]), p)) <$> fromDistribution nbr - return $ mergeWeightedDistributions vcs + vcs <- sequence $ (\(hv, p) -> toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]) <&> (,p)) <$> fromDistribution nbr + mergeWeightedDistributions vcs range (CustomDie (LVBList es)) = do exprs <- mapM range es let l = fromIntegral $ length es - return $ mergeWeightedDistributions ((,1 / l) <$> exprs) + mergeWeightedDistributions ((,1 / l) <$> exprs) range cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] instance Range Dice where range (Dice b d mdor) = do b' <- range b d' <- range d - mergeWeightedDistributions . (fromCountAndDie <$>) <$> rangeDieOp d' mdor [(b', d', 1)] + dcs <- rangeDieOp d' mdor [(b', d', 1)] >>= sequence . (fromCountAndDie <$>) + mergeWeightedDistributions dcs type DiceCollection = (Distribution, Distribution, Rational) -fromCountAndDie :: DiceCollection -> (Distribution, Rational) -fromCountAndDie (c, d, r) = (,r) . mergeWeightedDistributions $ do - (i, p) <- fromDistribution c - if i < 1 - then [] - else do - let v = Prelude.foldr1 (combineDistributionsBinOp (+)) (genericTake i (repeat d)) - [(v, p)] +fromCountAndDie :: MonadException m => DiceCollection -> m (Distribution, Rational) +fromCountAndDie (c, d, r) = do + mwd <- sequence $ do + (i, p) <- fromDistribution c + if i < 1 + then [] + else do + let v = Prelude.foldr1 (\a b -> a >>= \a' -> b >>= \b' -> combineDistributionsBinOp (+) a' b') (genericTake i (repeat (return d))) + [v <&> (,p)] + mwd' <- mergeWeightedDistributions mwd + return (mwd', r) rangeDieOp :: (MonadException m) => Distribution -> Maybe DieOpRecur -> [DiceCollection] -> m [DiceCollection] rangeDieOp _ Nothing ds = return ds @@ -118,17 +138,21 @@ rangeDieOp' die (Reroll rro cond lim) ds = do return ( do nd <- die' v - return + sequence ( do (c, d, cp) <- ds - let d' = - ( do - (dieV, dieP) <- fromDistribution d - if applyCompare cond dieV v - then [(nd, dieP)] - else [(toDistribution [(dieV, 1)], dieP)] - ) - return (c, mergeWeightedDistributions d', cp * p) + let d' = do + (dieV, dieP) <- fromDistribution d + if applyCompare cond dieV v + then [return (nd, dieP)] + else [toDistribution [(dieV, 1)] <&> (,dieP)] + + return $ + sequence + d' + >>= ( mergeWeightedDistributions + >=> (\mwd -> return (c, mwd, cp * p)) + ) ) ) ) @@ -136,7 +160,11 @@ rangeDieOp' die (Reroll rro cond lim) ds = do die' :: forall m. (MonadException m) => Integer -> m Distribution die' v | rro = return die - | otherwise = let d = dropWhereDistribution (\i -> not $ applyCompare cond i v) die in if nullDistribution d then evaluationException "cannot reroll die infinitely; range is incorrect" [] else return d + | otherwise = do + d <- dropWhereDistribution (\i -> not $ applyCompare cond i v) die + if nullDistribution d + then evaluationException "cannot reroll die infinitely; range is incorrect" [] + else return d rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> [DiceCollection] -> m [DiceCollection] rangeDieOpHelpKD kd lhw ds = do @@ -146,14 +174,14 @@ rangeDieOpHelpKD kd lhw ds = do Just nb' -> do repeatType <- chooseType kd lhw nbd <- range nb' - return + sequence ( do (i, p) <- fromDistribution nbd (c, d, dcp) <- ds (ci, cp) <- fromDistribution c let toKeep = getRemaining ci i d' = repeatType (ci - toKeep) d - return (toDistribution [(toKeep, 1)], d', p * dcp * cp) + return $ d' >>= \d'' -> toDistribution [(toKeep, 1)] <&> (,d'',p * dcp * cp) ) where whereException = evaluationException "keep/drop where is unsupported" [] @@ -161,12 +189,12 @@ rangeDieOpHelpKD kd lhw ds = do | kd == Keep = min total value | otherwise = max 0 (total - value) repeatedM m i d - | i <= 0 = d - | otherwise = combineDistributionsBinOp m d (repeatedM m (i - 1) d) + | i <= 0 = return d + | otherwise = repeatedM m (i - 1) d >>= combineDistributionsBinOp m d repeatedMinimum = repeatedM min repeatedMaximum = repeatedM max chooseType Keep (High _) = return repeatedMaximum chooseType Keep (Low _) = return repeatedMinimum chooseType Drop (Low _) = return repeatedMaximum chooseType Drop (High _) = return repeatedMaximum - chooseType _ _ = whereException + chooseType _ _ = evaluationException "keep/drop where is unsupported" [] diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 9618ce1c..febd5017 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -23,6 +23,7 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase where import Codec.Picture (PngSavable (encodePng)) +import Control.Monad.Exception (MonadException) import qualified Data.ByteString.Lazy as B import qualified Data.Map as M import Diagrams (Diagram, dims2D, renderDia) @@ -30,6 +31,7 @@ import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy +import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) -- | A wrapper type for mapping values to their probabilities. -- @@ -43,15 +45,21 @@ nullDistribution :: Distribution -> Bool nullDistribution (Distribution m) = M.null m -- | Given a distribution, normalise the probabilities so that they sum to 1. -normaliseDistribution :: Distribution -> Distribution -normaliseDistribution d@(Distribution m) = if M.null m then d else Distribution $ M.map (/ total) m +-- +-- If the distribution is empty, an exception is thrown. +normaliseDistribution :: MonadException m => Distribution -> m Distribution +normaliseDistribution (Distribution m) = + if emptyDis + then evaluationException "cannot process empty distribution" [] + else return $ Distribution $ M.map (/ total) m where total = M.foldr (+) 0 m + emptyDis = M.null $ M.mapMaybe (\a -> if a == 0 then Nothing else Just a) m -- | Turn a list of integer-rational tuples into a Distribution. Normalises so -- that the Distribution is valid. -toDistribution :: [(Integer, Rational)] -> Distribution -toDistribution [(i, _)] = Distribution (M.singleton i 1) +toDistribution :: MonadException m => [(Integer, Rational)] -> m Distribution +toDistribution [(i, _)] = return $ Distribution (M.singleton i 1) toDistribution xs = normaliseDistribution $ Distribution $ M.fromListWith (+) xs -- | Get the integer-rational tuples that represent a distribution. @@ -60,7 +68,7 @@ fromDistribution (Distribution m) = M.toList m -- | Combine two distributions by applying the given function between every -- element of each one, returning the resultant distribution. -combineDistributionsBinOp :: (Integer -> Integer -> Integer) -> Distribution -> Distribution -> Distribution +combineDistributionsBinOp :: MonadException m => (Integer -> Integer -> Integer) -> Distribution -> Distribution -> m Distribution combineDistributionsBinOp f (Distribution m) (Distribution m') = toDistribution $ combineFunc <$> d <*> d' where d = M.toList m @@ -69,7 +77,7 @@ combineDistributionsBinOp f (Distribution m) (Distribution m') = toDistribution -- | Merge all distributions by adding together the probabilities of any values -- that are in multiple distributions, and normalising at the end. -mergeDistributions :: [Distribution] -> Distribution +mergeDistributions :: MonadException m => [Distribution] -> m Distribution mergeDistributions ds = normaliseDistribution $ Prelude.foldr helper (Distribution M.empty) ds where helper (Distribution d) (Distribution d') = Distribution $ M.unionWith (+) d d' @@ -77,11 +85,11 @@ mergeDistributions ds = normaliseDistribution $ Prelude.foldr helper (Distributi -- | Merge all distributions according to a given weighting by multiplying the -- probabilities in each distribution by the given weighting. Uses -- `mergeDistributions`. -mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution +mergeWeightedDistributions :: MonadException m => [(Distribution, Rational)] -> m Distribution mergeWeightedDistributions ds = mergeDistributions $ (\(Distribution m, p) -> Distribution $ M.map (* p) m) <$> ds -- | Drop all items in the distribution that fulfill the given function. -dropWhereDistribution :: (Integer -> Bool) -> Distribution -> Distribution +dropWhereDistribution :: MonadException m => (Integer -> Bool) -> Distribution -> m Distribution dropWhereDistribution f (Distribution m) = normaliseDistribution $ Distribution $ M.filterWithKey (\k _ -> f k) m -- | Map over all the integer values, combining the probabilities that then map From 34b779abecefb8c65ffa872d97bcc79c3eafb2f6 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 14:12:02 +0000 Subject: [PATCH 11/58] more comments, and moving do's around a lot --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 86 ++++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 2c41171f..b89ebdae 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -59,14 +59,12 @@ instance Range Term where range (Div t e) = do d <- range t d' <- range e - -- having 0 as a denominator is bad + -- having 0 as a denominator is disallowed d'' <- dropWhereDistribution (== 0) d' combineDistributionsBinOp div d d'' instance Range Negation where - range (Neg t) = do - d <- range t - return $ mapOverValue negate d + range (Neg t) = mapOverValue negate <$> range t range (NoNeg t) = range t instance Range Expo where @@ -74,12 +72,13 @@ instance Range Expo where range (Expo t e) = do d <- range t d' <- range e - d'' <- dropWhereDistribution (>= 0) d' + -- having negative values is disallowed + d'' <- dropWhereDistribution (< 0) d' combineDistributionsBinOp (^) d d'' instance Range Func where range (NoFunc t) = range t - range f@(Func _ _) = evaluationException "tried to find range of function" [prettyShow f] + range f@(Func _ _) = evaluationException "tried to find range of function, which is currently unsupported" [prettyShow f] instance Range NumBase where range (Value i) = toDistribution [(i, 1)] @@ -93,11 +92,18 @@ instance Range Die where range (LazyDie d) = range d range (Die nb) = do nbr <- range nb + -- for each possible nb value, create a (Distribution, Rational) pair + -- representing the distribution of the die and the probability of that + -- distribution coming up vcs <- sequence $ (\(hv, p) -> toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]) <&> (,p)) <$> fromDistribution nbr + -- then condense that into a single distribution mergeWeightedDistributions vcs range (CustomDie (LVBList es)) = do + -- get the distribution for each value in the custom die exprs <- mapM range es - let l = fromIntegral $ length es + let l = genericLength es + -- then merge all the distributions. each distribution is equally likely to + -- come up. mergeWeightedDistributions ((,1 / l) <$> exprs) range cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] @@ -108,8 +114,12 @@ instance Range Dice where dcs <- rangeDieOp d' mdor [(b', d', 1)] >>= sequence . (fromCountAndDie <$>) mergeWeightedDistributions dcs +-- | Aliased type to represent a singular instance of (number of dice, +-- distribution of a die, the probability of this occuring). type DiceCollection = (Distribution, Distribution, Rational) +-- | From a `DiceCollection`, get a distribution and the probability of that +-- distribution. fromCountAndDie :: MonadException m => DiceCollection -> m (Distribution, Rational) fromCountAndDie (c, d, r) = do mwd <- sequence $ do @@ -122,50 +132,66 @@ fromCountAndDie (c, d, r) = do mwd' <- mergeWeightedDistributions mwd return (mwd', r) +-- | Step by step apply `rangeDieOp'`, returning the current list of +-- `DiceCollection`s when `Nothing` is encountered. rangeDieOp :: (MonadException m) => Distribution -> Maybe DieOpRecur -> [DiceCollection] -> m [DiceCollection] rangeDieOp _ Nothing ds = return ds rangeDieOp die (Just (DieOpRecur doo mdor)) ds = rangeDieOp' die doo ds >>= rangeDieOp die mdor +-- | Apply a single `DieOpOption` to the current list of `DiceCollection`s. rangeDieOp' :: forall m. MonadException m => Distribution -> DieOpOption -> [DiceCollection] -> m [DiceCollection] rangeDieOp' die (DieOpOptionLazy o) ds = rangeDieOp' die o ds rangeDieOp' _ (DieOpOptionKD kd lhw) ds = rangeDieOpHelpKD kd lhw ds rangeDieOp' die (Reroll rro cond lim) ds = do limd <- range lim + -- join together the nested lists, as well as sequencing the + -- `MonadException` values join <$> sequence ( do - (v, p) <- fromDistribution limd + -- for each possible value of the limit, perform the rest of the input + (limitValue, limitProbability) <- fromDistribution limd return - ( do - nd <- die' v - sequence - ( do - (c, d, cp) <- ds - let d' = do - (dieV, dieP) <- fromDistribution d - if applyCompare cond dieV v - then [return (nd, dieP)] - else [toDistribution [(dieV, 1)] <&> (,dieP)] - - return $ - sequence - d' - >>= ( mergeWeightedDistributions - >=> (\mwd -> return (c, mwd, cp * p)) - ) - ) + ( -- get the new die distribution (only relevant on infinite + -- rerolls). if the die distribution is invalid (no values), an + -- exception is thrown here, as early as possible. + -- then, transform the given dice collections + die' limitValue >>= transformDiceCollections limitValue limitProbability ) ) where - die' :: forall m. (MonadException m) => Integer -> m Distribution - die' v + die' limitValue | rro = return die | otherwise = do - d <- dropWhereDistribution (\i -> not $ applyCompare cond i v) die + d <- dropWhereDistribution (\i -> not $ applyCompare cond i limitValue) die if nullDistribution d then evaluationException "cannot reroll die infinitely; range is incorrect" [] else return d + -- Go through all the dice values and conditionally perform the reroll. + conditionallyReroll dieDistribution limitValue newDie = sequence $ do + (dieV, dieP) <- fromDistribution dieDistribution + if applyCompare cond dieV limitValue + then [return (newDie, dieP)] + else [toDistribution [(dieV, 1)] <&> (,dieP)] + transformDiceCollections limitValue limitProbability newDie = + sequence + ( do + -- for each dice collection in the list, perform the + -- below. + (c, dieDistribution, cp) <- ds + -- return the list of dice collections, sequencing as + -- needed + return $ + conditionallyReroll dieDistribution limitValue newDie + >>= ( mergeWeightedDistributions + >=> ( \mwd -> return (c, mwd, cp * limitProbability) + ) + ) + ) + +-- | Apply a keep/drop dice operation using the given `LowHighWhere` onto the +-- list of `DiceCollection`s. rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> [DiceCollection] -> m [DiceCollection] rangeDieOpHelpKD kd lhw ds = do let nb = getValueLowHigh lhw @@ -196,5 +222,5 @@ rangeDieOpHelpKD kd lhw ds = do chooseType Keep (High _) = return repeatedMaximum chooseType Keep (Low _) = return repeatedMinimum chooseType Drop (Low _) = return repeatedMaximum - chooseType Drop (High _) = return repeatedMaximum + chooseType Drop (High _) = return repeatedMinimum chooseType _ _ = evaluationException "keep/drop where is unsupported" [] From b3c6203618074a043741a1de48c1ccd1e87d8ef2 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 16:06:21 +0000 Subject: [PATCH 12/58] letting some empty distributions be equal to a zero value --- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 2 +- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 57 ++++++++++--------- .../Plugins/Roll/Dice/DiceStatsBase.hs | 25 +++++--- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 4c51bc3d..902aec3a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -8,7 +8,7 @@ -- -- Functions, type classes, and other utilities to evaluate dice values and -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalList, evalInteger, evaluationException) where +module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalList, evalInteger, evaluationException, propagateException) where import Control.Monad (when) import Control.Monad.Exception (MonadException) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index b89ebdae..7d7d80ef 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -33,7 +33,7 @@ getStats d = (modalOrder, fromRational mean, std) -- `Tablebot.Plugins.Roll.Dice.DiceStatsBase.combineDistributionsBinOp`, which -- gets the range of the given values then applies the function to the resultant -- distributions. -combineRangesBinOp :: (MonadException m, Range a, Range b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution +combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyShow b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution combineRangesBinOp f a b = do d <- range a d' <- range b @@ -46,17 +46,20 @@ combineRangesBinOp f a b = do class Range a where -- | Try and get the `Distribution` of the given value, throwing a -- `MonadException` on failure. - range :: MonadException m => a -> m Distribution + range :: (MonadException m, PrettyShow a) => a -> m Distribution + range a = propagateException (prettyShow a) (range' a) + + range' :: (MonadException m, PrettyShow a) => a -> m Distribution instance Range Expr where - range (NoExpr t) = range t - range (Add t e) = combineRangesBinOp (+) t e - range (Sub t e) = combineRangesBinOp (-) t e + range' (NoExpr t) = range t + range' (Add t e) = combineRangesBinOp (+) t e + range' (Sub t e) = combineRangesBinOp (-) t e instance Range Term where - range (NoTerm t) = range t - range (Multi t e) = combineRangesBinOp (*) t e - range (Div t e) = do + range' (NoTerm t) = range t + range' (Multi t e) = combineRangesBinOp (*) t e + range' (Div t e) = do d <- range t d' <- range e -- having 0 as a denominator is disallowed @@ -64,12 +67,12 @@ instance Range Term where combineDistributionsBinOp div d d'' instance Range Negation where - range (Neg t) = mapOverValue negate <$> range t - range (NoNeg t) = range t + range' (Neg t) = mapOverValue negate <$> range t + range' (NoNeg t) = range t instance Range Expo where - range (NoExpo t) = range t - range (Expo t e) = do + range' (NoExpo t) = range t + range' (Expo t e) = do d <- range t d' <- range e -- having negative values is disallowed @@ -77,20 +80,20 @@ instance Range Expo where combineDistributionsBinOp (^) d d'' instance Range Func where - range (NoFunc t) = range t - range f@(Func _ _) = evaluationException "tried to find range of function, which is currently unsupported" [prettyShow f] + range' (NoFunc t) = range t + range' f@(Func _ _) = evaluationException "tried to find range of function, which is currently unsupported" [prettyShow f] instance Range NumBase where - range (Value i) = toDistribution [(i, 1)] - range (NBParen (Paren e)) = range e + range' (Value i) = toDistribution [(i, 1)] + range' (NBParen (Paren e)) = range e instance Range Base where - range (NBase nb) = range nb - range (DiceBase d) = range d + range' (NBase nb) = range nb + range' (DiceBase d) = range d instance Range Die where - range (LazyDie d) = range d - range (Die nb) = do + range' (LazyDie d) = range d + range' (Die nb) = do nbr <- range nb -- for each possible nb value, create a (Distribution, Rational) pair -- representing the distribution of the die and the probability of that @@ -98,17 +101,17 @@ instance Range Die where vcs <- sequence $ (\(hv, p) -> toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]) <&> (,p)) <$> fromDistribution nbr -- then condense that into a single distribution mergeWeightedDistributions vcs - range (CustomDie (LVBList es)) = do + range' (CustomDie (LVBList es)) = do -- get the distribution for each value in the custom die exprs <- mapM range es let l = genericLength es -- then merge all the distributions. each distribution is equally likely to -- come up. mergeWeightedDistributions ((,1 / l) <$> exprs) - range cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] + range' cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] instance Range Dice where - range (Dice b d mdor) = do + range' (Dice b d mdor) = do b' <- range b d' <- range d dcs <- rangeDieOp d' mdor [(b', d', 1)] >>= sequence . (fromCountAndDie <$>) @@ -125,11 +128,11 @@ fromCountAndDie (c, d, r) = do mwd <- sequence $ do (i, p) <- fromDistribution c if i < 1 - then [] + then [toDistribution [(0, 1)] <&> (,p)] else do - let v = Prelude.foldr1 (\a b -> a >>= \a' -> b >>= \b' -> combineDistributionsBinOp (+) a' b') (genericTake i (repeat (return d))) + let v = catchEmptyDistribution $ Prelude.foldr1 (\a b -> a >>= \a' -> b >>= \b' -> combineDistributionsBinOp (+) a' b') (genericTake i (repeat (return d))) [v <&> (,p)] - mwd' <- mergeWeightedDistributions mwd + mwd' <- catchEmptyDistribution $ mergeWeightedDistributions mwd return (mwd', r) -- | Step by step apply `rangeDieOp'`, returning the current list of @@ -216,7 +219,7 @@ rangeDieOpHelpKD kd lhw ds = do | otherwise = max 0 (total - value) repeatedM m i d | i <= 0 = return d - | otherwise = repeatedM m (i - 1) d >>= combineDistributionsBinOp m d + | otherwise = repeatedM m (i - 1) d >>= \d' -> combineDistributionsBinOp m d d' repeatedMinimum = repeatedM min repeatedMaximum = repeatedM max chooseType Keep (High _) = return repeatedMaximum diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index febd5017..f066b9d7 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -11,6 +11,7 @@ -- that a given `Distribution` is valid. module Tablebot.Plugins.Roll.Dice.DiceStatsBase ( Distribution, + catchEmptyDistribution, toDistribution, fromDistribution, combineDistributionsBinOp, @@ -24,14 +25,15 @@ where import Codec.Picture (PngSavable (encodePng)) import Control.Monad.Exception (MonadException) -import qualified Data.ByteString.Lazy as B -import qualified Data.Map as M +import Data.ByteString.Lazy qualified as B +import Data.Map qualified as M import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) +import Tablebot.Utility.Exception -- | A wrapper type for mapping values to their probabilities. -- @@ -42,24 +44,31 @@ newtype Distribution = Distribution (M.Map Integer Rational) -- | Check whether the distribution is empty. nullDistribution :: Distribution -> Bool -nullDistribution (Distribution m) = M.null m +nullDistribution (Distribution m) = M.null $ M.mapMaybe (\a -> if a == 0 then Nothing else Just a) m -- | Given a distribution, normalise the probabilities so that they sum to 1. -- -- If the distribution is empty, an exception is thrown. normaliseDistribution :: MonadException m => Distribution -> m Distribution -normaliseDistribution (Distribution m) = - if emptyDis - then evaluationException "cannot process empty distribution" [] +normaliseDistribution d@(Distribution m) = + if nullDistribution d + then evaluationException "empty distribution" [] else return $ Distribution $ M.map (/ total) m where total = M.foldr (+) 0 m - emptyDis = M.null $ M.mapMaybe (\a -> if a == 0 then Nothing else Just a) m + +catchEmptyDistribution :: MonadException m => m Distribution -> m Distribution +catchEmptyDistribution md = + catchBot + md + ( \case + EvaluationException "empty distribution" _ -> return (Distribution (M.singleton 0 1)) + e -> throwBot e + ) -- | Turn a list of integer-rational tuples into a Distribution. Normalises so -- that the Distribution is valid. toDistribution :: MonadException m => [(Integer, Rational)] -> m Distribution -toDistribution [(i, _)] = return $ Distribution (M.singleton i 1) toDistribution xs = normaliseDistribution $ Distribution $ M.fromListWith (+) xs -- | Get the integer-rational tuples that represent a distribution. From 660a9a53074c91511780b415ff20ce4de047cd57 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 16:06:43 +0000 Subject: [PATCH 13/58] ormolu --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index f066b9d7..3f954db1 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -25,8 +25,8 @@ where import Codec.Picture (PngSavable (encodePng)) import Control.Monad.Exception (MonadException) -import Data.ByteString.Lazy qualified as B -import Data.Map qualified as M +import qualified Data.ByteString.Lazy as B +import qualified Data.Map as M import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) From 93df68be8ad8c0fe31cb3f5332f8543cc1507ee0 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 18:22:28 +0000 Subject: [PATCH 14/58] moved to library version of Distribution --- package.yaml | 1 + src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 110 ++++++++---------- .../Plugins/Roll/Dice/DiceStatsBase.hs | 96 +++------------ stack.yaml | 4 + 4 files changed, 72 insertions(+), 139 deletions(-) diff --git a/package.yaml b/package.yaml index 9a6042f6..dd6c3d0b 100644 --- a/package.yaml +++ b/package.yaml @@ -68,6 +68,7 @@ dependencies: - graphviz - JuicyPixels - regex-pcre +- distribution library: source-dirs: src diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 7d7d80ef..45697638 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -12,6 +12,7 @@ module Tablebot.Plugins.Roll.Dice.DiceStats where import Control.Monad import Control.Monad.Exception (MonadException) +import Data.Distribution as D hiding (Distribution, fromList) import Data.Functor ((<&>)) import Data.List import Tablebot.Plugins.Roll.Dice.DiceData @@ -21,23 +22,18 @@ import Tablebot.Plugins.Roll.Dice.DiceStatsBase -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. getStats :: Distribution -> ([Integer], Double, Double) -getStats d = (modalOrder, fromRational mean, std) +getStats d = (modalOrder, expectation d, standardDeviation d) where - vals = fromDistribution d - (mean, _, _) = Prelude.foldr (\(i, r) (a, c, nz) -> (fromInteger i * r + a, c + 1, nz + fromIntegral (fromEnum (r /= 0)))) (0, 0 :: Integer, 0 :: Integer) vals + vals = toList d modalOrder = fst <$> sortBy (\(_, r) (_, r') -> compare r' r) vals - -- https://stats.stackexchange.com/a/295015 - std = sqrt $ fromRational $ sum ((\(x, w) -> w * (fromInteger x - mean) * (fromInteger x - mean)) <$> vals) - --- | Convenience wrapper for --- `Tablebot.Plugins.Roll.Dice.DiceStatsBase.combineDistributionsBinOp`, which --- gets the range of the given values then applies the function to the resultant --- distributions. -combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyShow b) => (Integer -> Integer -> Integer) -> a -> b -> m Distribution + +-- | Convenience wrapper which gets the range of the given values then applies +-- the function to the resultant distributions. +combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyShow b) => (Distribution -> Distribution -> Distribution) -> a -> b -> m Distribution combineRangesBinOp f a b = do d <- range a d' <- range b - combineDistributionsBinOp f d d' + return $ f d d' -- | Type class to get the overall range of a value. -- @@ -63,11 +59,11 @@ instance Range Term where d <- range t d' <- range e -- having 0 as a denominator is disallowed - d'' <- dropWhereDistribution (== 0) d' - combineDistributionsBinOp div d d'' + d'' <- ifInvalidThrow $ assuming (/= 0) d' + return $ combineWith div d d'' instance Range Negation where - range' (Neg t) = mapOverValue negate <$> range t + range' (Neg t) = select negate <$> range t range' (NoNeg t) = range t instance Range Expo where @@ -76,15 +72,15 @@ instance Range Expo where d <- range t d' <- range e -- having negative values is disallowed - d'' <- dropWhereDistribution (< 0) d' - combineDistributionsBinOp (^) d d'' + d'' <- ifInvalidThrow $ assuming (>= 0) d' + return $ combineWith (^) d d'' instance Range Func where range' (NoFunc t) = range t range' f@(Func _ _) = evaluationException "tried to find range of function, which is currently unsupported" [prettyShow f] instance Range NumBase where - range' (Value i) = toDistribution [(i, 1)] + range' (Value i) = return $ always i range' (NBParen (Paren e)) = range e instance Range Base where @@ -98,16 +94,16 @@ instance Range Die where -- for each possible nb value, create a (Distribution, Rational) pair -- representing the distribution of the die and the probability of that -- distribution coming up - vcs <- sequence $ (\(hv, p) -> toDistribution ((,1 / fromIntegral hv) <$> [1 .. hv]) <&> (,p)) <$> fromDistribution nbr + let vcs = (\(hv, p) -> (fromList ((,1 / fromIntegral hv) <$> [1 .. hv]), p)) <$> toList nbr -- then condense that into a single distribution - mergeWeightedDistributions vcs + ifInvalidThrow $ mergeWeightedDistributions vcs range' (CustomDie (LVBList es)) = do -- get the distribution for each value in the custom die exprs <- mapM range es let l = genericLength es -- then merge all the distributions. each distribution is equally likely to -- come up. - mergeWeightedDistributions ((,1 / l) <$> exprs) + ifInvalidThrow $ mergeWeightedDistributions ((,1 / l) <$> exprs) range' cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] instance Range Dice where @@ -115,7 +111,7 @@ instance Range Dice where b' <- range b d' <- range d dcs <- rangeDieOp d' mdor [(b', d', 1)] >>= sequence . (fromCountAndDie <$>) - mergeWeightedDistributions dcs + return $ mergeWeightedDistributions dcs -- | Aliased type to represent a singular instance of (number of dice, -- distribution of a die, the probability of this occuring). @@ -125,15 +121,15 @@ type DiceCollection = (Distribution, Distribution, Rational) -- distribution. fromCountAndDie :: MonadException m => DiceCollection -> m (Distribution, Rational) fromCountAndDie (c, d, r) = do - mwd <- sequence $ do - (i, p) <- fromDistribution c - if i < 1 - then [toDistribution [(0, 1)] <&> (,p)] - else do - let v = catchEmptyDistribution $ Prelude.foldr1 (\a b -> a >>= \a' -> b >>= \b' -> combineDistributionsBinOp (+) a' b') (genericTake i (repeat (return d))) - [v <&> (,p)] - mwd' <- catchEmptyDistribution $ mergeWeightedDistributions mwd - return (mwd', r) + let mwd = do + (i, p) <- toList c + return $ + if i < 1 + then (fromList [(0, 1)], p) + else do + let v = sum (genericTake i (repeat d)) + (v, p) + return (mergeWeightedDistributions mwd, r) -- | Step by step apply `rangeDieOp'`, returning the current list of -- `DiceCollection`s when `Nothing` is encountered. @@ -153,45 +149,35 @@ rangeDieOp' die (Reroll rro cond lim) ds = do <$> sequence ( do -- for each possible value of the limit, perform the rest of the input - (limitValue, limitProbability) <- fromDistribution limd + (limitValue, limitProbability) <- toList limd return ( -- get the new die distribution (only relevant on infinite -- rerolls). if the die distribution is invalid (no values), an -- exception is thrown here, as early as possible. -- then, transform the given dice collections - die' limitValue >>= transformDiceCollections limitValue limitProbability + die' limitValue <&> transformDiceCollections limitValue limitProbability ) ) where die' limitValue | rro = return die - | otherwise = do - d <- dropWhereDistribution (\i -> not $ applyCompare cond i limitValue) die - if nullDistribution d - then evaluationException "cannot reroll die infinitely; range is incorrect" [] - else return d + | otherwise = let d = assuming (\i -> not $ applyCompare cond i limitValue) die in ifInvalidThrow d -- Go through all the dice values and conditionally perform the reroll. - conditionallyReroll dieDistribution limitValue newDie = sequence $ do - (dieV, dieP) <- fromDistribution dieDistribution + conditionallyReroll dieDistribution limitValue newDie = do + (dieV, dieP) <- toList dieDistribution if applyCompare cond dieV limitValue - then [return (newDie, dieP)] - else [toDistribution [(dieV, 1)] <&> (,dieP)] + then [(newDie, dieP)] + else [(fromList [(dieV, 1)], dieP)] transformDiceCollections limitValue limitProbability newDie = - sequence - ( do - -- for each dice collection in the list, perform the - -- below. - (c, dieDistribution, cp) <- ds - -- return the list of dice collections, sequencing as - -- needed - return $ - conditionallyReroll dieDistribution limitValue newDie - >>= ( mergeWeightedDistributions - >=> ( \mwd -> return (c, mwd, cp * limitProbability) - ) - ) - ) + do + -- for each dice collection in the list, perform the + -- below. + (c, dieDistribution, cp) <- ds + -- return the list of dice collections, sequencing as + -- needed + let mwd = mergeWeightedDistributions $ conditionallyReroll dieDistribution limitValue newDie + return (c, mwd, cp * limitProbability) -- | Apply a keep/drop dice operation using the given `LowHighWhere` onto the -- list of `DiceCollection`s. @@ -203,14 +189,14 @@ rangeDieOpHelpKD kd lhw ds = do Just nb' -> do repeatType <- chooseType kd lhw nbd <- range nb' - sequence + return ( do - (i, p) <- fromDistribution nbd + (i, p) <- toList nbd (c, d, dcp) <- ds - (ci, cp) <- fromDistribution c + (ci, cp) <- toList c let toKeep = getRemaining ci i d' = repeatType (ci - toKeep) d - return $ d' >>= \d'' -> toDistribution [(toKeep, 1)] <&> (,d'',p * dcp * cp) + return (fromList [(toKeep, 1 :: Rational)], d', p * dcp * cp) ) where whereException = evaluationException "keep/drop where is unsupported" [] @@ -218,8 +204,8 @@ rangeDieOpHelpKD kd lhw ds = do | kd == Keep = min total value | otherwise = max 0 (total - value) repeatedM m i d - | i <= 0 = return d - | otherwise = repeatedM m (i - 1) d >>= \d' -> combineDistributionsBinOp m d d' + | i <= 0 = d + | otherwise = combineWith m d $ repeatedM m (i - 1) d repeatedMinimum = repeatedM min repeatedMaximum = repeatedM max chooseType Keep (High _) = return repeatedMaximum diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 3f954db1..604a8805 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -7,104 +7,46 @@ -- Portability : POSIX -- -- The basics for dice stats. Functions for creating and manipulating --- `Distribution`s. The constructor for `Distribution` is not exported to ensure --- that a given `Distribution` is valid. +-- `Distribution`s. module Tablebot.Plugins.Roll.Dice.DiceStatsBase ( Distribution, - catchEmptyDistribution, - toDistribution, - fromDistribution, - combineDistributionsBinOp, + fromList, mergeWeightedDistributions, - dropWhereDistribution, - mapOverValue, + ifInvalidThrow, distributionByteString, - nullDistribution, ) where import Codec.Picture (PngSavable (encodePng)) import Control.Monad.Exception (MonadException) import qualified Data.ByteString.Lazy as B -import qualified Data.Map as M +import qualified Data.Distribution as D import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) -import Tablebot.Utility.Exception -- | A wrapper type for mapping values to their probabilities. --- --- The constructor is not exported to ensure that the Distribution is always --- valid. -newtype Distribution = Distribution (M.Map Integer Rational) - deriving (Show) - --- | Check whether the distribution is empty. -nullDistribution :: Distribution -> Bool -nullDistribution (Distribution m) = M.null $ M.mapMaybe (\a -> if a == 0 then Nothing else Just a) m - --- | Given a distribution, normalise the probabilities so that they sum to 1. --- --- If the distribution is empty, an exception is thrown. -normaliseDistribution :: MonadException m => Distribution -> m Distribution -normaliseDistribution d@(Distribution m) = - if nullDistribution d - then evaluationException "empty distribution" [] - else return $ Distribution $ M.map (/ total) m - where - total = M.foldr (+) 0 m - -catchEmptyDistribution :: MonadException m => m Distribution -> m Distribution -catchEmptyDistribution md = - catchBot - md - ( \case - EvaluationException "empty distribution" _ -> return (Distribution (M.singleton 0 1)) - e -> throwBot e - ) +type Distribution = D.Distribution Integer --- | Turn a list of integer-rational tuples into a Distribution. Normalises so --- that the Distribution is valid. -toDistribution :: MonadException m => [(Integer, Rational)] -> m Distribution -toDistribution xs = normaliseDistribution $ Distribution $ M.fromListWith (+) xs +-- | Convenient way to set the types being used so that warnings don't pop up. +fromList :: [(Integer, Rational)] -> Distribution +fromList = D.fromList --- | Get the integer-rational tuples that represent a distribution. -fromDistribution :: Distribution -> [(Integer, Rational)] -fromDistribution (Distribution m) = M.toList m - --- | Combine two distributions by applying the given function between every --- element of each one, returning the resultant distribution. -combineDistributionsBinOp :: MonadException m => (Integer -> Integer -> Integer) -> Distribution -> Distribution -> m Distribution -combineDistributionsBinOp f (Distribution m) (Distribution m') = toDistribution $ combineFunc <$> d <*> d' - where - d = M.toList m - d' = M.toList m' - combineFunc (v, c) (v', c') = (f v v', c * c') - --- | Merge all distributions by adding together the probabilities of any values --- that are in multiple distributions, and normalising at the end. -mergeDistributions :: MonadException m => [Distribution] -> m Distribution -mergeDistributions ds = normaliseDistribution $ Prelude.foldr helper (Distribution M.empty) ds - where - helper (Distribution d) (Distribution d') = Distribution $ M.unionWith (+) d d' +-- | If the distribution given is invalid (it is empty), an exception is thrown. +-- Else, the value is just returned. +ifInvalidThrow :: (MonadException m) => Distribution -> m Distribution +ifInvalidThrow d = if D.isValid d then return d else evaluationException "empty distribution" [] -- | Merge all distributions according to a given weighting by multiplying the --- probabilities in each distribution by the given weighting. Uses --- `mergeDistributions`. -mergeWeightedDistributions :: MonadException m => [(Distribution, Rational)] -> m Distribution -mergeWeightedDistributions ds = mergeDistributions $ (\(Distribution m, p) -> Distribution $ M.map (* p) m) <$> ds - --- | Drop all items in the distribution that fulfill the given function. -dropWhereDistribution :: MonadException m => (Integer -> Bool) -> Distribution -> m Distribution -dropWhereDistribution f (Distribution m) = normaliseDistribution $ Distribution $ M.filterWithKey (\k _ -> f k) m - --- | Map over all the integer values, combining the probabilities that then map --- to the same integer. -mapOverValue :: (Integer -> Integer) -> Distribution -> Distribution -mapOverValue f (Distribution m) = Distribution $ M.mapKeysWith (+) f m +-- probabilities in each distribution by the given weighting. +mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution +mergeWeightedDistributions ds = D.fromList $ do + (d, r) <- ds + (i, p) <- D.toList d + return (i, p * r) -- | Get the ByteString representation of the given distribution, setting the -- string as its title. @@ -131,4 +73,4 @@ distributionRenderable t d = toRenderable $ do plot $ plotBars <$> bars ["values"] pts where pts :: [(Double, [Double])] - pts = (\(o, s) -> (fromInteger o, [fromRational s])) <$> fromDistribution d + pts = (\(o, s) -> (fromInteger o, [fromRational s])) <$> D.toList d diff --git a/stack.yaml b/stack.yaml index a6088942..fbbf116a 100644 --- a/stack.yaml +++ b/stack.yaml @@ -39,6 +39,8 @@ packages: # - git: https://github.com/commercialhaskell/stack.git # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # +allow-newer: true + extra-deps: - discord-haskell-1.9.2 - emoji-0.1.0.2 @@ -63,6 +65,8 @@ extra-deps: - monoid-extras-0.6.1 - statestack-0.3 - diagrams-rasterific-1.4.2.2 +- git: https://github.com/jmct/haskell-distribution.git + commit: c8cefde8b4d50ffccf4f9d940eced23f0d56e4c7 # Override default flag values for local packages and extra-deps # flags: {} From a198f0c2e235d8e6c8925828025aa9611421ae3d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 21:56:07 +0000 Subject: [PATCH 15/58] added typing to stop repl complaining --- app/Main.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Main.hs b/app/Main.hs index de2cf5a6..27b2c99a 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -16,7 +16,7 @@ main :: IO () main = forever $ do loadEnv dToken <- pack <$> getEnv "DISCORD_TOKEN" - unless (encodeUtf8 dToken =~ "^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{27}$") $ + unless (encodeUtf8 dToken =~ ("^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{27}$" :: String)) $ die "Invalid token format. Please check it is a bot token" prefix <- pack . fromMaybe "!" <$> lookupEnv "PREFIX" dbpath <- getEnv "SQLITE_FILENAME" From e7cd7b6e8add976f90616f584133f8e88b30c189 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 5 Jan 2022 21:56:56 +0000 Subject: [PATCH 16/58] beginning the switch over to Experiment s for Dice statistics. seemed to have half implemented keep/drop, onto rerolls and integration --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 45697638..a69210f6 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -137,6 +137,30 @@ rangeDieOp :: (MonadException m) => Distribution -> Maybe DieOpRecur -> [DiceCol rangeDieOp _ Nothing ds = return ds rangeDieOp die (Just (DieOpRecur doo mdor)) ds = rangeDieOp' die doo ds >>= rangeDieOp die mdor +rangeDiceExperiment :: (MonadException m) => Distribution -> Maybe DieOpRecur -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDiceExperiment _ Nothing is = return is +rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor + +rangeDieOpExperiment :: MonadException m => Distribution -> DieOpOption -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDieOpExperiment die (DieOpOptionLazy o) is = rangeDieOpExperiment die o is +rangeDieOpExperiment die (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is + +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDieOpExperimentKD kd lhw is = do + let nb = getValueLowHigh lhw + case nb of + Nothing -> whereException + Just nb' -> do + nbd <- range nb' + return $ do + kdlh <- from nbd + getKeep kdlh . sortBy' <$> is + where + whereException = evaluationException "keep/drop where is unsupported" [] + order l l' = if isLow lhw then compare l l' else compare l' l + sortBy' = sortBy order + getKeep = if kd == Keep then genericTake else genericDrop + -- | Apply a single `DieOpOption` to the current list of `DiceCollection`s. rangeDieOp' :: forall m. MonadException m => Distribution -> DieOpOption -> [DiceCollection] -> m [DiceCollection] rangeDieOp' die (DieOpOptionLazy o) ds = rangeDieOp' die o ds From 359a3018cd1a1c259d89c4f6f2c58ecfbffb78da Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 16:21:20 +0000 Subject: [PATCH 17/58] moved more fully over to Experiments and finished that off. more commenting. added stats help page. made graph nicer (thanks john). --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 177 ++++++------------ .../Plugins/Roll/Dice/DiceStatsBase.hs | 26 ++- src/Tablebot/Plugins/Roll/Plugin.hs | 45 ++++- 3 files changed, 108 insertions(+), 140 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index a69210f6..8d9ceba7 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -8,12 +8,11 @@ -- -- This plugin generates statistics based on the values of dice in given -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceStats where +module Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) where import Control.Monad import Control.Monad.Exception (MonadException) import Data.Distribution as D hiding (Distribution, fromList) -import Data.Functor ((<&>)) import Data.List import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval @@ -58,9 +57,8 @@ instance Range Term where range' (Div t e) = do d <- range t d' <- range e - -- having 0 as a denominator is disallowed - d'' <- ifInvalidThrow $ assuming (/= 0) d' - return $ combineWith div d d'' + -- If 0 is always the denominator, the distribution will be empty. + return $ combineWith div d (assuming (/= 0) d') instance Range Negation where range' (Neg t) = select negate <$> range t @@ -71,9 +69,8 @@ instance Range Expo where range' (Expo t e) = do d <- range t d' <- range e - -- having negative values is disallowed - d'' <- ifInvalidThrow $ assuming (>= 0) d' - return $ combineWith (^) d d'' + -- if the exponent is always negative, the distribution will be empty + return $ combineWith (^) d (assuming (>= 0) d') instance Range Func where range' (NoFunc t) = range t @@ -91,61 +88,70 @@ instance Range Die where range' (LazyDie d) = range d range' (Die nb) = do nbr <- range nb - -- for each possible nb value, create a (Distribution, Rational) pair - -- representing the distribution of the die and the probability of that - -- distribution coming up - let vcs = (\(hv, p) -> (fromList ((,1 / fromIntegral hv) <$> [1 .. hv]), p)) <$> toList nbr - -- then condense that into a single distribution - ifInvalidThrow $ mergeWeightedDistributions vcs + return $ + run $ do + nbV <- from nbr + from $ uniform [1 .. nbV] range' (CustomDie (LVBList es)) = do -- get the distribution for each value in the custom die exprs <- mapM range es - let l = genericLength es - -- then merge all the distributions. each distribution is equally likely to - -- come up. - ifInvalidThrow $ mergeWeightedDistributions ((,1 / l) <$> exprs) + return $ run $ from (uniform exprs) >>= from range' cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] instance Range Dice where range' (Dice b d mdor) = do b' <- range b d' <- range d - dcs <- rangeDieOp d' mdor [(b', d', 1)] >>= sequence . (fromCountAndDie <$>) - return $ mergeWeightedDistributions dcs - --- | Aliased type to represent a singular instance of (number of dice, --- distribution of a die, the probability of this occuring). -type DiceCollection = (Distribution, Distribution, Rational) - --- | From a `DiceCollection`, get a distribution and the probability of that --- distribution. -fromCountAndDie :: MonadException m => DiceCollection -> m (Distribution, Rational) -fromCountAndDie (c, d, r) = do - let mwd = do - (i, p) <- toList c - return $ - if i < 1 - then (fromList [(0, 1)], p) - else do - let v = sum (genericTake i (repeat d)) - (v, p) - return (mergeWeightedDistributions mwd, r) - --- | Step by step apply `rangeDieOp'`, returning the current list of --- `DiceCollection`s when `Nothing` is encountered. -rangeDieOp :: (MonadException m) => Distribution -> Maybe DieOpRecur -> [DiceCollection] -> m [DiceCollection] -rangeDieOp _ Nothing ds = return ds -rangeDieOp die (Just (DieOpRecur doo mdor)) ds = rangeDieOp' die doo ds >>= rangeDieOp die mdor - + let e = do + diecount <- from b' + getDiceExperiment diecount d' + res <- rangeDiceExperiment d' mdor e + return $ run $ sum <$> res + +-- | Get the distribution of dice values from a given number of dice and the +-- distribution of the die. +getDiceExperiment :: Integer -> Distribution -> Experiment [Integer] +getDiceExperiment i di = replicateM (fromInteger i) (from di) + +-- | Go through each operator on dice and modify the `Experiment` representing +-- all possible collections of rolls, returning the `Experiment` produced on +-- finding `Nothing`. rangeDiceExperiment :: (MonadException m) => Distribution -> Maybe DieOpRecur -> Experiment [Integer] -> m (Experiment [Integer]) rangeDiceExperiment _ Nothing is = return is rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor +-- | Perform one dice operation on the given `Experiment`, possibly returning +-- a modified experiment representing the distribution of dice rolls. rangeDieOpExperiment :: MonadException m => Distribution -> DieOpOption -> Experiment [Integer] -> m (Experiment [Integer]) rangeDieOpExperiment die (DieOpOptionLazy o) is = rangeDieOpExperiment die o is -rangeDieOpExperiment die (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is +rangeDieOpExperiment _ (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is +rangeDieOpExperiment die (Reroll rro cond lim) is = do + limd <- range lim + return $ do + limit <- from limd + let newDie = mkNewDie limit + rolls <- is + let (count, cutdownRolls) = countTriggers limit rolls + if count == 0 + then return cutdownRolls + else (cutdownRolls ++) <$> getDiceExperiment count newDie + where + mkNewDie limitValue + | rro = die + | otherwise = assuming (\i -> not $ applyCompare cond i limitValue) die + countTriggers limitValue = foldr (\i (c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i : xs')) (0, []) +-- | Perform a keep/drop operation on the `Experiment` of dice rolls. rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDieOpExperimentKD kd (Where cond nb) is = do + nbDis <- range nb + return $ do + wherelimit <- from nbDis + filter (\i -> keepDrop $ applyCompare cond i wherelimit) <$> is + where + keepDrop + | kd == Keep = id + | otherwise = not rangeDieOpExperimentKD kd lhw is = do let nb = getValueLowHigh lhw case nb of @@ -156,84 +162,9 @@ rangeDieOpExperimentKD kd lhw is = do kdlh <- from nbd getKeep kdlh . sortBy' <$> is where + -- the below exception should never trigger - it is a hold over. it is + -- present so that this thing type checks nicely. whereException = evaluationException "keep/drop where is unsupported" [] order l l' = if isLow lhw then compare l l' else compare l' l sortBy' = sortBy order getKeep = if kd == Keep then genericTake else genericDrop - --- | Apply a single `DieOpOption` to the current list of `DiceCollection`s. -rangeDieOp' :: forall m. MonadException m => Distribution -> DieOpOption -> [DiceCollection] -> m [DiceCollection] -rangeDieOp' die (DieOpOptionLazy o) ds = rangeDieOp' die o ds -rangeDieOp' _ (DieOpOptionKD kd lhw) ds = rangeDieOpHelpKD kd lhw ds -rangeDieOp' die (Reroll rro cond lim) ds = do - limd <- range lim - -- join together the nested lists, as well as sequencing the - -- `MonadException` values - join - <$> sequence - ( do - -- for each possible value of the limit, perform the rest of the input - (limitValue, limitProbability) <- toList limd - return - ( -- get the new die distribution (only relevant on infinite - -- rerolls). if the die distribution is invalid (no values), an - -- exception is thrown here, as early as possible. - -- then, transform the given dice collections - die' limitValue <&> transformDiceCollections limitValue limitProbability - ) - ) - where - die' limitValue - | rro = return die - | otherwise = let d = assuming (\i -> not $ applyCompare cond i limitValue) die in ifInvalidThrow d - - -- Go through all the dice values and conditionally perform the reroll. - conditionallyReroll dieDistribution limitValue newDie = do - (dieV, dieP) <- toList dieDistribution - if applyCompare cond dieV limitValue - then [(newDie, dieP)] - else [(fromList [(dieV, 1)], dieP)] - transformDiceCollections limitValue limitProbability newDie = - do - -- for each dice collection in the list, perform the - -- below. - (c, dieDistribution, cp) <- ds - -- return the list of dice collections, sequencing as - -- needed - let mwd = mergeWeightedDistributions $ conditionallyReroll dieDistribution limitValue newDie - return (c, mwd, cp * limitProbability) - --- | Apply a keep/drop dice operation using the given `LowHighWhere` onto the --- list of `DiceCollection`s. -rangeDieOpHelpKD :: (MonadException m) => KeepDrop -> LowHighWhere -> [DiceCollection] -> m [DiceCollection] -rangeDieOpHelpKD kd lhw ds = do - let nb = getValueLowHigh lhw - case nb of - Nothing -> whereException - Just nb' -> do - repeatType <- chooseType kd lhw - nbd <- range nb' - return - ( do - (i, p) <- toList nbd - (c, d, dcp) <- ds - (ci, cp) <- toList c - let toKeep = getRemaining ci i - d' = repeatType (ci - toKeep) d - return (fromList [(toKeep, 1 :: Rational)], d', p * dcp * cp) - ) - where - whereException = evaluationException "keep/drop where is unsupported" [] - getRemaining total value - | kd == Keep = min total value - | otherwise = max 0 (total - value) - repeatedM m i d - | i <= 0 = d - | otherwise = combineWith m d $ repeatedM m (i - 1) d - repeatedMinimum = repeatedM min - repeatedMaximum = repeatedM max - chooseType Keep (High _) = return repeatedMaximum - chooseType Keep (Low _) = return repeatedMinimum - chooseType Drop (Low _) = return repeatedMaximum - chooseType Drop (High _) = return repeatedMinimum - chooseType _ _ = evaluationException "keep/drop where is unsupported" [] diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 604a8805..b4ae6c09 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -12,7 +12,6 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase ( Distribution, fromList, mergeWeightedDistributions, - ifInvalidThrow, distributionByteString, ) where @@ -35,11 +34,6 @@ type Distribution = D.Distribution Integer fromList :: [(Integer, Rational)] -> Distribution fromList = D.fromList --- | If the distribution given is invalid (it is empty), an exception is thrown. --- Else, the value is just returned. -ifInvalidThrow :: (MonadException m) => Distribution -> m Distribution -ifInvalidThrow d = if D.isValid d then return d else evaluationException "empty distribution" [] - -- | Merge all distributions according to a given weighting by multiplying the -- probabilities in each distribution by the given weighting. mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution @@ -48,29 +42,41 @@ mergeWeightedDistributions ds = D.fromList $ do (i, p) <- D.toList d return (i, p * r) +-- | Default x and y values for the output chart. +diagramX, diagramY :: Double +(diagramX, diagramY) = (700.0, 400.0) + -- | Get the ByteString representation of the given distribution, setting the -- string as its title. distributionByteString :: String -> Distribution -> IO B.ByteString distributionByteString t d = encodePng . renderDia Rasterific opts <$> distributionDiagram t d where - opts = RasterificOptions (dims2D 700 400) + opts = RasterificOptions (dims2D diagramX diagramY) -- | Get the Diagram representation of the given distribution, setting the -- string as its title. distributionDiagram :: String -> Distribution -> IO (Diagram B) distributionDiagram t d = do - defEnv <- defaultEnv (AlignmentFns id id) 700 400 + defEnv <- defaultEnv (AlignmentFns id id) diagramX diagramY return . fst $ runBackendR defEnv r where r = distributionRenderable t d +-- TODO: make the numbers on the side of the graph have .0 on the end to show they are continuous + -- | Get the Renderable representation of the given distribution, setting the -- string as its title. distributionRenderable :: String -> Distribution -> Renderable () distributionRenderable t d = toRenderable $ do layout_title .= t + layout_title_style .= defFontStyle + layout_axes_title_styles .= defFontStyle + layout_axes_styles .= def {_axis_label_style = defFontStyle} + layout_x_axis . laxis_title .= "value" + layout_y_axis . laxis_title .= "probability (%)" setColors [opaque blue, opaque red] - plot $ plotBars <$> bars ["values"] pts + plot $ plotBars <$> bars [""] pts where pts :: [(Double, [Double])] - pts = (\(o, s) -> (fromInteger o, [fromRational s])) <$> D.toList d + pts = (\(o, s) -> (fromInteger o, [fromRational s * 100])) <$> D.toList d + defFontStyle = def {_font_size = 2 * _font_size def} diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index c4a2de81..ff9e4ab4 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -12,6 +12,7 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO), void) import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Lazy (toStrict) +import Data.Distribution (isValid) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T @@ -84,7 +85,7 @@ rollHelp = ["r"] "roll dice and do maths" rollHelpText - [] + [statsHelp] None -- | A large chunk of help text for the roll command. @@ -137,6 +138,8 @@ gencharHelp = [] None +-- | The command to get the statistics for an expression and display the +-- results. statsCommand :: Command statsCommand = Command "stats" (parseComm statsCommand') [] where @@ -145,23 +148,51 @@ statsCommand = Command "stats" (parseComm statsCommand') [] statsCommand' e m = do mrange' <- liftIO $ timeout (oneSecond * 5) $ range e case mrange' of - Nothing -> liftIO $ throwBot (EvaluationException "Timed out calculating statistics" []) + Nothing -> throwBot (EvaluationException "Timed out calculating statistics" []) (Just range') -> do mimage <- liftIO $ timeout (oneSecond * 5) $ distributionByteString sse range' case mimage of - Nothing -> liftIO $ throwBot (EvaluationException ("Timed out displaying statistics.\n" <> msg range') []) + Nothing -> do + sendMessage m (msg range') + throwBot (EvaluationException "Timed out displaying statistics." []) (Just image) -> do liftDiscord $ void $ restCall - ( CreateMessageDetailed (messageChannel m) (MessageDetailedOpts (pack (msg range')) False Nothing (Just (se <> ".png", toStrict image)) Nothing Nothing) + ( CreateMessageDetailed (messageChannel m) (MessageDetailedOpts (msg range') False Nothing (Just (se <> ".png", toStrict image)) Nothing Nothing) ) where se = prettyShow e sse = unpack se - msg d = let (modalOrder, mean, std) = getStats d in ("Here are the statistics for your dice (" <> sse <> ").\n Ten most common totals: " <> show (take 10 modalOrder) <> "\n Mean: " <> show mean <> "\n Standard deviation: " <> show std) - --- sendMessage m (T.pack $ show range') + msg d = + if (not . isValid) d + then "The distribution was empty." + else + let (modalOrder, mean, std) = getStats d + in ( "Here are the statistics for your dice (" + <> se + <> ").\n Ten most common totals: " + <> T.pack (show (take 10 modalOrder)) + <> "\n Mean: " + <> roundShow mean + <> "\n Standard deviation: " + <> roundShow std + ) + roundShow :: Double -> Text + roundShow d = T.pack $ show $ fromInteger (round (d * 10 ** precision)) / 10 ** precision + where + precision = 5 :: Double + +-- | Help page for dice stats. +statsHelp :: HelpPage +statsHelp = + HelpPage + "stats" + [] + "calculate and display statistics for expressions." + "**Roll Stats**\nCan be used to display statistics for expressions of dice.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`" + [] + None -- | @rollPlugin@ assembles the command into a plugin. rollPlugin :: Plugin From 088a04c514557708c2ea2e140210e376686a621a Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 16:27:54 +0000 Subject: [PATCH 18/58] a smidge of tidying up --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index b4ae6c09..37b19861 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -17,7 +17,6 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase where import Codec.Picture (PngSavable (encodePng)) -import Control.Monad.Exception (MonadException) import qualified Data.ByteString.Lazy as B import qualified Data.Distribution as D import Diagrams (Diagram, dims2D, renderDia) @@ -25,7 +24,6 @@ import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy -import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) -- | A wrapper type for mapping values to their probabilities. type Distribution = D.Distribution Integer From a39119bd773bd5e98bb394aa6d3dbe053d60046a Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 19:37:50 +0000 Subject: [PATCH 19/58] beginning list distributions --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 60 +++++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 8d9ceba7..9ed92d61 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -12,10 +12,12 @@ module Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) where import Control.Monad import Control.Monad.Exception (MonadException) -import Data.Distribution as D hiding (Distribution, fromList) +import Data.Distribution hiding (Distribution, fromList) +import qualified Data.Distribution as D import Data.List import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval +import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Plugins.Roll.Dice.DiceStatsBase -- | Get the most common values, the mean, and the standard deviation of a given @@ -36,8 +38,8 @@ combineRangesBinOp f a b = do -- | Type class to get the overall range of a value. -- --- A `Tablebot.Plugins.Roll.Dice.DiceStatsBase.Distribution` is a map of values --- to probabilities, and has a variety of functions that operate on them. +-- A `Data.Distribution.Distribution` is a map of values to probabilities, and +-- has a variety of functions that operate on them. class Range a where -- | Try and get the `Distribution` of the given value, throwing a -- `MonadException` on failure. @@ -108,8 +110,8 @@ instance Range Dice where res <- rangeDiceExperiment d' mdor e return $ run $ sum <$> res --- | Get the distribution of dice values from a given number of dice and the --- distribution of the die. +-- | Get the distribution of values from a given number of (identically +-- distributed) values and the distribution of that value. getDiceExperiment :: Integer -> Distribution -> Experiment [Integer] getDiceExperiment i di = replicateM (fromInteger i) (from di) @@ -168,3 +170,51 @@ rangeDieOpExperimentKD kd lhw is = do order l l' = if isLow lhw then compare l l' else compare l' l sortBy' = sortBy order getKeep = if kd == Keep then genericTake else genericDrop + +-- | Convenient alias for a distribution of lists of integers. +type DistributionList = D.Distribution [Integer] + +-- | Type class to get the overall range of a list of values. +-- +-- Only used within `DiceStats` as I have no interest in producing statistics on +-- lists +class RangeList a where + -- | Try and get the `DistributionList` of the given value, throwing a + -- `MonadException` on failure. + rangeList :: (MonadException m, PrettyShow a) => a -> m DistributionList + rangeList a = propagateException (prettyShow a) (rangeList' a) + + rangeList' :: (MonadException m, PrettyShow a) => a -> m DistributionList + +spreadDistributions :: (Ord a) => [D.Distribution a] -> Experiment [a] +spreadDistributions [] = return [] +spreadDistributions (d : ds) = from d >>= \d' -> (d' :) <$> spreadDistributions ds + +instance RangeList ListValuesBase where + rangeList' (LVBList es) = do + exprs <- mapM range es + return $ run $ spreadDistributions exprs + rangeList' (LVBParen (Paren lv)) = rangeList lv + +instance RangeList ListValues where + rangeList' (LVBase lvb) = rangeList lvb + rangeList' (MultipleValues nb b) = do + nbd <- range nb + bd <- range b + return $ + run $ do + valNum <- from nbd + getDiceExperiment valNum bd + rangeList' (LVFunc fi avs) = evaluationException "list evaluations are not implmeneted yet" [] + +rangeArgValue :: MonadException m => ArgValue -> m (D.Distribution ListInteger) +rangeArgValue (AVExpr e) = run . (LIInteger <$>) . from <$> range e +rangeArgValue (AVListValues lv) = run . (LIList <$>) . from <$> rangeList lv + +rangeFunction :: (MonadException m, MonadException n, Ord (n j)) => FuncInfoBase n j -> [ArgValue] -> m (D.Distribution (n j)) +rangeFunction fi exprs = do + exprs' <- mapM rangeArgValue exprs + return $ + run $ do + params <- spreadDistributions exprs' + return (funcInfoFunc fi params) From 135d88f0dfa2eb833f2fcc803f9f8cb34f1b4088 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 20:57:56 +0000 Subject: [PATCH 20/58] made functions fully work --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 4 +- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 2 +- .../Plugins/Roll/Dice/DiceFunctions.hs | 41 ++++++++++--------- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 6 +-- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 28 ++++++------- .../Plugins/Roll/Dice/DiceStatsBase.hs | 14 ------- 6 files changed, 42 insertions(+), 53 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 0f7dece1..1280e4de 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -22,7 +22,7 @@ data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) -- | The type for list values. -data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase IO [Integer]) [ArgValue] | LVBase ListValuesBase +data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) [ArgValue] | LVBase ListValuesBase deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). @@ -47,7 +47,7 @@ data Expo = Expo Func Expo | NoExpo Func deriving (Show) -- | The type representing a single function application, or a base item. -data Func = Func (FuncInfo IO) [ArgValue] | NoFunc Base +data Func = Func FuncInfo [ArgValue] | NoFunc Base deriving (Show) -- | The type representing an integer value or an expression in brackets. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 902aec3a..dc43d99d 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -359,7 +359,7 @@ instance IOEval Func where evalShow' rngCount (NoFunc b) = evalShow rngCount b -- | Evaluate a function when given a list of parameters -evaluateFunction :: RNGCount -> FuncInfoBase IO j -> [ArgValue] -> IO (j, Text, RNGCount) +evaluateFunction :: RNGCount -> FuncInfoBase j -> [ArgValue] -> IO (j, Text, RNGCount) evaluateFunction rngCount fi exprs = do (exprs', rngCount') <- evalShowList'' (\r a -> evalArgValue r a >>= \(i, r') -> return (i, "", r')) rngCount exprs f <- funcInfoFunc fi (fst <$> exprs') diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index d0c80295..e4283294 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -36,16 +36,16 @@ factorialLimit = 50 -- | Mapping from function names to the functions themselves for integer -- functions. -integerFunctions :: MonadException m => Map Text (FuncInfo m) +integerFunctions :: Map Text FuncInfo integerFunctions = M.fromList $ fmap (\fi -> (funcInfoName fi, fi)) integerFunctions' -- | The names of the integer functions currently supported. integerFunctionsList :: [Text] -integerFunctionsList = M.keys (integerFunctions @IO) +integerFunctionsList = M.keys integerFunctions -- | The base details of the integer functions, containing all the information -- for each function that returns an integer. -integerFunctions' :: MonadException m => [FuncInfo m] +integerFunctions' :: [FuncInfo] integerFunctions' = funcInfoIndex : constructFuncInfo "length" (genericLength @Integer @Integer) : @@ -63,23 +63,26 @@ integerFunctions' = | otherwise = n * fact (n - 1) -- | Mapping from function names to the functions themselves for list functions. -listFunctions :: MonadException m => Map Text (FuncInfoBase m [Integer]) +listFunctions :: Map Text (FuncInfoBase [Integer]) listFunctions = M.fromList $ fmap (\fi -> (funcInfoName fi, fi)) listFunctions' -- | The names of the list functions currently supported. listFunctionsList :: [Text] -listFunctionsList = M.keys (listFunctions @IO) +listFunctionsList = M.keys listFunctions -- | The base details of the list functions, containing all the information for -- each function that returns an integer. -listFunctions' :: MonadException m => [FuncInfoBase m [Integer]] +listFunctions' :: [FuncInfoBase [Integer]] listFunctions' = - constructFuncInfo @[Integer] "drop" (genericDrop @Integer) : + constructFuncInfo "between" between : + constructFuncInfo "drop" (genericDrop @Integer) : constructFuncInfo "take" (genericTake @Integer) : (uncurry constructFuncInfo <$> [("sort", sort), ("reverse", reverse)]) + where + between i i' = let (mi, ma, rev) = (min i i', max i i', if i > i' then reverse else id) in rev [mi .. ma] -- | The `FuncInfo` of the function that indexes into a list. -funcInfoIndex :: FuncInfo m +funcInfoIndex :: FuncInfo funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex where fiIndex (LIInteger i : [LIList is]) @@ -89,20 +92,20 @@ funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex -- | A data structure to contain the information about a given function, -- including types, the function name, and the function itself. -data FuncInfoBase m j = FuncInfo {funcInfoName :: Text, funcInfoParameters :: [ArgType], funcReturnType :: ArgType, funcInfoFunc :: MonadException m => [ListInteger] -> m j} +data FuncInfoBase j = FuncInfo {funcInfoName :: Text, funcInfoParameters :: [ArgType], funcReturnType :: ArgType, funcInfoFunc :: forall m. (MonadException m) => [ListInteger] -> m j} -type FuncInfo m = FuncInfoBase m Integer +type FuncInfo = FuncInfoBase Integer -instance Show (FuncInfoBase m j) where +instance Show (FuncInfoBase j) where show (FuncInfo fin ft frt _) = "FuncInfo " <> unpack fin <> " " <> show ft <> " " <> show frt -- | A simple way to construct a function that returns a value j, and has no -- constraints on the given values. -constructFuncInfo :: forall j f m. (MonadException m, ApplyFunc m f, Returns f ~ j) => Text -> f -> FuncInfoBase m j +constructFuncInfo :: forall j f. (ApplyFunc f, Returns f ~ j) => Text -> f -> FuncInfoBase j constructFuncInfo s f = constructFuncInfo' s f (Nothing, Nothing, const False) -- | Construct a function info when given optional constraints. -constructFuncInfo' :: forall j f m. (MonadException m, ApplyFunc m f, Returns f ~ j) => Text -> f -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> FuncInfoBase m j +constructFuncInfo' :: forall j f. (ApplyFunc f, Returns f ~ j) => Text -> f -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> FuncInfoBase j constructFuncInfo' s f bs = FuncInfo s params (last types) (applyFunc f (fromIntegral (length params)) bs) where types = getTypes f @@ -144,12 +147,12 @@ instance ArgCount f => ArgCount ([Integer] -> f) where -- -- If the number of inputs is incorrect or the value given out of the range, an -- exception is thrown. -class ArgCount f => ApplyFunc m f where +class ArgCount f => ApplyFunc f where -- | Takes a function, the number of arguments in the function overall, bounds -- on integer values to the function, and a list of `ListInteger`s (which are -- either a list of integers or an integer), and returns a wrapped `j` value, -- which is a value that the function originally returns. - applyFunc :: (MonadException m, Returns f ~ j) => f -> Integer -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> [ListInteger] -> m j + applyFunc :: forall m j. (MonadException m, Returns f ~ j) => f -> Integer -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> [ListInteger] -> m j -- | Check whether a given value is within the given bounds. checkBounds :: (MonadException m) => Integer -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> m Integer @@ -159,22 +162,22 @@ checkBounds i (ml, mh, bs) | bs i = throwBot $ EvaluationException ("invalid value for function: `" <> show i ++ "`") [] | otherwise = return i -instance {-# OVERLAPPING #-} ApplyFunc m Integer where +instance {-# OVERLAPPING #-} ApplyFunc Integer where applyFunc f _ _ [] = return f applyFunc _ args _ _ = throwBot $ EvaluationException ("incorrect number of arguments to function. expected " <> show args <> ", got more than that") [] -instance {-# OVERLAPPING #-} ApplyFunc m [Integer] where +instance {-# OVERLAPPING #-} ApplyFunc [Integer] where applyFunc f _ _ [] = return f applyFunc _ args _ _ = throwBot $ EvaluationException ("incorrect number of arguments to function. expected " <> show args <> ", got more than that") [] -instance {-# OVERLAPPABLE #-} (ApplyFunc m f) => ApplyFunc m (Integer -> f) where +instance {-# OVERLAPPABLE #-} (ApplyFunc f) => ApplyFunc (Integer -> f) where applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] where dif = args - getArgs f applyFunc f args bs ((LIInteger x) : xs) = checkBounds x bs >>= \x' -> applyFunc (f x') args bs xs applyFunc _ _ _ (_ : _) = throwBot $ EvaluationException "incorrect type given to function. expected an integer, got a list" [] -instance {-# OVERLAPPABLE #-} (ApplyFunc m f) => ApplyFunc m ([Integer] -> f) where +instance {-# OVERLAPPABLE #-} (ApplyFunc f) => ApplyFunc ([Integer] -> f) where applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] where dif = args - getArgs f diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 977aa876..88190c71 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -46,7 +46,7 @@ instance CanParse ListValues where _ <- char '#' MultipleValues nb <$> pars ) - <|> functionParser (listFunctions @IO) LVFunc + <|> functionParser listFunctions LVFunc instance CanParse ListValuesBase where pars = do @@ -74,13 +74,13 @@ instance CanParse Term where binOpParseHelp '*' (Multi t) <|> binOpParseHelp '/' (Div t) <|> (return . NoTerm) t instance CanParse Func where - pars = try (functionParser (integerFunctions @IO) Func) <|> NoFunc <$> pars + pars = try (functionParser integerFunctions Func) <|> NoFunc <$> pars -- | A generic function parser that takes a mapping from function names to -- functions, the main way to contruct the function data type `e`, and a -- constructor for `e` that takes only one value, `a` (which has its own, -- previously defined parser). -functionParser :: M.Map Text (FuncInfoBase m j) -> (FuncInfoBase m j -> [ArgValue] -> e) -> Parser e +functionParser :: M.Map Text (FuncInfoBase j) -> (FuncInfoBase j -> [ArgValue] -> e) -> Parser e functionParser m mainCons = do fi <- try (choice (string <$> M.keys m) >>= \t -> return (m M.! t)) "could not find function" diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 9ed92d61..95b0c106 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -11,14 +11,16 @@ module Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) where import Control.Monad -import Control.Monad.Exception (MonadException) +import Control.Monad.Exception +import Data.Bifunctor (Bifunctor (first)) import Data.Distribution hiding (Distribution, fromList) import qualified Data.Distribution as D import Data.List import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval import Tablebot.Plugins.Roll.Dice.DiceFunctions -import Tablebot.Plugins.Roll.Dice.DiceStatsBase +import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) +import Tablebot.Utility.Exception (catchBot) -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. @@ -76,7 +78,7 @@ instance Range Expo where instance Range Func where range' (NoFunc t) = range t - range' f@(Func _ _) = evaluationException "tried to find range of function, which is currently unsupported" [prettyShow f] + range' (Func fi avs) = rangeFunction fi avs instance Range NumBase where range' (Value i) = return $ always i @@ -94,11 +96,9 @@ instance Range Die where run $ do nbV <- from nbr from $ uniform [1 .. nbV] - range' (CustomDie (LVBList es)) = do - -- get the distribution for each value in the custom die - exprs <- mapM range es - return $ run $ from (uniform exprs) >>= from - range' cd@(CustomDie _) = evaluationException "tried to find range of complex custom die" [prettyShow cd] + range' (CustomDie lv) = do + dievs <- rangeList lv + return $ run $ from dievs >>= from . uniform instance Range Dice where range' (Dice b d mdor) = do @@ -205,16 +205,16 @@ instance RangeList ListValues where run $ do valNum <- from nbd getDiceExperiment valNum bd - rangeList' (LVFunc fi avs) = evaluationException "list evaluations are not implmeneted yet" [] + rangeList' (LVFunc fi avs) = rangeFunction fi avs rangeArgValue :: MonadException m => ArgValue -> m (D.Distribution ListInteger) rangeArgValue (AVExpr e) = run . (LIInteger <$>) . from <$> range e rangeArgValue (AVListValues lv) = run . (LIList <$>) . from <$> rangeList lv -rangeFunction :: (MonadException m, MonadException n, Ord (n j)) => FuncInfoBase n j -> [ArgValue] -> m (D.Distribution (n j)) +rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Distribution j) rangeFunction fi exprs = do exprs' <- mapM rangeArgValue exprs - return $ - run $ do - params <- spreadDistributions exprs' - return (funcInfoFunc fi params) + let params = first (funcInfoFunc fi) <$> toList (run $ spreadDistributions exprs') + D.fromList <$> foldAndIgnoreErrors params + where + foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 37b19861..14f70494 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -10,8 +10,6 @@ -- `Distribution`s. module Tablebot.Plugins.Roll.Dice.DiceStatsBase ( Distribution, - fromList, - mergeWeightedDistributions, distributionByteString, ) where @@ -28,18 +26,6 @@ import Graphics.Rendering.Chart.Easy -- | A wrapper type for mapping values to their probabilities. type Distribution = D.Distribution Integer --- | Convenient way to set the types being used so that warnings don't pop up. -fromList :: [(Integer, Rational)] -> Distribution -fromList = D.fromList - --- | Merge all distributions according to a given weighting by multiplying the --- probabilities in each distribution by the given weighting. -mergeWeightedDistributions :: [(Distribution, Rational)] -> Distribution -mergeWeightedDistributions ds = D.fromList $ do - (d, r) <- ds - (i, p) <- D.toList d - return (i, p * r) - -- | Default x and y values for the output chart. diagramX, diagramY :: Double (diagramX, diagramY) = (700.0, 400.0) From ba1b8cc92988f32d9a100e89dec8582256340401 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 21:52:11 +0000 Subject: [PATCH 21/58] some tidying --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 3 +++ src/Tablebot/Plugins/Roll/Plugin.hs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 95b0c106..e7e40d53 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -186,6 +186,9 @@ class RangeList a where rangeList' :: (MonadException m, PrettyShow a) => a -> m DistributionList +-- | Take a list of distributions of type a. For add each one, perform an +-- experiment where the values in that distribution are prepended to the values +-- in the rest of the distribution spreadDistributions :: (Ord a) => [D.Distribution a] -> Experiment [a] spreadDistributions [] = return [] spreadDistributions (d : ds) = from d >>= \d' -> (d' :) <$> spreadDistributions ds diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index ff9e4ab4..ff1d54b7 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,15 +15,15 @@ import Data.ByteString.Lazy (toStrict) import Data.Distribution (isValid) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) -import qualified Data.Text as T +import Data.Text qualified as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types (Message (messageAuthor, messageChannel)) -import System.Timeout +import System.Timeout (timeout) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) -import Tablebot.Plugins.Roll.Dice.DiceStatsBase +import Tablebot.Plugins.Roll.Dice.DiceStatsBase (distributionByteString) import Tablebot.Utility import Tablebot.Utility.Discord (sendMessage, toMention) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) From b95aca82d546b08cc4e5eea3a187c9804bf4a829 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 6 Jan 2022 21:52:38 +0000 Subject: [PATCH 22/58] some tidying pt2 --- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index ff1d54b7..1ba33a24 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,7 +15,7 @@ import Data.ByteString.Lazy (toStrict) import Data.Distribution (isValid) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) -import Data.Text qualified as T +import qualified Data.Text as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types (Message (messageAuthor, messageChannel)) From 8e142f2464d83bf3239320a8f5888c5cf426f593 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 11:41:32 +0000 Subject: [PATCH 23/58] added some documentation and ensured that distribution output is always valid --- docs/Roll.md | 21 ++++++++++++++++++++ docs/resources/dicestats_2d20kh1.jpg | Bin 0 -> 32963 bytes src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 5 ++++- src/Tablebot/Plugins/Roll/Plugin.hs | 4 ++-- 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100755 docs/resources/dicestats_2d20kh1.jpg diff --git a/docs/Roll.md b/docs/Roll.md index 54bd1047..347893fc 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -4,6 +4,8 @@ The roll command has a staggering amount of flexibility, as well as additional f Below are listed the current full capabilities of the bot for rolling dice and evaluation expressions. All operations (currently) result in integers or a list. A list of functions is available in the [functions section](#Functions). +You can also generate statistics of an expression. See the [Statistics](#Statistics) section for more information. + ## Basic Operators - Addition @@ -83,3 +85,22 @@ Here are all the functions, what they take, and what they return. - reverse (list) - reverse the list - sort (list) - sort the list in ascending order - take (integer, list) - take the first `n` values from a list, where `n` is the integer given +- between (integer, integer) - generate a list between the two given integers + +# Statistics + +As well as generating values, statistics based off of expressions can be found. + +To get these statistics, calling the `roll` command with the `stats` subcommand will generate the requested statistics. The expression given has to return an integer. + +The bot will give the mean, the standard deviation, and the top ten most common values of the distribution, as well as graphing the entire distribution. + +For example, the result of calling `roll stats 2d20kh1` (roll two twenty sided dice and keep the higher die) can be seen below. + +!["The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly."](./resources/dicestats_2d20kh1.jpg "the result of asking for stats of 2d20kh1") + +(above: The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly.) + +Currently, the statistics generation supports all valid expressions. + +If invalid states occur (such as with division by zero, negative exponents, or infinite rerolls) the bot will alert the user only if the entire distribution becomes empty. For example, in `1d20rr<(21-d{0,1})`, half of the time infinite rerolls will occur. In this case, these invalid cases are ignored, as they can never be actually rolled, and the only value output is `20`. If the expression given is instead `1/0`, the entire distribution will be empty, as there is no valid output from this expression. diff --git a/docs/resources/dicestats_2d20kh1.jpg b/docs/resources/dicestats_2d20kh1.jpg new file mode 100755 index 0000000000000000000000000000000000000000..85eefe42300d5bf64cb1ae6f7362ef598603879c GIT binary patch literal 32963 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 literal 0 HcmV?d00001 diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index e7e40d53..c9ccc076 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -8,7 +8,7 @@ -- -- This plugin generates statistics based on the values of dice in given -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) where +module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, getStats) where import Control.Monad import Control.Monad.Exception @@ -38,6 +38,9 @@ combineRangesBinOp f a b = do d' <- range b return $ f d d' +rangeExpr :: (MonadException m) => Expr -> m Distribution +rangeExpr = (D.fromList . D.toList <$>) . range + -- | Type class to get the overall range of a value. -- -- A `Data.Distribution.Distribution` is a map of values to probabilities, and diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 1ba33a24..63098f9e 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -22,7 +22,7 @@ import Discord.Types (Message (messageAuthor, messageChannel)) import System.Timeout (timeout) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData -import Tablebot.Plugins.Roll.Dice.DiceStats (Range (range), getStats) +import Tablebot.Plugins.Roll.Dice.DiceStats (getStats, rangeExpr) import Tablebot.Plugins.Roll.Dice.DiceStatsBase (distributionByteString) import Tablebot.Utility import Tablebot.Utility.Discord (sendMessage, toMention) @@ -146,7 +146,7 @@ statsCommand = Command "stats" (parseComm statsCommand') [] oneSecond = 1000000 statsCommand' :: Expr -> Message -> DatabaseDiscord () statsCommand' e m = do - mrange' <- liftIO $ timeout (oneSecond * 5) $ range e + mrange' <- liftIO $ timeout (oneSecond * 5) $ rangeExpr e case mrange' of Nothing -> throwBot (EvaluationException "Timed out calculating statistics" []) (Just range') -> do From 1a08e2785a25e16588bec79e7dfd5646b4a0ea40 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 13:57:52 +0000 Subject: [PATCH 24/58] allowed for multiple stats to be checked at the same time, and fixed a small bug with advanced orderings --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 14 +++--- .../Plugins/Roll/Dice/DiceStatsBase.hs | 50 ++++++++++++------- src/Tablebot/Plugins/Roll/Plugin.hs | 42 ++++++++++------ 3 files changed, 68 insertions(+), 38 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 88190c71..9c15c85c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -13,11 +13,12 @@ module Tablebot.Plugins.Roll.Dice.DiceParsing () where import Data.Functor (($>), (<&>)) +import Data.List (sortBy) import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Maybe (fromMaybe) import Data.Set as S (Set, fromList, map) -import Data.Text (Text, singleton, unpack) +import qualified Data.Text as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), @@ -33,8 +34,8 @@ import Text.Megaparsec.Char (char, string) import Text.Megaparsec.Error (ErrorItem (Tokens)) -- | An easier way to handle failure in parsers. -failure' :: Text -> Set Text -> Parser a -failure' s ss = failure (Just $ Tokens $ NE.fromList $ unpack s) (S.map (Tokens . NE.fromList . unpack) ss) +failure' :: T.Text -> Set T.Text -> Parser a +failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Tokens . NE.fromList . T.unpack) ss) instance CanParse ListValues where pars = @@ -80,7 +81,7 @@ instance CanParse Func where -- functions, the main way to contruct the function data type `e`, and a -- constructor for `e` that takes only one value, `a` (which has its own, -- previously defined parser). -functionParser :: M.Map Text (FuncInfoBase j) -> (FuncInfoBase j -> [ArgValue] -> e) -> Parser e +functionParser :: M.Map T.Text (FuncInfoBase j) -> (FuncInfoBase j -> [ArgValue] -> e) -> Parser e functionParser m mainCons = do fi <- try (choice (string <$> M.keys m) >>= \t -> return (m M.! t)) "could not find function" @@ -147,9 +148,8 @@ parseDice' = do parseAdvancedOrdering :: Parser AdvancedOrdering parseAdvancedOrdering = (try (choice opts) "could not parse an ordering") >>= matchO where - matchO :: Text -> Parser AdvancedOrdering matchO s = M.findWithDefault (failure' s (S.fromList opts')) s (M.map return $ fst advancedOrderingMapping) - opts' = M.keys $ fst advancedOrderingMapping + opts' = sortBy (\a b -> compare (T.length b) (T.length a)) $ M.keys $ fst advancedOrderingMapping opts = fmap string opts' -- | Parse a `LowHighWhere`, which is an `h` followed by an integer. @@ -159,7 +159,7 @@ parseLowHigh = (try (choice @[] $ char <$> "lhw") "could not parse high, low helper 'h' = High <$> pars helper 'l' = Low <$> pars helper 'w' = parseAdvancedOrdering >>= \o -> pars <&> Where o - helper c = failure' (singleton c) (S.fromList ["h", "l", "w"]) + helper c = failure' (T.singleton c) (S.fromList ["h", "l", "w"]) -- | Parse a bunch of die options into, possibly, a DieOpRecur. parseDieOpRecur :: Parser (Maybe DieOpRecur) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 14f70494..2e7c99ce 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -15,52 +15,68 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase where import Codec.Picture (PngSavable (encodePng)) -import qualified Data.ByteString.Lazy as B -import qualified Data.Distribution as D +import Data.ByteString.Lazy qualified as B +import Data.Distribution qualified as D +import Data.Map qualified as M +import Data.Set qualified as S +import Data.Text qualified as T import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy +import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) -- | A wrapper type for mapping values to their probabilities. type Distribution = D.Distribution Integer -- | Default x and y values for the output chart. diagramX, diagramY :: Double -(diagramX, diagramY) = (700.0, 400.0) +(diagramX, diagramY) = (1400.0, 400.0) -- | Get the ByteString representation of the given distribution, setting the -- string as its title. -distributionByteString :: String -> Distribution -> IO B.ByteString -distributionByteString t d = encodePng . renderDia Rasterific opts <$> distributionDiagram t d +distributionByteString :: [(Distribution, T.Text)] -> IO B.ByteString +distributionByteString d = encodePng . renderDia Rasterific opts <$> distributionDiagram d where opts = RasterificOptions (dims2D diagramX diagramY) -- | Get the Diagram representation of the given distribution, setting the -- string as its title. -distributionDiagram :: String -> Distribution -> IO (Diagram B) -distributionDiagram t d = do - defEnv <- defaultEnv (AlignmentFns id id) diagramX diagramY - return . fst $ runBackendR defEnv r +distributionDiagram :: [(Distribution, T.Text)] -> IO (Diagram B) +distributionDiagram d = do + if null d + then evaluationException "empty distribution" [] + else do + defEnv <- defaultEnv (AlignmentFns id id) diagramX diagramY + return . fst $ runBackendR defEnv r where - r = distributionRenderable t d + r = distributionRenderable d -- TODO: make the numbers on the side of the graph have .0 on the end to show they are continuous -- | Get the Renderable representation of the given distribution, setting the -- string as its title. -distributionRenderable :: String -> Distribution -> Renderable () -distributionRenderable t d = toRenderable $ do - layout_title .= t +distributionRenderable :: [(Distribution, T.Text)] -> Renderable () +distributionRenderable d = toRenderable $ do + layout_title .= T.unpack (title' d) layout_title_style .= defFontStyle layout_axes_title_styles .= defFontStyle layout_axes_styles .= def {_axis_label_style = defFontStyle} layout_x_axis . laxis_title .= "value" layout_y_axis . laxis_title .= "probability (%)" - setColors [opaque blue, opaque red] - plot $ plotBars <$> bars [""] pts + -- setColors (take[opaque blue, opaque red, opaque green, opaque teal, opaque violet]) + plot $ plotBars <$> (bars @Double @Double) (barNames d) pts where - pts :: [(Double, [Double])] - pts = (\(o, s) -> (fromInteger o, [fromRational s * 100])) <$> D.toList d + ds = M.fromList . D.toList . fst <$> d + allIntegers = S.toList $ S.unions $ M.keysSet <$> ds + insertEmpty k = M.insertWith (\_ a -> a) k 0 + ds' = M.unionsWith (++) $ M.map (: []) <$> (applyAll (insertEmpty <$> allIntegers) <$> ds) + pts = bimap fromInteger (fromRational . (*100) <$>) <$> M.toList ds' + applyAll [] = id + applyAll (f:fs) = f . applyAll fs defFontStyle = def {_font_size = 2 * _font_size def} + barNames [_] = [""] + barNames xs = T.unpack . snd <$> xs + title' [(_, t)] = t + title' xs = "Range of " <> T.intercalate ", " (snd <$> xs) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 63098f9e..498199ce 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,7 +15,7 @@ import Data.ByteString.Lazy (toStrict) import Data.Distribution (isValid) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) -import qualified Data.Text as T +import Data.Text qualified as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types (Message (messageAuthor, messageChannel)) @@ -25,11 +25,11 @@ import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceStats (getStats, rangeExpr) import Tablebot.Plugins.Roll.Dice.DiceStatsBase (distributionByteString) import Tablebot.Utility -import Tablebot.Utility.Discord (sendMessage, toMention) +import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMention) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) -import Tablebot.Utility.Parser (inlineCommandHelper) +import Tablebot.Utility.Parser (inlineCommandHelper, skipSpace) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), pars) -import Text.Megaparsec (MonadParsec (try), choice, ()) +import Text.Megaparsec (MonadParsec (try), choice, many, ()) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -141,16 +141,21 @@ gencharHelp = -- | The command to get the statistics for an expression and display the -- results. statsCommand :: Command -statsCommand = Command "stats" (parseComm statsCommand') [] +statsCommand = Command "stats" statsCommandParser [] where oneSecond = 1000000 - statsCommand' :: Expr -> Message -> DatabaseDiscord () - statsCommand' e m = do - mrange' <- liftIO $ timeout (oneSecond * 5) $ rangeExpr e + statsCommandParser :: Parser (Message -> DatabaseDiscord ()) + statsCommandParser = do + firstE <- pars + restEs <- many (try $ skipSpace *> pars) + return $ statsCommand' (firstE : restEs) + statsCommand' :: [Expr] -> Message -> DatabaseDiscord () + statsCommand' es m = do + mrange' <- liftIO $ timeout (oneSecond * 5) $ mapM (\e -> (,prettyShow e) <$> rangeExpr e) es case mrange' of Nothing -> throwBot (EvaluationException "Timed out calculating statistics" []) (Just range') -> do - mimage <- liftIO $ timeout (oneSecond * 5) $ distributionByteString sse range' + mimage <- liftIO $ timeout (oneSecond * 5) $ distributionByteString range' case mimage of Nothing -> do sendMessage m (msg range') @@ -159,18 +164,16 @@ statsCommand = Command "stats" (parseComm statsCommand') [] liftDiscord $ void $ restCall - ( CreateMessageDetailed (messageChannel m) (MessageDetailedOpts (msg range') False Nothing (Just (se <> ".png", toStrict image)) Nothing Nothing) + ( CreateMessageDetailed (messageChannel m) (MessageDetailedOpts (msg range') False Nothing (Just (T.unwords (snd <$> range') <> ".png", toStrict image)) Nothing Nothing) ) where - se = prettyShow e - sse = unpack se - msg d = + msg [(d, t)] = if (not . isValid) d then "The distribution was empty." else let (modalOrder, mean, std) = getStats d in ( "Here are the statistics for your dice (" - <> se + <> formatText Code t <> ").\n Ten most common totals: " <> T.pack (show (take 10 modalOrder)) <> "\n Mean: " @@ -178,6 +181,17 @@ statsCommand = Command "stats" (parseComm statsCommand') [] <> "\n Standard deviation: " <> roundShow std ) + msg dts = + let (modalOrders, means, stds) = unzip3 $ getStats . fst <$> dts + in ( "Here are the statistics for your dice (" + <> intercalate ", " (formatText Code . snd <$> dts) + <> ").\n Most common totals (capped to ten total): " + <> T.pack (show (take (div 10 (length modalOrders)) <$> modalOrders)) + <> "\n Means: " + <> intercalate ", " (roundShow <$> means) + <> "\n Standard deviations: " + <> intercalate ", " (roundShow <$> stds) + ) roundShow :: Double -> Text roundShow d = T.pack $ show $ fromInteger (round (d * 10 ** precision)) / 10 ** precision where From 0a70f24d9158cb163c82ab380d4a1d8cc583149b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 13:59:11 +0000 Subject: [PATCH 25/58] ormolu --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 16 ++++++++-------- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 2e7c99ce..3c2a083e 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -15,11 +15,11 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase where import Codec.Picture (PngSavable (encodePng)) -import Data.ByteString.Lazy qualified as B -import Data.Distribution qualified as D -import Data.Map qualified as M -import Data.Set qualified as S -import Data.Text qualified as T +import qualified Data.ByteString.Lazy as B +import qualified Data.Distribution as D +import qualified Data.Map as M +import qualified Data.Set as S +import qualified Data.Text as T import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) @@ -66,15 +66,15 @@ distributionRenderable d = toRenderable $ do layout_x_axis . laxis_title .= "value" layout_y_axis . laxis_title .= "probability (%)" -- setColors (take[opaque blue, opaque red, opaque green, opaque teal, opaque violet]) - plot $ plotBars <$> (bars @Double @Double) (barNames d) pts + plot $ plotBars <$> (bars @Double @Double) (barNames d) pts where ds = M.fromList . D.toList . fst <$> d allIntegers = S.toList $ S.unions $ M.keysSet <$> ds insertEmpty k = M.insertWith (\_ a -> a) k 0 ds' = M.unionsWith (++) $ M.map (: []) <$> (applyAll (insertEmpty <$> allIntegers) <$> ds) - pts = bimap fromInteger (fromRational . (*100) <$>) <$> M.toList ds' + pts = bimap fromInteger (fromRational . (* 100) <$>) <$> M.toList ds' applyAll [] = id - applyAll (f:fs) = f . applyAll fs + applyAll (f : fs) = f . applyAll fs defFontStyle = def {_font_size = 2 * _font_size def} barNames [_] = [""] barNames xs = T.unpack . snd <$> xs diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 498199ce..ae6c682d 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,7 +15,7 @@ import Data.ByteString.Lazy (toStrict) import Data.Distribution (isValid) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) -import Data.Text qualified as T +import qualified Data.Text as T import Discord (restCall) import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types (Message (messageAuthor, messageChannel)) From f291653ebde7268c2f78b46b8cec63ccf16a113d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 14:37:00 +0000 Subject: [PATCH 26/58] set all of the fonts in the graph to the same font to ensure readability --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 3c2a083e..7b67c6fd 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -60,12 +60,9 @@ distributionDiagram d = do distributionRenderable :: [(Distribution, T.Text)] -> Renderable () distributionRenderable d = toRenderable $ do layout_title .= T.unpack (title' d) - layout_title_style .= defFontStyle - layout_axes_title_styles .= defFontStyle - layout_axes_styles .= def {_axis_label_style = defFontStyle} layout_x_axis . laxis_title .= "value" layout_y_axis . laxis_title .= "probability (%)" - -- setColors (take[opaque blue, opaque red, opaque green, opaque teal, opaque violet]) + layout_all_font_styles .= defFontStyle plot $ plotBars <$> (bars @Double @Double) (barNames d) pts where ds = M.fromList . D.toList . fst <$> d From 54d172c1754cd7c6d7e6e0fa36658d212ff80790 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 15:22:28 +0000 Subject: [PATCH 27/58] adjusted Roll.md slightly --- docs/Roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Roll.md b/docs/Roll.md index 347893fc..ec6a5ff0 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -97,7 +97,7 @@ The bot will give the mean, the standard deviation, and the top ten most common For example, the result of calling `roll stats 2d20kh1` (roll two twenty sided dice and keep the higher die) can be seen below. -!["The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly."](./resources/dicestats_2d20kh1.jpg "the result of asking for stats of 2d20kh1") +["The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly."](./resources/dicestats_2d20kh1.jpg "the result of asking for stats of 2d20kh1") (above: The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly.) From 41d6c3324068500cddbe6fd0ad1253076e2cb4ba Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 7 Jan 2022 15:24:00 +0000 Subject: [PATCH 28/58] undoing prev commit --- docs/Roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Roll.md b/docs/Roll.md index ec6a5ff0..347893fc 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -97,7 +97,7 @@ The bot will give the mean, the standard deviation, and the top ten most common For example, the result of calling `roll stats 2d20kh1` (roll two twenty sided dice and keep the higher die) can be seen below. -["The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly."](./resources/dicestats_2d20kh1.jpg "the result of asking for stats of 2d20kh1") +!["The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly."](./resources/dicestats_2d20kh1.jpg "the result of asking for stats of 2d20kh1") (above: The results of asking for stats of 2d20kh1 (roll two twenty sided dice and keep the highest one). The ten most common rolls are 20 to 11. The mean is 13.825. The standard deviation is about 4.7. The bar chart has values on each integer from 1 to 20, with the height of each bar increasing linearly.) From e31781b9b0737ee3f0acbdacff13528aeeab3666 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 11 Jan 2022 13:09:49 +0000 Subject: [PATCH 29/58] adjusting spacing in graph --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 7b67c6fd..990cb6d0 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -63,7 +63,9 @@ distributionRenderable d = toRenderable $ do layout_x_axis . laxis_title .= "value" layout_y_axis . laxis_title .= "probability (%)" layout_all_font_styles .= defFontStyle - plot $ plotBars <$> (bars @Double @Double) (barNames d) pts + pb <- (bars @Double @Double) (barNames d) pts + let pb' = pb {_plot_bars_spacing = BarsFixGap 10 5} + plot $ return $ plotBars pb' where ds = M.fromList . D.toList . fst <$> d allIntegers = S.toList $ S.unions $ M.keysSet <$> ds From 00e934939fd0624bc718b47908ddc22f490cfa7d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 11 Jan 2022 13:13:51 +0000 Subject: [PATCH 30/58] making some parser changes and running into issues with no errors showing up --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 69 ++++++++++++------- src/Tablebot/Plugins/Roll/Plugin.hs | 26 +++++-- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 9c15c85c..80d47c7b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -10,7 +10,7 @@ -- -- This plugin contains the tools for parsing Dice. -Wno-orphans is enabled so -- that parsing can occur here instead of in SmartParser or DiceData. -module Tablebot.Plugins.Roll.Dice.DiceParsing () where +module Tablebot.Plugins.Roll.Dice.DiceParsing where import Data.Functor (($>), (<&>)) import Data.List (sortBy) @@ -18,7 +18,7 @@ import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Maybe (fromMaybe) import Data.Set as S (Set, fromList, map) -import qualified Data.Text as T +import Data.Text qualified as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), @@ -75,7 +75,7 @@ instance CanParse Term where binOpParseHelp '*' (Multi t) <|> binOpParseHelp '/' (Div t) <|> (return . NoTerm) t instance CanParse Func where - pars = try (functionParser integerFunctions Func) <|> NoFunc <$> pars + pars = functionParser integerFunctions Func <|> NoFunc <$> pars -- | A generic function parser that takes a mapping from function names to -- functions, the main way to contruct the function data type `e`, and a @@ -101,8 +101,8 @@ instance CanParse Expo where instance CanParse NumBase where pars = - try (NBParen . unnest <$> pars) - <|> Value <$> integer + (NBParen . unnest <$> try pars "could not parse number in parentheses") + <|> Value <$> try integer "could not parse numbase integer" where unnest (Paren (NoExpr (NoTerm (NoNeg (NoExpo (NoFunc (NBase (NBParen (Paren e))))))))) = Paren e unnest e = e @@ -111,38 +111,53 @@ instance (CanParse a) => CanParse (Paren a) where pars = char '(' *> skipSpace *> (Paren <$> pars) <* skipSpace <* char ')' instance CanParse Base where - pars = try (DiceBase <$> pars) <|> try (NBase <$> pars) + pars = + ( (try pars "could not parse numbase in base") >>= \nb -> + (DiceBase <$> parseDice nb) + <|> return (NBase nb) + ) + <|> DiceBase <$> try (parseDice (Value 1)) "cannot parse numberless die" + +--try (DiceBase <$> pars) <|> (NBase <$> pars) instance CanParse Die where pars = do - _ <- char 'd' + _ <- try (char 'd') "could not find 'd' for die" lazyFunc <- (try (char '!') $> LazyDie) <|> return id - try - ( lazyFunc . CustomDie - <$> pars + ( try + ( lazyFunc . CustomDie + <$> pars + ) + "could not parse list values for die" ) <|> lazyFunc . Die - <$> pars + <$> (try pars "couldn't parse base number for die") -instance CanParse Dice where - pars = do - t <- optional $ try (pars :: Parser NumBase) - bd <- parseDice' - let t' = NBase $ fromMaybe (Value 1) t - return $ bd t' +-- instance CanParse Dice where +-- pars = do +-- t <- optional $ try (pars :: Parser NumBase) +-- bd <- parseDice' +-- let t' = NBase $ fromMaybe (Value 1) t +-- return $ bd t' + +parseDice :: NumBase -> Parser Dice +parseDice nb = parseDice' <*> return (NBase nb) -- | Helper for parsing Dice, where as many `Dice` as possible are parsed and a -- function that takes a `Base` value and returns a `Dice` value is returned. -- This `Base` value is meant to be first value that `Dice` have. parseDice' :: Parser (Base -> Dice) parseDice' = do - d <- pars :: Parser Die + d <- try (pars :: Parser Die) "could not parse die in dice" mdor <- parseDieOpRecur - ( do - bd <- try parseDice' - return (\b -> bd (DiceBase $ Dice b d mdor)) + try + ( ( do + bd <- try parseDice' "trying to recurse dice failed" + return (\b -> bd (DiceBase $ Dice b d mdor)) + ) + <|> return (\b -> Dice b d mdor) ) - <|> return (\b -> Dice b d mdor) + "could not recurse dice proper" -- | Parse a `/=`, `<=`, `>=`, `<`, `=`, `>` as an `AdvancedOrdering`. parseAdvancedOrdering :: Parser AdvancedOrdering @@ -164,7 +179,7 @@ parseLowHigh = (try (choice @[] $ char <$> "lhw") "could not parse high, low -- | Parse a bunch of die options into, possibly, a DieOpRecur. parseDieOpRecur :: Parser (Maybe DieOpRecur) parseDieOpRecur = do - dopo <- optional (try parseDieOpOption) + dopo <- optional parseDieOpOption maybe (return Nothing) (\dopo' -> Just . DieOpRecur dopo' <$> parseDieOpRecur) dopo -- | Parse a single die option. @@ -173,8 +188,12 @@ parseDieOpOption = do lazyFunc <- (try (char '!') $> DieOpOptionLazy) <|> return id ( ( (try (string "ro") *> parseAdvancedOrdering >>= \o -> Reroll True o <$> pars) <|> (try (string "rr") *> parseAdvancedOrdering >>= \o -> Reroll False o <$> pars) - <|> ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) - <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) + <|> ( try + ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) + <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) + ) + "could not parse keep/drop" + ) ) <&> lazyFunc ) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index ae6c682d..cef69b66 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -28,8 +28,8 @@ import Tablebot.Utility import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMention) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper, skipSpace) -import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), pars) -import Text.Megaparsec (MonadParsec (try), choice, many, ()) +import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), WithError (WErr), pars) +import Text.Megaparsec (MonadParsec (try), choice, many) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -62,17 +62,31 @@ rollDice' e' t m = do rollDiceParser :: Parser (Message -> DatabaseDiscord ()) rollDiceParser = choice (try <$> options) where + justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () + justEither (WErr x) = rollDice' (Just x) Nothing + nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () + nothingAtAll (WErr _) = rollDice' Nothing Nothing + bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () + bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) + justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () + justText (WErr x) = rollDice' Nothing (Just x) options = - [ parseComm (\lv -> rollDice' (Just lv) Nothing), - parseComm (rollDice' Nothing Nothing), - try (parseComm (\lv qt -> rollDice' (Just lv) (Just qt))) "", - try (parseComm (rollDice' Nothing . Just)) "" + [ parseComm justEither, + parseComm nothingAtAll, + parseComm bothVals, + parseComm justText ] -- | Basic command for rolling dice. rollDice :: Command rollDice = Command "roll" rollDiceParser [statsCommand] +-- where +-- below does not work +-- rollDiceParser = parseComm rollDiceParser' +-- rollDiceParser' :: WithError "Incorrect rolling format. Please check your expression and quote is of the correct format" (Maybe (Either ListValues Expr), Maybe (Quoted Text)) -> Message -> DatabaseDiscord () +-- rollDiceParser' (WErr (x,y)) = rollDice' x y + -- | Rolling dice inline. rollDiceInline :: InlineCommand rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> rollDice' (Just e) Nothing m) From 5cd270d7dcc2c7a231e1f1856d637973f241382f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 11 Jan 2022 13:14:24 +0000 Subject: [PATCH 31/58] ORMOLU! --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 80d47c7b..0f11c3e3 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -18,7 +18,7 @@ import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Maybe (fromMaybe) import Data.Set as S (Set, fromList, map) -import Data.Text qualified as T +import qualified Data.Text as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), From 011ac00ff79a3792f22c465359ddcc7031a3b264 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 12 Jan 2022 13:47:59 +0000 Subject: [PATCH 32/58] updated distribution --- stack.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stack.yaml b/stack.yaml index fbbf116a..c026104d 100644 --- a/stack.yaml +++ b/stack.yaml @@ -39,7 +39,7 @@ packages: # - git: https://github.com/commercialhaskell/stack.git # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # -allow-newer: true +# allow-newer: true extra-deps: - discord-haskell-1.9.2 @@ -65,8 +65,8 @@ extra-deps: - monoid-extras-0.6.1 - statestack-0.3 - diagrams-rasterific-1.4.2.2 -- git: https://github.com/jmct/haskell-distribution.git - commit: c8cefde8b4d50ffccf4f9d940eced23f0d56e4c7 +- git: https://github.com/L0neGamer/haskell-distribution.git + commit: 569d6452e4bffedb2c0d3795885fccdb22a4d29d # Override default flag values for local packages and extra-deps # flags: {} From c6526cf98083090ec93d682aa62b31e89838240e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 12 Jan 2022 14:41:20 +0000 Subject: [PATCH 33/58] add some comments --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 0f11c3e3..03519512 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -16,7 +16,6 @@ import Data.Functor (($>), (<&>)) import Data.List (sortBy) import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) -import Data.Maybe (fromMaybe) import Data.Set as S (Set, fromList, map) import qualified Data.Text as T import Tablebot.Plugins.Roll.Dice.DiceData @@ -57,8 +56,11 @@ instance CanParse ListValuesBase where <* skipSpace <* char '}' ) - <|> LVBParen + <|> LVBParen . unnest <$> pars + where + unnest (Paren (LVBase (LVBParen e))) = e + unnest e = e -- | Helper function to try to parse the second part of a binary operator. binOpParseHelp :: (CanParse a) => Char -> (a -> a) -> Parser a @@ -104,7 +106,7 @@ instance CanParse NumBase where (NBParen . unnest <$> try pars "could not parse number in parentheses") <|> Value <$> try integer "could not parse numbase integer" where - unnest (Paren (NoExpr (NoTerm (NoNeg (NoExpo (NoFunc (NBase (NBParen (Paren e))))))))) = Paren e + unnest (Paren (NoExpr (NoTerm (NoNeg (NoExpo (NoFunc (NBase (NBParen e)))))))) = e unnest e = e instance (CanParse a) => CanParse (Paren a) where @@ -118,8 +120,6 @@ instance CanParse Base where ) <|> DiceBase <$> try (parseDice (Value 1)) "cannot parse numberless die" ---try (DiceBase <$> pars) <|> (NBase <$> pars) - instance CanParse Die where pars = do _ <- try (char 'd') "could not find 'd' for die" @@ -133,13 +133,8 @@ instance CanParse Die where <|> lazyFunc . Die <$> (try pars "couldn't parse base number for die") --- instance CanParse Dice where --- pars = do --- t <- optional $ try (pars :: Parser NumBase) --- bd <- parseDice' --- let t' = NBase $ fromMaybe (Value 1) t --- return $ bd t' - +-- | Given a `NumBase` (the value on the front of a set of dice), construct a +-- set of dice. parseDice :: NumBase -> Parser Dice parseDice nb = parseDice' <*> return (NBase nb) @@ -199,10 +194,12 @@ parseDieOpOption = do ) "could not parse dieOpOption - expecting one of the options described in the doc (call `help roll` to access)" +-- | Parse a single `ArgType` into an `ArgValue`. parseArgValue :: ArgType -> Parser ArgValue parseArgValue ATIntegerList = AVListValues <$> try pars "could not parse a list value from the argument" parseArgValue ATInteger = AVExpr <$> try pars "could not parse an integer from the argument" +-- | Parse a list of comma separated arguments. parseArgValues :: [ArgType] -> Parser [ArgValue] parseArgValues [] = return [] parseArgValues [at] = (: []) <$> parseArgValue at From c5017c5a085935fb05b1e1bfcad3d76fbbd57951 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 12 Jan 2022 18:06:57 +0000 Subject: [PATCH 34/58] update docs --- docs/Roll.md | 10 +++++++++- docs/resources/dicestats_2d20kh1_4d6dl1.jpg | Bin 0 -> 32693 bytes 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100755 docs/resources/dicestats_2d20kh1_4d6dl1.jpg diff --git a/docs/Roll.md b/docs/Roll.md index 347893fc..40411bde 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -89,7 +89,7 @@ Here are all the functions, what they take, and what they return. # Statistics -As well as generating values, statistics based off of expressions can be found. +As well as generating values, statistics based off of expressions can be found. There is a total time limit of 10 seconds for this command, with 5 seconds given to calculations and 5 seconds given to generating the bar chart. To get these statistics, calling the `roll` command with the `stats` subcommand will generate the requested statistics. The expression given has to return an integer. @@ -104,3 +104,11 @@ For example, the result of calling `roll stats 2d20kh1` (roll two twenty sided d Currently, the statistics generation supports all valid expressions. If invalid states occur (such as with division by zero, negative exponents, or infinite rerolls) the bot will alert the user only if the entire distribution becomes empty. For example, in `1d20rr<(21-d{0,1})`, half of the time infinite rerolls will occur. In this case, these invalid cases are ignored, as they can never be actually rolled, and the only value output is `20`. If the expression given is instead `1/0`, the entire distribution will be empty, as there is no valid output from this expression. + +As well as statistics for a given expression, multiple expressions can be shown in the same instance. + +For example, the result of calling `roll stats 2d20kh1 4d6dl1` is as follows. + +!["The results of asking for stats of 2d20kh1 and 4d6dl1 (roll two twenty sided dice and keep the highest one, and roll four dice with six sides, and drop the lowest value of each). The most common rolls for each expression are 20 to 16, and 13, 12, 14, 11, and 15. The means are about 13.8 and 12.2. The standard deviation are about 4.7 and 2.8. The bar chart has blue values on each integer from 1 to 20, with the height of each bar increasing linearly, and green values that form a weighted bell curve centered on 13."](./resources/dicestats_2d20kh1_4d6dl1.jpg "the result of asking for stats of 2d20kh1 and 4d6dl1") + +(above: The results of asking for stats of 2d20kh1 and 4d6dl1 (roll two twenty sided dice and keep the highest one, and roll four dice with six sides, and drop the lowest value of each). The most common rolls for each expression are 20 to 16, and 13, 12, 14, 11, and 15. The means are about 13.8 and 12.2. The standard deviation are about 4.7 and 2.8. The bar chart has blue values on each integer from 1 to 20, with the height of each bar increasing linearly, and green values that form a weighted bell curve centered on 13.) diff --git a/docs/resources/dicestats_2d20kh1_4d6dl1.jpg b/docs/resources/dicestats_2d20kh1_4d6dl1.jpg new file mode 100755 index 0000000000000000000000000000000000000000..f321941446bbdc398796c28ffb310f40c7ca264a GIT binary patch literal 32693 zcmb@tcT|(n*De^ONf)G7r7Bga(nX|1r1!2;1JVUTje>%70qISomw?pJdzIdTbO^mC z)Bqve{JuNun_2V6%$>XD&3pdHN?H3k=iU1}=h=JT{kvNQJb0rDR0ZJS;Q`*^UVys= zfHDC8-o5|axQhUH6W%8zBp@IpB_<}iPfkisPDV;bMnOsQfP#{ml8o#D{R8TUv~+ZI zR+;0d7NeBr^Xer1jX#YQ7cO3xg`+&uJl=yg@fP2(<_|$lJ zJpc{>0FMx7?SBmZKQ6p`I2(zG?~{;{;l5D!0B{cvAO9W!{(r2-eLD#EIe>tgkmm7o zC8CGA?}<4*Xhpxo=icXfRnh9_7>mL~WJ2^Ey1E2jjH@}8h-`L#R-r3zlot&PXUtFTEuK$Az z4}kwaVf{C<{~Io99Ikr=1o#BR|G|ZK&lflFsR;-lKPRG5(j|WH@sLyW%YE8c@wru< zBwS*8NV*T66QuOq;%hvp|3LdMWdCcxLjJdq{Wq}x6W2U|93Kz2c=*%+MZgtlq`)p! z1Un`2@Uow$wFz~URJbBOAHXK*EHE2yqD)EK^irP>KhGxpAaW}q&MQ5?k#lp-;;Jq3 zTBC>K{-+gM@B)G(!aGg4p#Bc{24?j2&6diJ^8>#`1jd!?>Dv58VY z{cP8_W|tr*4$-<+sIuU(CWW}QfG=L9gKia_c@?FuThoH@nT_WpZGVL>uWsrk3Bt=D zl=mgNFnmb)l;{cxF-W1ae>8o3USh$Bn=`p7H+?n&lJK~niR1a$TcdVRKkWP#5}%FI zGgsU{$EI~mSZxL&)w;5or{#hqVdZ-c>d5J)fY*XFPx+c+_awdJeBQV+DYeivkiV3^ zSH5!xs9BEkUd!UwI5aYs*D#tg*AKg}R2^l}c=>mPT(9eZ*R#`DxPjE-og{^FLZr(b zz}Ohm4vlD6M`AE;K6)UHSz z5$5tk=C;iYs$5e5(*Luy%ho?^InS6*x~b zLQW{aUC5gk=4VU|+>+BHxWDem!uFB{=J3dfuPZ0ipQ`7@dofdVB%JJ4sT}GJauqyu z;Ur(MxdWIfHU6NH;Wn#Gvi-f!DI^T9m4)qeQ9_3s@(6WQC|bq5G5b4<^w z{GJaN&eIqeek|9Uyr@2Ma`ec~$E#m@#1YmdFV@T4A1lGKJ&Q3fHv@-U&m*Rkljqzx zBRHZ;9@{MmUkLR${rc#tb}#(*)^6pR+bJ_#i^}^BK#XaxXjjm+TJo@4`-av<4wkRa z2x62Kpq?c@g%3iMg#N^5Y%ae1dvLZbU5v3@ZKvFs&xw#zUUyM8n;uz-a64NtIh!K) z8h-C?*N~J(dLI8Q66Yia`u@R+MOTSTPo>z~v9osoPB2t~%+IwjmeIzqFg_&Tqs0D$ zKY&=@s&}pOYa8pV9KLVmTt9+?M!m+8C{!)ry+RfZ$6{-5Ovv8zb)V}W4G~> z!Q2Em=yAGaq{+X$zCC3D{E++d_r?ESI79Hz3K#p`{XNV@-#PSAKTovn++%2SY`z__ zGuk#Tiiy=c6{IaB_5_z&@uQ8`j`6_vDAX*bC79%Z-oC}4ag({$HL@JX{Dsj6HlSHb%qJK$AqQ62MD^QO1UA>jZxND0EQSBCv|Tma(L-UKQ81{5Xwy?RSD#?eG}O zgN!#-zD$HvcbHerrK`qa4)Ql0|EG0JKJhtFoG#o&4}XwS_F89IHzMoW_*y0lBiD zY1}N96>Yb+9S_S`-T|!aITrHItpCQrfIIC;0g%`>3b3mp9XfVP7LIHS{RK+a-#4mv z@_zcUj^wjZ^>^{8mI$ePX4)wP^xPky=qWYiZCsyYEd^JH5mq=uck1lZRQb|om3HaEjI>=Hir3iCmWBg>kBJNoqiwvheNI%JeB-1Nf>SnE zHbpuw1X7kagXV84vDT3ay8mT>?!~S=!$~D%BA5X!)(4?%s1v?ObwL~?T=^dugF{ONl}lW-8vQM8qkU;`hXb8BM1XHSimP>u7b3iAjWT# zHcb4*`O49iqAS5EL-Psq0nB5&eS@3&1u@TLR_?^9*_oMVn zKUAjwR`(9zUyq?)ZI1iSE><`3D_izO{&0#^4WzUdrm`eB8o>XE583Z;+su{yf3-dk zHQPmXA06In#g1bbYQxCwZ|2=J&alf_cDv!_*ZOZ`q>{&mV`&tZN(_e}_XnX3SX!`% znKkL2s(NBKvvz2PX>()ycMufvs6CmRfgPCE2LxQ%ZSa3F2os2Ci^GMza4B4{{TsQp zPz`|yrC{I2r7JA){l*#u{bw!9=H!%+_H*sEa=?;BkODi>yDbakN4WA8(b%prrl1=8 zoYvS&@C%GP7t^oVrn?@Wy{|cW=Aw8}3Pa8m#^hUoy>zBrqLX3i5@lh1$vl*$f3f;0 zSE+gTwDp`U%5zfAPuS{UNp}E|(=&zl|ApLeNtC5kx5zxeRG9p^_@v+S=e^zIM`{+p;{of}|Nm9<@n> zUHiMN=~|QvcjJSyJMDmLp|t{nAdzJGl;9OKg?p{a5-*?g4y~?=4k4}X%m2smDVqzm zm5YF|aL`&RyRheODe3c#_#yAN+*i#ptPyrsM8b=NrBrNs1!tRK3;LlU!R;Oh)8iL++m` z@&B~gdgE?ebzwx=cgFkXOWe9+FFCpE*g!LOox53zU zSuCGYNuY_$RtmFIFvIvco0BZh)T~8==}7<2sw>AgB(F6&D*!&Pf_3~qJH>@!IdjN% z%#x#gb3tv7Co3Hy4;Gwwy-DQW&#*Gus&dgh;|CHok~S7PUN%i9_S?Ad40RHu=zZ_= zQDIK@Rp_|q)ZBIlK;mu)jStWV$82!X7R?qOp&;t4@FcYN!Q$NRc+q#wh$i~+>igKg z8JyHHXdN{jrHfu%_)yhkcv~|QD3ZHB+hhs*ci=6g#3$N)U(k?=#xV9}LG03w$M}Ya z1qGd4ne~1_yAaW*U_w+amT*S6BCo1$`tVqA^G{Pv%PE1E(xdJQ8#_BWcjbHhi#ZP^ zh-#2^%=dUU#oNndTH1lkT}_(!Nz;4 z7jofj?#CDTm!sCAiIOwUzi|DqaXiSx@J)dE^&$4K+v2SeCw4w;;o>%)`?oFZP@?J

MYu)6*ITJKLwhy%tyX?bg$}Hd9ao%@622XiC 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

YG{#d9FD5C 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 literal 0 HcmV?d00001 From 8b0498aa3ee0d14e30ece4cb42a8ad6b7d93ad76 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 12 Jan 2022 18:08:08 +0000 Subject: [PATCH 35/58] made a bunch of changes to how the graphs are displayed to make them nicer --- .../Plugins/Roll/Dice/DiceStatsBase.hs | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 990cb6d0..6312d825 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -15,13 +15,16 @@ module Tablebot.Plugins.Roll.Dice.DiceStatsBase where import Codec.Picture (PngSavable (encodePng)) +import Data.Bifunctor import qualified Data.ByteString.Lazy as B import qualified Data.Distribution as D +import Data.List (genericLength) import qualified Data.Map as M import qualified Data.Set as S import qualified Data.Text as T import Diagrams (Diagram, dims2D, renderDia) import Diagrams.Backend.Rasterific +import Graphics.Rendering.Chart.Axis.Int import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) import Graphics.Rendering.Chart.Backend.Types import Graphics.Rendering.Chart.Easy @@ -53,8 +56,6 @@ distributionDiagram d = do where r = distributionRenderable d --- TODO: make the numbers on the side of the graph have .0 on the end to show they are continuous - -- | Get the Renderable representation of the given distribution, setting the -- string as its title. distributionRenderable :: [(Distribution, T.Text)] -> Renderable () @@ -62,8 +63,10 @@ distributionRenderable d = toRenderable $ do layout_title .= T.unpack (title' d) layout_x_axis . laxis_title .= "value" layout_y_axis . laxis_title .= "probability (%)" + layout_x_axis . laxis_generate .= scaledIntAxis' r + layout_y_axis . laxis_override .= \ad@AxisData {_axis_labels = axisLabels} -> ad {_axis_labels = (second (\s -> if '.' `elem` s then s else s ++ ".0") <$>) <$> axisLabels} layout_all_font_styles .= defFontStyle - pb <- (bars @Double @Double) (barNames d) pts + pb <- (bars @Integer @Double) (barNames d) pts let pb' = pb {_plot_bars_spacing = BarsFixGap 10 5} plot $ return $ plotBars pb' where @@ -72,6 +75,7 @@ distributionRenderable d = toRenderable $ do insertEmpty k = M.insertWith (\_ a -> a) k 0 ds' = M.unionsWith (++) $ M.map (: []) <$> (applyAll (insertEmpty <$> allIntegers) <$> ds) pts = bimap fromInteger (fromRational . (* 100) <$>) <$> M.toList ds' + r = (fst $ M.findMin ds', fst $ M.findMax ds') applyAll [] = id applyAll (f : fs) = f . applyAll fs defFontStyle = def {_font_size = 2 * _font_size def} @@ -79,3 +83,53 @@ distributionRenderable d = toRenderable $ do barNames xs = T.unpack . snd <$> xs title' [(_, t)] = t title' xs = "Range of " <> T.intercalate ", " (snd <$> xs) + +-- | Custom scaling function due to some difficulties for drawing charts. +-- +-- Using +-- https://hackage.haskell.org/package/Chart-1.9.3/docs/src/Graphics.Rendering.Chart.Axis.Int.html#scaledIntAxis +-- for pointers. +scaledIntAxis' :: (Integer, Integer) -> AxisFn Integer +scaledIntAxis' r@(minI, maxI) _ = makeAxis (_la_labelf lap) ((minI - 1) : (maxI + 1) : labelvs, tickvs, gridvs) + where + lap = defaultIntAxis + labelvs = stepsInt' (fromIntegral $ _la_nLabels lap) r + tickvs = + stepsInt' + (fromIntegral $ _la_nTicks lap) + ( fromIntegral $ minimum labelvs, + fromIntegral $ maximum labelvs + ) + gridvs = labelvs + +-- | Taken and modified from +-- https://hackage.haskell.org/package/Chart-1.9.3/docs/src/Graphics.Rendering.Chart.Axis.Int.html#stepsInt +stepsInt' :: Integer -> (Integer, Integer) -> [Integer] +stepsInt' nSteps range = bestSize (goodness alt0) alt0 alts + where + bestSize n a (a' : as) = + let n' = goodness a' + in if n' < n then bestSize n' a' as else a + bestSize _ _ [] = [] + + goodness vs = abs (genericLength vs - nSteps) + + (alt0 : alts) = map (`steps` range) sampleSteps' + + -- throw away sampleSteps that are definitely too small as + -- they takes a long time to process + sampleSteps' = + let rangeMag = (snd range - fst range) + + (s1, s2) = span (< (rangeMag `div` nSteps)) sampleSteps + in (reverse . take 5 . reverse) s1 ++ s2 + + -- generate all possible step sizes + sampleSteps = [1, 2, 5] ++ sampleSteps1 + sampleSteps1 = [10, 20, 25, 50] ++ map (* 10) sampleSteps1 + + steps :: Integer -> (Integer, Integer) -> [Integer] + steps size' (minV, maxV) = takeWhile (< b) [a, a + size' ..] ++ [b] + where + a = floor @Double (fromIntegral minV / fromIntegral size') * size' + b = ceiling @Double (fromIntegral maxV / fromIntegral size') * size' From f3a305321daad16339ed63dff00cb3abaaddca76 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 19 Jan 2022 13:25:43 +0000 Subject: [PATCH 36/58] added commented out more modern distribution --- stack.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/stack.yaml b/stack.yaml index 581cbc4b..08e82a73 100644 --- a/stack.yaml +++ b/stack.yaml @@ -65,6 +65,7 @@ extra-deps: - monoid-extras-0.6.1 - statestack-0.3 - diagrams-rasterific-1.4.2.2 +# - distribution-1.1.1.1 - git: https://github.com/L0neGamer/haskell-distribution.git commit: 569d6452e4bffedb2c0d3795885fccdb22a4d29d From 5e18d29a4e5b72562c4034f22c9380874a1832ae Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 16:21:04 +0000 Subject: [PATCH 37/58] merge --- src/Tablebot/Internal/Handler/Command.hs | 27 +++--------------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 910bb46e..b2805188 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -68,30 +68,9 @@ parseCommands cs m prefix = case parse (parser cs) "" (messageText m) of parser cs' = do _ <- chunk prefix - res <- parser' cs' - case res of - Nothing -> fail "No command with that name was found!" - Just p -> return p - parser' :: [CompiledCommand] -> Parser (Maybe (Message -> CompiledDatabaseDiscord ())) - parser' cs' = - do - -- 1. Parse the command name (fails if no such command exists). - res <- choice (map matchCommand cs') <|> return Nothing - case res of - Nothing -> return Nothing - Just (command, subcommands) -> - do - -- 2. Try to get a subcommand. - maybeComm <- parser' subcommands - case maybeComm of - -- 2.1. If there's a subcommand, use that. - Just pars -> return (Just pars) - -- 2.2. Otherwise, use the main command. - Nothing -> Just <$> command - matchCommand :: CompiledCommand -> Parser (Maybe (Parser (Message -> CompiledDatabaseDiscord ()), [CompiledCommand])) - matchCommand c = do - try (chunk (commandName c) *> (skipSpace1 <|> eof)) - return (Just (commandParser c, commandSubcommands c)) + choice (map toErroringParser cs') "No command with that name was found!" + toErroringParser :: CompiledCommand -> Parser (Message -> CompiledDatabaseDiscord ()) + toErroringParser c = try (chunk $ commandName c) *> (skipSpace1 <|> eof) *> (try (choice $ map toErroringParser $ commandSubcommands c) <|> commandParser c) data ReadableError = UnknownError | KnownError String [String] deriving (Show, Eq, Ord) From d5bb87b3462d4a58fd079d557e058fc381430103 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 16:21:43 +0000 Subject: [PATCH 38/58] making parsers better for dice --- docs/Roll.md | 2 + .../Plugins/Roll/Dice/DiceFunctions.hs | 2 + src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 65 +++++++++++-------- src/Tablebot/Plugins/Roll/Plugin.hs | 4 +- src/Tablebot/Utility/Parser.hs | 10 ++- src/Tablebot/Utility/SmartParser.hs | 4 +- 6 files changed, 52 insertions(+), 35 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index 40411bde..1a1f5df6 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -72,6 +72,8 @@ Here are all the functions, what they take, and what they return. - abs (integer) - the absolute value of an integer - fact (integer < 50) - the factorial of an integer - id (integer) - the integer +- max (integer, integer) - get the maximum item between two items +- min (integer, integer) - get the minimum item between two items - maximum (list) - get the maximum item in a list - minimum (list) - get the minimum item in a list - mod (two integers, second /= 0) - get the modulo of two integers diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index e4283294..58bba1f9 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -50,6 +50,8 @@ integerFunctions' = funcInfoIndex : constructFuncInfo "length" (genericLength @Integer @Integer) : constructFuncInfo "sum" (sum @[] @Integer) : + constructFuncInfo "max" (max @Integer) : + constructFuncInfo "min" (min @Integer) : constructFuncInfo "maximum" (maximum @[] @Integer) : constructFuncInfo "minimum" (minimum @[] @Integer) : constructFuncInfo' "mod" (mod @Integer) (Nothing, Nothing, (== 0)) : diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 03519512..973bd581 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -17,7 +17,7 @@ import Data.List (sortBy) import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Set as S (Set, fromList, map) -import qualified Data.Text as T +import Data.Text qualified as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), @@ -28,7 +28,7 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace) import Tablebot.Utility.SmartParser (CanParse (..)) import Tablebot.Utility.Types (Parser) -import Text.Megaparsec (MonadParsec (try), choice, failure, optional, (), (<|>)) +import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, (), (<|>)) import Text.Megaparsec.Char (char, string) import Text.Megaparsec.Error (ErrorItem (Tokens)) @@ -36,17 +36,24 @@ import Text.Megaparsec.Error (ErrorItem (Tokens)) failure' :: T.Text -> Set T.Text -> Parser a failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Tokens . NE.fromList . T.unpack) ss) +() :: Parser a -> String -> Parser a +() p s = do + r <- observing p + case r of + Left _ -> fail s + Right a -> return a + instance CanParse ListValues where pars = do - try (LVBase <$> pars) - <|> try + LVBase <$> pars + <|> functionParser listFunctions LVFunc + <|> ( do nb <- pars _ <- char '#' MultipleValues nb <$> pars ) - <|> functionParser listFunctions LVFunc instance CanParse ListValuesBase where pars = do @@ -103,35 +110,39 @@ instance CanParse Expo where instance CanParse NumBase where pars = - (NBParen . unnest <$> try pars "could not parse number in parentheses") - <|> Value <$> try integer "could not parse numbase integer" + (NBParen . unnest <$> pars) + <|> Value <$> integer "could not parse integer" where unnest (Paren (NoExpr (NoTerm (NoNeg (NoExpo (NoFunc (NBase (NBParen e)))))))) = e unnest e = e instance (CanParse a) => CanParse (Paren a) where - pars = char '(' *> skipSpace *> (Paren <$> pars) <* skipSpace <* char ')' + pars = try (char '(') *> skipSpace *> (Paren <$> pars) <* skipSpace <* char ')' instance CanParse Base where pars = - ( (try pars "could not parse numbase in base") >>= \nb -> - (DiceBase <$> parseDice nb) + ( do + nb <- try pars + (DiceBase <$> parseDice nb) <|> return (NBase nb) + -- try pars >>= \nb -> + -- (DiceBase <$> parseDice nb) + -- <|> return (NBase nb) ) - <|> DiceBase <$> try (parseDice (Value 1)) "cannot parse numberless die" + <|> DiceBase <$> parseDice (Value 1) instance CanParse Die where pars = do _ <- try (char 'd') "could not find 'd' for die" lazyFunc <- (try (char '!') $> LazyDie) <|> return id - ( try - ( lazyFunc . CustomDie - <$> pars - ) - "could not parse list values for die" + ( ( lazyFunc . CustomDie + <$> pars ) - <|> lazyFunc . Die - <$> (try pars "couldn't parse base number for die") + "could not parse list values for die" + ) + <|> ( lazyFunc . Die + <$> (pars "couldn't parse base number for die") + ) -- | Given a `NumBase` (the value on the front of a set of dice), construct a -- set of dice. @@ -143,16 +154,14 @@ parseDice nb = parseDice' <*> return (NBase nb) -- This `Base` value is meant to be first value that `Dice` have. parseDice' :: Parser (Base -> Dice) parseDice' = do - d <- try (pars :: Parser Die) "could not parse die in dice" + d <- (pars :: Parser Die) mdor <- parseDieOpRecur - try - ( ( do - bd <- try parseDice' "trying to recurse dice failed" - return (\b -> bd (DiceBase $ Dice b d mdor)) - ) - <|> return (\b -> Dice b d mdor) + + ( do + bd <- try parseDice' "trying to recurse dice failed" + return (\b -> bd (DiceBase $ Dice b d mdor)) ) - "could not recurse dice proper" + <|> return (\b -> Dice b d mdor) -- | Parse a `/=`, `<=`, `>=`, `<`, `=`, `>` as an `AdvancedOrdering`. parseAdvancedOrdering :: Parser AdvancedOrdering @@ -183,11 +192,11 @@ parseDieOpOption = do lazyFunc <- (try (char '!') $> DieOpOptionLazy) <|> return id ( ( (try (string "ro") *> parseAdvancedOrdering >>= \o -> Reroll True o <$> pars) <|> (try (string "rr") *> parseAdvancedOrdering >>= \o -> Reroll False o <$> pars) - <|> ( try + <|> ( ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) ) - "could not parse keep/drop" + "could not parse keep/drop" ) ) <&> lazyFunc diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index cef69b66..60581529 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -29,7 +29,7 @@ import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMenti import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper, skipSpace) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), WithError (WErr), pars) -import Text.Megaparsec (MonadParsec (try), choice, many) +import Text.Megaparsec (MonadParsec (eof, try), choice, many) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -161,7 +161,7 @@ statsCommand = Command "stats" statsCommandParser [] statsCommandParser :: Parser (Message -> DatabaseDiscord ()) statsCommandParser = do firstE <- pars - restEs <- many (try $ skipSpace *> pars) + restEs <- many (skipSpace *> pars) <* eof return $ statsCommand' (firstE : restEs) statsCommand' :: [Expr] -> Message -> DatabaseDiscord () statsCommand' es m = do diff --git a/src/Tablebot/Utility/Parser.hs b/src/Tablebot/Utility/Parser.hs index e4754806..c6bbeced 100644 --- a/src/Tablebot/Utility/Parser.hs +++ b/src/Tablebot/Utility/Parser.hs @@ -193,10 +193,14 @@ inlineCommandHelper open close p action = -- | Parse 0 or more comma separated values. parseCommaSeparated :: Parser a -> Parser [a] parseCommaSeparated p = do - f <- optional $ try p - maybe (return []) (\first' -> (first' :) <$> many (try (skipSpace *> char ',' *> skipSpace) *> p)) f + first <- optional $ try p + case first of + Nothing -> return [] + Just first' -> (first' :) <$> many (try (skipSpace *> char ',' *> skipSpace) *> p) -- | Parse 1 or more comma separated values. parseCommaSeparated1 :: Parser a -> Parser [a] parseCommaSeparated1 p = do - p >>= (\first' -> (first' :) <$> many (try (skipSpace *> char ',' *> skipSpace) *> p)) + first <- p + others <- many (try (skipSpace *> char ',' *> skipSpace) *> p) + return (first : others) diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 7501e8ad..be1f8fab 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -89,7 +89,7 @@ instance {-# OVERLAPPABLE #-} CanParse a => CanParse [a] where -- A parser for @Either a b@ attempts to parse @a@, and if that fails then -- attempts to parse @b@. instance (CanParse a, CanParse b) => CanParse (Either a b) where - pars = (Left <$> pars @a) <|> (Right <$> pars @b) + pars = (Left <$> try (pars @a)) <|> (Right <$> pars @b) -- TODO: automate creation of tuple instances using TemplateHaskell instance (CanParse a, CanParse b) => CanParse (a, b) where @@ -143,7 +143,7 @@ instance KnownSymbol s => CanParse (Exactly s) where newtype WithError (err :: Symbol) x = WErr x instance (KnownSymbol err, CanParse x) => CanParse (WithError err x) where - pars = (WErr <$> try (pars @x)) symbolVal (Proxy :: Proxy err) + pars = (WErr <$> (pars @x)) symbolVal (Proxy :: Proxy err) -- | Parsing implementation for all integral types -- Overlappable due to the really flexible head state From 8d31a24223d07c7086d43bc7c0380ff62be0621c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 16:22:24 +0000 Subject: [PATCH 39/58] qualified post issue --- package.yaml | 1 - src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 30 +++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/package.yaml b/package.yaml index 3e9a3270..493ad1c9 100644 --- a/package.yaml +++ b/package.yaml @@ -74,7 +74,6 @@ dependencies: library: source-dirs: src default-extensions: - - ImportQualifiedPost - OverloadedStrings - LambdaCase - EmptyDataDecls diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 973bd581..fa4366b5 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -17,7 +17,7 @@ import Data.List (sortBy) import Data.List.NonEmpty as NE (fromList) import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Set as S (Set, fromList, map) -import Data.Text qualified as T +import qualified Data.Text as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), @@ -48,12 +48,11 @@ instance CanParse ListValues where do LVBase <$> pars <|> functionParser listFunctions LVFunc - <|> - ( do - nb <- pars - _ <- char '#' - MultipleValues nb <$> pars - ) + <|> ( do + nb <- pars + _ <- char '#' + MultipleValues nb <$> pars + ) instance CanParse ListValuesBase where pars = do @@ -122,12 +121,12 @@ instance (CanParse a) => CanParse (Paren a) where instance CanParse Base where pars = ( do - nb <- try pars - (DiceBase <$> parseDice nb) + nb <- try pars + (DiceBase <$> parseDice nb) <|> return (NBase nb) - -- try pars >>= \nb -> - -- (DiceBase <$> parseDice nb) - -- <|> return (NBase nb) + -- try pars >>= \nb -> + -- (DiceBase <$> parseDice nb) + -- <|> return (NBase nb) ) <|> DiceBase <$> parseDice (Value 1) @@ -192,10 +191,9 @@ parseDieOpOption = do lazyFunc <- (try (char '!') $> DieOpOptionLazy) <|> return id ( ( (try (string "ro") *> parseAdvancedOrdering >>= \o -> Reroll True o <$> pars) <|> (try (string "rr") *> parseAdvancedOrdering >>= \o -> Reroll False o <$> pars) - <|> ( - ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) - <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) - ) + <|> ( ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) + <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) + ) "could not parse keep/drop" ) ) From af8ac8756a155eae67f1d36e502eabfb82ae5a09 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 17:03:30 +0000 Subject: [PATCH 40/58] expanded help message --- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 60581529..6bb81f22 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -218,7 +218,7 @@ statsHelp = "stats" [] "calculate and display statistics for expressions." - "**Roll Stats**\nCan be used to display statistics for expressions of dice.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`" + "**Roll Stats**\nCan be used to display statistics for expressions of dice.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`, `roll stats 3d6dl1+6 4d6dl1`" [] None From dd8fcc8e21c3df2ed312a81390d1350737694de1 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 18:59:08 +0000 Subject: [PATCH 41/58] adding some useful stuff --- package.yaml | 1 + src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.yaml b/package.yaml index 493ad1c9..ea2c1268 100644 --- a/package.yaml +++ b/package.yaml @@ -98,6 +98,7 @@ library: - ScopedTypeVariables - TypeOperators - RankNTypes + - BangPatterns ghc-options: - -Wall diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 6bb81f22..eb03e1c8 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -165,7 +165,7 @@ statsCommand = Command "stats" statsCommandParser [] return $ statsCommand' (firstE : restEs) statsCommand' :: [Expr] -> Message -> DatabaseDiscord () statsCommand' es m = do - mrange' <- liftIO $ timeout (oneSecond * 5) $ mapM (\e -> (,prettyShow e) <$> rangeExpr e) es + mrange' <- liftIO $ timeout (oneSecond * 5) $ mapM (\e -> rangeExpr e >>= \re -> re `seq` return (re, prettyShow e)) es case mrange' of Nothing -> throwBot (EvaluationException "Timed out calculating statistics" []) (Just range') -> do From a1c686565301049b9c0d69a4b5978b0df0e6ffe7 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 19:24:40 +0000 Subject: [PATCH 42/58] clean up and making sure errors don't occur during graphing --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 6312d825..266458eb 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -70,11 +70,14 @@ distributionRenderable d = toRenderable $ do let pb' = pb {_plot_bars_spacing = BarsFixGap 10 5} plot $ return $ plotBars pb' where - ds = M.fromList . D.toList . fst <$> d - allIntegers = S.toList $ S.unions $ M.keysSet <$> ds + removeNullMap m + | M.null m = M.singleton 0 0 + | otherwise = m + ds = removeNullMap . D.toMap . fst <$> d + allIntegers = let s = S.unions $ M.keysSet <$> ds in [S.findMin s .. S.findMax s] insertEmpty k = M.insertWith (\_ a -> a) k 0 ds' = M.unionsWith (++) $ M.map (: []) <$> (applyAll (insertEmpty <$> allIntegers) <$> ds) - pts = bimap fromInteger (fromRational . (* 100) <$>) <$> M.toList ds' + pts = second (fromRational . (* 100) <$>) <$> M.toList ds' r = (fst $ M.findMin ds', fst $ M.findMax ds') applyAll [] = id applyAll (f : fs) = f . applyAll fs From 236134b795cd6589edabbe8c5dd2e1babc57a15d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 31 Jan 2022 19:25:53 +0000 Subject: [PATCH 43/58] tried to move over to an Experiment based system for range instead. not sure how successful this was, but it should at least be on par. --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 92 +++++++++++---------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index c9ccc076..3fc7fb57 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -13,7 +13,7 @@ module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, getStats) where import Control.Monad import Control.Monad.Exception import Data.Bifunctor (Bifunctor (first)) -import Data.Distribution hiding (Distribution, fromList) +import Data.Distribution hiding (Distribution, Experiment, fromList) import qualified Data.Distribution as D import Data.List import Tablebot.Plugins.Roll.Dice.DiceData @@ -22,6 +22,9 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) import Tablebot.Utility.Exception (catchBot) +-- | Alias for an experiment of integers. +type Experiment = D.Experiment Integer + -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. getStats :: Distribution -> ([Integer], Double, Double) @@ -32,26 +35,30 @@ getStats d = (modalOrder, expectation d, standardDeviation d) -- | Convenience wrapper which gets the range of the given values then applies -- the function to the resultant distributions. -combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyShow b) => (Distribution -> Distribution -> Distribution) -> a -> b -> m Distribution +combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyShow b) => (Integer -> Integer -> Integer) -> a -> b -> m Experiment combineRangesBinOp f a b = do d <- range a d' <- range b - return $ f d d' + return $ (f <$> d) <*> d' rangeExpr :: (MonadException m) => Expr -> m Distribution -rangeExpr = (D.fromList . D.toList <$>) . range +rangeExpr e = do + ex <- range e + return $ run ex -- | Type class to get the overall range of a value. -- -- A `Data.Distribution.Distribution` is a map of values to probabilities, and -- has a variety of functions that operate on them. +-- +-- An `Data.Distribution.Experiment` is a monadic form of this. class Range a where - -- | Try and get the `Distribution` of the given value, throwing a + -- | Try and get the `Experiment` of the given value, throwing a -- `MonadException` on failure. - range :: (MonadException m, PrettyShow a) => a -> m Distribution + range :: (MonadException m, PrettyShow a) => a -> m Experiment range a = propagateException (prettyShow a) (range' a) - range' :: (MonadException m, PrettyShow a) => a -> m Distribution + range' :: (MonadException m, PrettyShow a) => a -> m Experiment instance Range Expr where range' (NoExpr t) = range t @@ -65,10 +72,10 @@ instance Range Term where d <- range t d' <- range e -- If 0 is always the denominator, the distribution will be empty. - return $ combineWith div d (assuming (/= 0) d') + return $ (div <$> d) <*> from (assuming (/= 0) (run d')) instance Range Negation where - range' (Neg t) = select negate <$> range t + range' (Neg t) = fmap negate <$> range t range' (NoNeg t) = range t instance Range Expo where @@ -77,14 +84,14 @@ instance Range Expo where d <- range t d' <- range e -- if the exponent is always negative, the distribution will be empty - return $ combineWith (^) d (assuming (>= 0) d') + return $ ((^) <$> d) <*> from (assuming (>= 0) (run d')) instance Range Func where range' (NoFunc t) = range t range' (Func fi avs) = rangeFunction fi avs instance Range NumBase where - range' (Value i) = return $ always i + range' (Value i) = return $ return i range' (NBParen (Paren e)) = range e instance Range Base where @@ -96,62 +103,62 @@ instance Range Die where range' (Die nb) = do nbr <- range nb return $ - run $ do - nbV <- from nbr + do + nbV <- nbr from $ uniform [1 .. nbV] range' (CustomDie lv) = do dievs <- rangeList lv - return $ run $ from dievs >>= from . uniform + return $ dievs >>= from . uniform instance Range Dice where range' (Dice b d mdor) = do b' <- range b d' <- range d let e = do - diecount <- from b' - getDiceExperiment diecount d' + diecount <- b' + getDiceExperiment diecount (run d') res <- rangeDiceExperiment d' mdor e - return $ run $ sum <$> res + return $ sum <$> res -- | Get the distribution of values from a given number of (identically -- distributed) values and the distribution of that value. -getDiceExperiment :: Integer -> Distribution -> Experiment [Integer] -getDiceExperiment i di = replicateM (fromInteger i) (from di) +getDiceExperiment :: Integer -> Distribution -> D.Experiment [Integer] +getDiceExperiment i = replicateM (fromInteger i) . from -- | Go through each operator on dice and modify the `Experiment` representing -- all possible collections of rolls, returning the `Experiment` produced on -- finding `Nothing`. -rangeDiceExperiment :: (MonadException m) => Distribution -> Maybe DieOpRecur -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDiceExperiment :: (MonadException m) => Experiment -> Maybe DieOpRecur -> D.Experiment [Integer] -> m (D.Experiment [Integer]) rangeDiceExperiment _ Nothing is = return is rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor -- | Perform one dice operation on the given `Experiment`, possibly returning -- a modified experiment representing the distribution of dice rolls. -rangeDieOpExperiment :: MonadException m => Distribution -> DieOpOption -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDieOpExperiment :: MonadException m => Experiment -> DieOpOption -> D.Experiment [Integer] -> m (D.Experiment [Integer]) rangeDieOpExperiment die (DieOpOptionLazy o) is = rangeDieOpExperiment die o is rangeDieOpExperiment _ (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is rangeDieOpExperiment die (Reroll rro cond lim) is = do limd <- range lim return $ do - limit <- from limd + limit <- limd let newDie = mkNewDie limit rolls <- is let (count, cutdownRolls) = countTriggers limit rolls if count == 0 then return cutdownRolls - else (cutdownRolls ++) <$> getDiceExperiment count newDie + else (cutdownRolls ++) <$> getDiceExperiment count (run newDie) where mkNewDie limitValue | rro = die - | otherwise = assuming (\i -> not $ applyCompare cond i limitValue) die + | otherwise = from $ assuming (\i -> not $ applyCompare cond i limitValue) (run die) countTriggers limitValue = foldr (\i (c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i : xs')) (0, []) -- | Perform a keep/drop operation on the `Experiment` of dice rolls. -rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> Experiment [Integer] -> m (Experiment [Integer]) +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> D.Experiment [Integer] -> m (D.Experiment [Integer]) rangeDieOpExperimentKD kd (Where cond nb) is = do nbDis <- range nb return $ do - wherelimit <- from nbDis + wherelimit <- nbDis filter (\i -> keepDrop $ applyCompare cond i wherelimit) <$> is where keepDrop @@ -164,7 +171,7 @@ rangeDieOpExperimentKD kd lhw is = do Just nb' -> do nbd <- range nb' return $ do - kdlh <- from nbd + kdlh <- nbd getKeep kdlh . sortBy' <$> is where -- the below exception should never trigger - it is a hold over. it is @@ -174,8 +181,8 @@ rangeDieOpExperimentKD kd lhw is = do sortBy' = sortBy order getKeep = if kd == Keep then genericTake else genericDrop --- | Convenient alias for a distribution of lists of integers. -type DistributionList = D.Distribution [Integer] +-- | Convenient alias for a experiments of lists of integers. +type ExperimentList = D.Experiment [Integer] -- | Type class to get the overall range of a list of values. -- @@ -184,22 +191,21 @@ type DistributionList = D.Distribution [Integer] class RangeList a where -- | Try and get the `DistributionList` of the given value, throwing a -- `MonadException` on failure. - rangeList :: (MonadException m, PrettyShow a) => a -> m DistributionList + rangeList :: (MonadException m, PrettyShow a) => a -> m ExperimentList rangeList a = propagateException (prettyShow a) (rangeList' a) - rangeList' :: (MonadException m, PrettyShow a) => a -> m DistributionList + rangeList' :: (MonadException m, PrettyShow a) => a -> m ExperimentList -- | Take a list of distributions of type a. For add each one, perform an -- experiment where the values in that distribution are prepended to the values -- in the rest of the distribution -spreadDistributions :: (Ord a) => [D.Distribution a] -> Experiment [a] -spreadDistributions [] = return [] -spreadDistributions (d : ds) = from d >>= \d' -> (d' :) <$> spreadDistributions ds +spreadDistributions :: (Ord a) => [D.Experiment a] -> D.Experiment [a] +spreadDistributions = foldr (\d -> (<*>) ((:) <$> d)) (return []) instance RangeList ListValuesBase where rangeList' (LVBList es) = do exprs <- mapM range es - return $ run $ spreadDistributions exprs + return $ spreadDistributions exprs rangeList' (LVBParen (Paren lv)) = rangeList lv instance RangeList ListValues where @@ -208,19 +214,19 @@ instance RangeList ListValues where nbd <- range nb bd <- range b return $ - run $ do - valNum <- from nbd - getDiceExperiment valNum bd + do + valNum <- nbd + getDiceExperiment valNum (run bd) rangeList' (LVFunc fi avs) = rangeFunction fi avs -rangeArgValue :: MonadException m => ArgValue -> m (D.Distribution ListInteger) -rangeArgValue (AVExpr e) = run . (LIInteger <$>) . from <$> range e -rangeArgValue (AVListValues lv) = run . (LIList <$>) . from <$> rangeList lv +rangeArgValue :: MonadException m => ArgValue -> m (D.Experiment ListInteger) +rangeArgValue (AVExpr e) = (LIInteger <$>) <$> range e +rangeArgValue (AVListValues lv) = (LIList <$>) <$> rangeList lv -rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Distribution j) +rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Experiment j) rangeFunction fi exprs = do exprs' <- mapM rangeArgValue exprs let params = first (funcInfoFunc fi) <$> toList (run $ spreadDistributions exprs') - D.fromList <$> foldAndIgnoreErrors params + from . D.fromList <$> foldAndIgnoreErrors params where foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) From 332c928310c57986b3a04b249352b45b33eb68d4 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 12:57:13 +0000 Subject: [PATCH 44/58] removed uneeded extensions --- package.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.yaml b/package.yaml index ea2c1268..38faf5e3 100644 --- a/package.yaml +++ b/package.yaml @@ -107,8 +107,6 @@ executables: tablebot-exe: main: Main.hs source-dirs: app - default-extensions: - - ImportQualifiedPost ghc-options: - -threaded - -rtsopts @@ -120,8 +118,6 @@ tests: tablebot-test: main: Spec.hs source-dirs: test - default-extensions: - - ImportQualifiedPost ghc-options: - -threaded - -rtsopts From 214b939c5f0d769dbff80bc842a42c03be73e1ef Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 13:46:27 +0000 Subject: [PATCH 45/58] added better dice parsing, better command parsing, remvoed some old stuff --- src/Tablebot/Internal/Handler/Command.hs | 2 +- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 13 +++-- src/Tablebot/Plugins/Roll/Plugin.hs | 48 +++++++++---------- src/Tablebot/Utility/SmartParser.hs | 30 ++++-------- 4 files changed, 40 insertions(+), 53 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index b2805188..4e7575ce 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -115,7 +115,7 @@ makeReadable (TrivialError i _ good) = getLabel :: [ErrorItem (Token Text)] -> (Maybe String, [String]) getLabel [] = (Nothing, []) getLabel ((Tokens nel) : xs) = (Nothing, [NE.toList nel]) <> getLabel xs - getLabel ((Label ls) : xs) = (Just (NE.toList ls), []) <> getLabel xs + getLabel ((Label ls) : xs) = (Just (NE.toList ls <> "\n"), []) <> getLabel xs getLabel (EndOfInput : xs) = (Nothing, ["no more input"]) <> getLabel xs makeReadable e = (mapParseError (const UnknownError) e, Nothing) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index fa4366b5..1828eb50 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -36,6 +36,8 @@ import Text.Megaparsec.Error (ErrorItem (Tokens)) failure' :: T.Text -> Set T.Text -> Parser a failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Tokens . NE.fromList . T.unpack) ss) +-- | Custom infix operator to replace the error of a failing parser (regardless +-- of parser position) with a user given error message. () :: Parser a -> String -> Parser a () p s = do r <- observing p @@ -124,9 +126,6 @@ instance CanParse Base where nb <- try pars (DiceBase <$> parseDice nb) <|> return (NBase nb) - -- try pars >>= \nb -> - -- (DiceBase <$> parseDice nb) - -- <|> return (NBase nb) ) <|> DiceBase <$> parseDice (Value 1) @@ -172,7 +171,7 @@ parseAdvancedOrdering = (try (choice opts) "could not parse an ordering") >> -- | Parse a `LowHighWhere`, which is an `h` followed by an integer. parseLowHigh :: Parser LowHighWhere -parseLowHigh = (try (choice @[] $ char <$> "lhw") "could not parse high, low or where") >>= helper +parseLowHigh = ((choice @[] $ char <$> "lhw") "could not parse high, low or where") >>= helper where helper 'h' = High <$> pars helper 'l' = Low <$> pars @@ -194,7 +193,7 @@ parseDieOpOption = do <|> ( ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) ) - "could not parse keep/drop" + "could not parse keep/drop" ) ) <&> lazyFunc @@ -203,8 +202,8 @@ parseDieOpOption = do -- | Parse a single `ArgType` into an `ArgValue`. parseArgValue :: ArgType -> Parser ArgValue -parseArgValue ATIntegerList = AVListValues <$> try pars "could not parse a list value from the argument" -parseArgValue ATInteger = AVExpr <$> try pars "could not parse an integer from the argument" +parseArgValue ATIntegerList = AVListValues <$> pars "could not parse a list value from the argument" +parseArgValue ATInteger = AVExpr <$> pars "could not parse an integer from the argument" -- | Parse a list of comma separated arguments. parseArgValues :: [ArgType] -> Parser [ArgValue] diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index eb03e1c8..8eadf018 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -29,7 +29,7 @@ import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMenti import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper, skipSpace) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), WithError (WErr), pars) -import Text.Megaparsec (MonadParsec (eof, try), choice, many) +import Text.Megaparsec (MonadParsec (eof), many) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -59,33 +59,31 @@ rollDice' e' t m = do -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values -rollDiceParser :: Parser (Message -> DatabaseDiscord ()) -rollDiceParser = choice (try <$> options) - where - justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () - justEither (WErr x) = rollDice' (Just x) Nothing - nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () - nothingAtAll (WErr _) = rollDice' Nothing Nothing - bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () - bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) - justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () - justText (WErr x) = rollDice' Nothing (Just x) - options = - [ parseComm justEither, - parseComm nothingAtAll, - parseComm bothVals, - parseComm justText - ] +-- rollDiceParser :: Parser (Message -> DatabaseDiscord ()) +-- rollDiceParser = choice (try <$> options) +-- where +-- justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () +-- justEither (WErr x) = rollDice' (Just x) Nothing +-- nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () +-- nothingAtAll (WErr _) = rollDice' Nothing Nothing +-- bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () +-- bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) +-- justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () +-- justText (WErr x) = rollDice' Nothing (Just x) +-- options = +-- [ parseComm justEither, +-- parseComm nothingAtAll, +-- parseComm bothVals, +-- parseComm justText +-- ] -- | Basic command for rolling dice. rollDice :: Command rollDice = Command "roll" rollDiceParser [statsCommand] - --- where --- below does not work --- rollDiceParser = parseComm rollDiceParser' --- rollDiceParser' :: WithError "Incorrect rolling format. Please check your expression and quote is of the correct format" (Maybe (Either ListValues Expr), Maybe (Quoted Text)) -> Message -> DatabaseDiscord () --- rollDiceParser' (WErr (x,y)) = rollDice' x y + where + rollDiceParser = parseComm rollDiceParser' + rollDiceParser' :: WithError "Incorrect rolling format. Please check your expression and quote is of the correct format" (Maybe (Either ListValues Expr), Maybe (Quoted Text)) -> Message -> DatabaseDiscord () + rollDiceParser' (WErr (x, y)) = rollDice' x y -- | Rolling dice inline. rollDiceInline :: InlineCommand @@ -169,7 +167,7 @@ statsCommand = Command "stats" statsCommandParser [] case mrange' of Nothing -> throwBot (EvaluationException "Timed out calculating statistics" []) (Just range') -> do - mimage <- liftIO $ timeout (oneSecond * 5) $ distributionByteString range' + mimage <- liftIO $ timeout (oneSecond * 5) (distributionByteString range' >>= \res -> res `seq` return res) case mimage of Nothing -> do sendMessage m (msg range') diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index be1f8fab..fc96955c 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -94,41 +94,31 @@ instance (CanParse a, CanParse b) => CanParse (Either a b) where -- TODO: automate creation of tuple instances using TemplateHaskell instance (CanParse a, CanParse b) => CanParse (a, b) where pars = do - x <- pars @a - skipSpace1 + x <- parsThenMoveToNext @a y <- pars @b return (x, y) instance (CanParse a, CanParse b, CanParse c) => CanParse (a, b, c) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b z <- pars @c return (x, y, z) instance (CanParse a, CanParse b, CanParse c, CanParse d) => CanParse (a, b, c, d) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 - z <- pars @c - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c w <- pars @d return (x, y, z, w) instance (CanParse a, CanParse b, CanParse c, CanParse d, CanParse e) => CanParse (a, b, c, d, e) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 - z <- pars @c - skipSpace1 - w <- pars @d - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c + w <- parsThenMoveToNext @d v <- pars @e return (x, y, z, w, v) From b27256d5e5518c1b08c7262c361f94a71e74e3df Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 14:29:05 +0000 Subject: [PATCH 46/58] limit length of lists to 50, added a random stats function if it needs to be expanded in future --- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 7 ++++++- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index dc43d99d..d4a1b300 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -37,6 +37,9 @@ newtype RNGCount = RNGCount {getRNGCount :: Integer} deriving (Eq, Ord) maximumRNG :: RNGCount maximumRNG = RNGCount 150 +maximumListLength :: Integer +maximumListLength = 50 + -- | Increment the rngcount by 1. incRNGCount :: RNGCount -> RNGCount incRNGCount (RNGCount i) = RNGCount (i + 1) @@ -136,7 +139,9 @@ class IOEvalList a where -- displayed. This function adds the current location to the exception -- callstack. evalShowL :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) - evalShowL rngCount a = propagateException (prettyShow a) (evalShowL' rngCount a) + evalShowL rngCount a = do + (is, mt, rngCount') <- propagateException (prettyShow a) (evalShowL' rngCount a) + return (genericTake maximumListLength is, mt, rngCount') evalShowL' :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 3fc7fb57..22785a29 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -8,7 +8,7 @@ -- -- This plugin generates statistics based on the values of dice in given -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, getStats) where +module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStats) where import Control.Monad import Control.Monad.Exception @@ -46,6 +46,19 @@ rangeExpr e = do ex <- range e return $ run ex +rangeListValues :: (MonadException m) => ListValues -> m [Distribution] +rangeListValues lv = do + lve <- rangeList lv + let lvd = run lve + lvd' = toList lvd + return $ D.fromList <$> zip' lvd' + where + head' [] = [] + head' (x : _) = [x] + getHeads xs = (\(xs', p) -> (,p) <$> head' xs') =<< xs + getTails xs = first tail <$> xs + zip' xs = getHeads xs : zip' (getTails xs) + -- | Type class to get the overall range of a value. -- -- A `Data.Distribution.Distribution` is a map of values to probabilities, and From ec63d64d119a3df694b0b828f23ebd04bd60e064 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 14:33:52 +0000 Subject: [PATCH 47/58] mention list length limitations --- docs/Roll.md | 2 ++ src/Tablebot/Plugins/Roll/Plugin.hs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index 1a1f5df6..a309d235 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -64,6 +64,8 @@ As well as simple expressions, basic list expressions can be formed. You can for As an addendum to custom dice, if a list value is bracketed then it can be used in custom dice. For example, `5d(4#4d6)` rolls five dice, whose sides are determined by rolling 4d6 4 times. Do note that laziness still applies here, meaning that the RNG cap can be very quickly reached. +Lists are limited to 50 items long currently (which is configurable). + ## Functions Here are all the functions, what they take, and what they return. diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 8eadf018..13d56d68 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -109,13 +109,13 @@ Given an expression, evaluate the expression. Can roll inline using |] ++ "`[|to roll|]`." ++ [r| Can use `r` instead of `roll`. -This supports addition, subtraction, multiplication, integer division, exponentiation, parentheses, dice of arbitrary size, dice with custom sides, rerolling dice once on a condition, rerolling dice indefinitely on a condition, keeping or dropping the highest or lowest dice, keeping or dropping dice based on a condition, operating on lists, and using functions like |] +This supports addition, subtraction, multiplication, integer division, exponentiation, parentheses, dice of arbitrary size, dice with custom sides, rerolling dice once on a condition, rerolling dice indefinitely on a condition, keeping or dropping the highest or lowest dice, keeping or dropping dice based on a condition, operating on lists (which have a maximum, configurable size of 50), and using functions like |] ++ unpack (intercalate ", " integerFunctionsList) ++ [r| (which return integers), or functions like |] ++ unpack (intercalate ", " listFunctionsList) ++ [r| (which return lists). -To see a full list of uses and options, please go to . +To see a full list of uses, options and limitations, please go to . *Usage:* - `roll 1d20` -> rolls a twenty sided die and returns the outcome From 292e7f5868dfbd726779fb6f0e20700fb8c26d8d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 15:27:12 +0000 Subject: [PATCH 48/58] moving some stuff around --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 13 ++----------- src/Tablebot/Utility/SmartParser.hs | 9 +++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 1828eb50..59ae6008 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -26,9 +26,9 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions listFunctions, ) import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace) -import Tablebot.Utility.SmartParser (CanParse (..)) +import Tablebot.Utility.SmartParser (CanParse (..), ()) import Tablebot.Utility.Types (Parser) -import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, (), (<|>)) +import Text.Megaparsec (MonadParsec (try), choice, failure, optional, (), (<|>)) import Text.Megaparsec.Char (char, string) import Text.Megaparsec.Error (ErrorItem (Tokens)) @@ -36,15 +36,6 @@ import Text.Megaparsec.Error (ErrorItem (Tokens)) failure' :: T.Text -> Set T.Text -> Parser a failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Tokens . NE.fromList . T.unpack) ss) --- | Custom infix operator to replace the error of a failing parser (regardless --- of parser position) with a user given error message. -() :: Parser a -> String -> Parser a -() p s = do - r <- observing p - case r of - Left _ -> fail s - Right a -> return a - instance CanParse ListValues where pars = do diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index fc96955c..64aa35cd 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -23,6 +23,15 @@ import Tablebot.Utility.Parser import Tablebot.Utility.Types (EnvDatabaseDiscord, Parser) import Text.Megaparsec +-- | Custom infix operator to replace the error of a failing parser (regardless +-- of parser position) with a user given error message. +() :: Parser a -> String -> Parser a +() p s = do + r <- observing p + case r of + Left _ -> fail s + Right a -> return a + -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. -- For example, @Int -> Maybe Text -> Message -> DatabaseDiscord s ()@ builds a From e5f65dca259c591587af5b5a9f53a5a68eaf5dcb Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 16:02:39 +0000 Subject: [PATCH 49/58] undid some work to investigate why parse errors are being weird --- src/Tablebot/Utility/Parser.hs | 4 ---- src/Tablebot/Utility/SmartParser.hs | 30 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Tablebot/Utility/Parser.hs b/src/Tablebot/Utility/Parser.hs index c6bbeced..5bf874cf 100644 --- a/src/Tablebot/Utility/Parser.hs +++ b/src/Tablebot/Utility/Parser.hs @@ -82,10 +82,6 @@ word = some letter nonSpaceWord :: Parser String nonSpaceWord = some notSpace --- | @number@ parses any whole, non-negative number. -number :: Parser Int -number = read <$> some digit - -- | @untilEnd@ gets all of the characters up to the end of the input. untilEnd :: Parser String untilEnd = manyTill anySingle eof diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 64aa35cd..8cd03186 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -103,31 +103,41 @@ instance (CanParse a, CanParse b) => CanParse (Either a b) where -- TODO: automate creation of tuple instances using TemplateHaskell instance (CanParse a, CanParse b) => CanParse (a, b) where pars = do - x <- parsThenMoveToNext @a + x <- pars @a + skipSpace1 y <- pars @b return (x, y) instance (CanParse a, CanParse b, CanParse c) => CanParse (a, b, c) where pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b + x <- pars @a + skipSpace1 + y <- pars @b + skipSpace1 z <- pars @c return (x, y, z) instance (CanParse a, CanParse b, CanParse c, CanParse d) => CanParse (a, b, c, d) where pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b - z <- parsThenMoveToNext @c + x <- pars @a + skipSpace1 + y <- pars @b + skipSpace1 + z <- pars @c + skipSpace1 w <- pars @d return (x, y, z, w) instance (CanParse a, CanParse b, CanParse c, CanParse d, CanParse e) => CanParse (a, b, c, d, e) where pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b - z <- parsThenMoveToNext @c - w <- parsThenMoveToNext @d + x <- pars @a + skipSpace1 + y <- pars @b + skipSpace1 + z <- pars @c + skipSpace1 + w <- pars @d + skipSpace1 v <- pars @e return (x, y, z, w, v) From 1b4d1e2a8778a8b9c4b3f8a3f26997488668e790 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 16:04:09 +0000 Subject: [PATCH 50/58] reverting revertion --- src/Tablebot/Utility/SmartParser.hs | 30 ++++++++++------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 8cd03186..64aa35cd 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -103,41 +103,31 @@ instance (CanParse a, CanParse b) => CanParse (Either a b) where -- TODO: automate creation of tuple instances using TemplateHaskell instance (CanParse a, CanParse b) => CanParse (a, b) where pars = do - x <- pars @a - skipSpace1 + x <- parsThenMoveToNext @a y <- pars @b return (x, y) instance (CanParse a, CanParse b, CanParse c) => CanParse (a, b, c) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b z <- pars @c return (x, y, z) instance (CanParse a, CanParse b, CanParse c, CanParse d) => CanParse (a, b, c, d) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 - z <- pars @c - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c w <- pars @d return (x, y, z, w) instance (CanParse a, CanParse b, CanParse c, CanParse d, CanParse e) => CanParse (a, b, c, d, e) where pars = do - x <- pars @a - skipSpace1 - y <- pars @b - skipSpace1 - z <- pars @c - skipSpace1 - w <- pars @d - skipSpace1 + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c + w <- parsThenMoveToNext @d v <- pars @e return (x, y, z, w, v) From 819b95083c0815f457246015c80718fdafe7ec35 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 3 Feb 2022 16:17:04 +0000 Subject: [PATCH 51/58] went to old method of parsing rolling dice which seems to work better --- src/Tablebot/Plugins/Roll/Plugin.hs | 45 +++++++++++++++-------------- src/Tablebot/Utility/SmartParser.hs | 2 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 13d56d68..4cf08fb2 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -29,7 +29,7 @@ import Tablebot.Utility.Discord (Format (Code), formatText, sendMessage, toMenti import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper, skipSpace) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), WithError (WErr), pars) -import Text.Megaparsec (MonadParsec (eof), many) +import Text.Megaparsec import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -59,31 +59,32 @@ rollDice' e' t m = do -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values --- rollDiceParser :: Parser (Message -> DatabaseDiscord ()) --- rollDiceParser = choice (try <$> options) --- where --- justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () --- justEither (WErr x) = rollDice' (Just x) Nothing --- nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () --- nothingAtAll (WErr _) = rollDice' Nothing Nothing --- bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () --- bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) --- justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () --- justText (WErr x) = rollDice' Nothing (Just x) --- options = --- [ parseComm justEither, --- parseComm nothingAtAll, --- parseComm bothVals, --- parseComm justText --- ] +rollDiceParser :: Parser (Message -> DatabaseDiscord ()) +rollDiceParser = choice (try <$> options) + where + justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () + justEither (WErr x) = rollDice' (Just x) Nothing + nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () + nothingAtAll (WErr _) = rollDice' Nothing Nothing + bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () + bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) + justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () + justText (WErr x) = rollDice' Nothing (Just x) + options = + [ parseComm justEither, + parseComm nothingAtAll, + parseComm bothVals, + parseComm justText + ] -- | Basic command for rolling dice. rollDice :: Command rollDice = Command "roll" rollDiceParser [statsCommand] - where - rollDiceParser = parseComm rollDiceParser' - rollDiceParser' :: WithError "Incorrect rolling format. Please check your expression and quote is of the correct format" (Maybe (Either ListValues Expr), Maybe (Quoted Text)) -> Message -> DatabaseDiscord () - rollDiceParser' (WErr (x, y)) = rollDice' x y + +-- where +-- rollDiceParser = parseComm rollDiceParser' +-- rollDiceParser' :: WithError "Incorrect rolling format. Please check your expression and quote is of the correct format" (Maybe (Either ListValues Expr), Maybe (Quoted Text)) -> Message -> DatabaseDiscord () +-- rollDiceParser' (WErr (x, y)) = rollDice' x y -- | Rolling dice inline. rollDiceInline :: InlineCommand diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 64aa35cd..35375e97 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -142,7 +142,7 @@ instance KnownSymbol s => CanParse (Exactly s) where newtype WithError (err :: Symbol) x = WErr x instance (KnownSymbol err, CanParse x) => CanParse (WithError err x) where - pars = (WErr <$> (pars @x)) symbolVal (Proxy :: Proxy err) + pars = (WErr <$> try (pars @x)) symbolVal (Proxy :: Proxy err) -- | Parsing implementation for all integral types -- Overlappable due to the really flexible head state From e245b4ca3adb2c3e9849e3f64e485a6af29737e0 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 14:11:30 +0000 Subject: [PATCH 52/58] limited exports from DiceParsing --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 59ae6008..bf273319 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -10,7 +10,7 @@ -- -- This plugin contains the tools for parsing Dice. -Wno-orphans is enabled so -- that parsing can occur here instead of in SmartParser or DiceData. -module Tablebot.Plugins.Roll.Dice.DiceParsing where +module Tablebot.Plugins.Roll.Dice.DiceParsing () where import Data.Functor (($>), (<&>)) import Data.List (sortBy) From 08757d95b1bffa01164ea2d72601fbc670df9e90 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 14:25:58 +0000 Subject: [PATCH 53/58] removed uneeded packages --- package.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.yaml b/package.yaml index 38faf5e3..1ce76393 100644 --- a/package.yaml +++ b/package.yaml @@ -61,11 +61,7 @@ dependencies: - Chart-diagrams - diagrams-core - diagrams-lib -- diagrams-postscript -- diagrams-svg - diagrams-rasterific -- SVGFonts -- graphviz - JuicyPixels - split - regex-pcre From 21a95e8430b7c010ff95b3240c39f43a79cc0b74 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 17:17:23 +0000 Subject: [PATCH 54/58] spelling change --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index bf273319..2b881e9b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -130,7 +130,7 @@ instance CanParse Die where "could not parse list values for die" ) <|> ( lazyFunc . Die - <$> (pars "couldn't parse base number for die") + <$> (pars "could not parse base number for die") ) -- | Given a `NumBase` (the value on the front of a set of dice), construct a From c323989db0eda7262e815ada1d0f7b72784f97a9 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 19:04:55 +0000 Subject: [PATCH 55/58] some requested changes (up to dice stats) --- docs/Roll.md | 3 +- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 8 +++++- .../Plugins/Roll/Dice/DiceFunctions.hs | 17 +++++++++++ src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 28 +++++++++++-------- src/Tablebot/Utility/SmartParser.hs | 5 ++++ 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index a309d235..1b5c6127 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -89,7 +89,8 @@ Here are all the functions, what they take, and what they return. - reverse (list) - reverse the list - sort (list) - sort the list in ascending order - take (integer, list) - take the first `n` values from a list, where `n` is the integer given -- between (integer, integer) - generate a list between the two given integers +- between (integer, integer) - generate a list between the two given integers (inclusive) +- concat (list, list) - concatenate two lists together # Statistics diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 1280e4de..a55847e6 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -22,7 +22,13 @@ data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) -- | The type for list values. -data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) [ArgValue] | LVBase ListValuesBase +data ListValues + = -- | Represents `N#B`, where N is a NumBase (numbers, parentheses) and B is a Base (numbase or dice value) + MultipleValues NumBase Base + | -- | Represents a function call with the given arguments + LVFunc (FuncInfoBase [Integer]) [ArgValue] + | -- | A base ListValues value - parentheses or a list of expressions + LVBase ListValuesBase deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 58bba1f9..b24dbd8b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -76,6 +76,7 @@ listFunctionsList = M.keys listFunctions -- each function that returns an integer. listFunctions' :: [FuncInfoBase [Integer]] listFunctions' = + constructFuncInfo "concat" (++) : constructFuncInfo "between" between : constructFuncInfo "drop" (genericDrop @Integer) : constructFuncInfo "take" (genericTake @Integer) : @@ -154,6 +155,10 @@ class ArgCount f => ApplyFunc f where -- on integer values to the function, and a list of `ListInteger`s (which are -- either a list of integers or an integer), and returns a wrapped `j` value, -- which is a value that the function originally returns. + -- + -- The bounds represent the exclusive lower bound, the exclusive upper bound, + -- and an arbitrary function which results in an exception when it is true; + -- say, with division when you want to deny just 0 as a value. applyFunc :: forall m j. (MonadException m, Returns f ~ j) => f -> Integer -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> [ListInteger] -> m j -- | Check whether a given value is within the given bounds. @@ -164,14 +169,23 @@ checkBounds i (ml, mh, bs) | bs i = throwBot $ EvaluationException ("invalid value for function: `" <> show i ++ "`") [] | otherwise = return i +-- This is one of two base cases for applyFunc. This is the case where the +-- return value is an integer. As it is the return value, no arguments are +-- accepted. instance {-# OVERLAPPING #-} ApplyFunc Integer where applyFunc f _ _ [] = return f applyFunc _ args _ _ = throwBot $ EvaluationException ("incorrect number of arguments to function. expected " <> show args <> ", got more than that") [] +-- This is one of two base cases for applyFunc. This is the case where the +-- return value is a list of integers. As it is the return value, no arguments +-- are accepted. instance {-# OVERLAPPING #-} ApplyFunc [Integer] where applyFunc f _ _ [] = return f applyFunc _ args _ _ = throwBot $ EvaluationException ("incorrect number of arguments to function. expected " <> show args <> ", got more than that") [] +-- This is one of two recursive cases for applyFunc. This is the case where the +-- argument value is an integer. If there are no arguments or the argument is +-- of the wrong type, an exception is thrown. instance {-# OVERLAPPABLE #-} (ApplyFunc f) => ApplyFunc (Integer -> f) where applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] where @@ -179,6 +193,9 @@ instance {-# OVERLAPPABLE #-} (ApplyFunc f) => ApplyFunc (Integer -> f) where applyFunc f args bs ((LIInteger x) : xs) = checkBounds x bs >>= \x' -> applyFunc (f x') args bs xs applyFunc _ _ _ (_ : _) = throwBot $ EvaluationException "incorrect type given to function. expected an integer, got a list" [] +-- This is one of two recursive cases for applyFunc. This is the case where the +-- argument value is a list of integers. If there are no arguments or the +-- argument is of the wrong type, an exception is thrown. instance {-# OVERLAPPABLE #-} (ApplyFunc f) => ApplyFunc ([Integer] -> f) where applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] where diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 2b881e9b..afd46bc8 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -53,7 +53,7 @@ instance CanParse ListValuesBase where <$> ( try (char '{' *> skipSpace) *> parseCommaSeparated1 pars <* skipSpace - <* char '}' + <* (char '}' "could not find closing brace for list") ) <|> LVBParen . unnest <$> pars @@ -85,10 +85,12 @@ instance CanParse Func where functionParser :: M.Map T.Text (FuncInfoBase j) -> (FuncInfoBase j -> [ArgValue] -> e) -> Parser e functionParser m mainCons = do - fi <- try (choice (string <$> M.keys m) >>= \t -> return (m M.! t)) "could not find function" + fi <- try (choice (string <$> functionNames) >>= \t -> return (m M.! t)) "could not find function" let ft = funcInfoParameters fi - es <- skipSpace *> string "(" *> skipSpace *> parseArgValues ft <* skipSpace <* (try (string ")") "expected only " ++ show (length ft) ++ " arguments, got more") + es <- skipSpace *> string "(" *> skipSpace *> parseArgValues ft <* skipSpace <* (string ")" "could not find closing bracket on function call") return $ mainCons fi es + where + functionNames = sortBy (\a b -> compare (T.length b) (T.length a)) $ M.keys m instance CanParse Negation where pars = @@ -124,13 +126,15 @@ instance CanParse Die where pars = do _ <- try (char 'd') "could not find 'd' for die" lazyFunc <- (try (char '!') $> LazyDie) <|> return id - ( ( lazyFunc . CustomDie - <$> pars - ) - "could not parse list values for die" - ) - <|> ( lazyFunc . Die - <$> (pars "could not parse base number for die") + lazyFunc + <$> ( (CustomDie . LVBParen <$> try pars <|> Die . NBParen <$> pars) + <|> ( ( CustomDie + <$> pars + "could not parse list values for die" + ) + <|> ( Die <$> pars "could not parse base number for die" + ) + ) ) -- | Given a `NumBase` (the value on the front of a set of dice), construct a @@ -193,8 +197,8 @@ parseDieOpOption = do -- | Parse a single `ArgType` into an `ArgValue`. parseArgValue :: ArgType -> Parser ArgValue -parseArgValue ATIntegerList = AVListValues <$> pars "could not parse a list value from the argument" -parseArgValue ATInteger = AVExpr <$> pars "could not parse an integer from the argument" +parseArgValue ATIntegerList = AVListValues <$> pars "could not parse a list value from the argument" +parseArgValue ATInteger = AVExpr <$> pars "could not parse an integer from the argument" -- | Parse a list of comma separated arguments. parseArgValues :: [ArgType] -> Parser [ArgValue] diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 35375e97..bcaf6b5f 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -25,6 +25,11 @@ import Text.Megaparsec -- | Custom infix operator to replace the error of a failing parser (regardless -- of parser position) with a user given error message. +-- +-- Has some effects on other error parsing. Use if you want the error you give +-- to be the one that is reported (unless this is used at a higher level.) +-- +-- Overwrites/overpowers WithError errors. () :: Parser a -> String -> Parser a () p s = do r <- observing p From 9d77865c7360a1abdca80a656a7a53f16b25aef2 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 19:24:58 +0000 Subject: [PATCH 56/58] more requested changes --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 37 +++++++++++---------- src/Tablebot/Plugins/Roll/Plugin.hs | 4 +++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 22785a29..fba5dbbb 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -23,8 +23,18 @@ import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) import Tablebot.Utility.Exception (catchBot) -- | Alias for an experiment of integers. +-- +-- Where a distribution is a concrete mapping between values and probabilities, +-- an Experiment is more a monadic representation of a Distribution, effectively +-- deferring calculation to the end. +-- +-- I'm not sure if it's more efficient but it certainly makes composing things +-- a lot easier type Experiment = D.Experiment Integer +-- | Convenient alias for a experiments of lists of integers. +type ExperimentList = D.Experiment [Integer] + -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. getStats :: Distribution -> ([Integer], Double, Double) @@ -39,7 +49,7 @@ combineRangesBinOp :: (MonadException m, Range a, Range b, PrettyShow a, PrettyS combineRangesBinOp f a b = do d <- range a d' <- range b - return $ (f <$> d) <*> d' + return $ f <$> d <*> d' rangeExpr :: (MonadException m) => Expr -> m Distribution rangeExpr e = do @@ -85,7 +95,7 @@ instance Range Term where d <- range t d' <- range e -- If 0 is always the denominator, the distribution will be empty. - return $ (div <$> d) <*> from (assuming (/= 0) (run d')) + return $ div <$> d <*> from (assuming (/= 0) (run d')) instance Range Negation where range' (Neg t) = fmap negate <$> range t @@ -97,7 +107,7 @@ instance Range Expo where d <- range t d' <- range e -- if the exponent is always negative, the distribution will be empty - return $ ((^) <$> d) <*> from (assuming (>= 0) (run d')) + return $ (^) <$> d <*> from (assuming (>= 0) (run d')) instance Range Func where range' (NoFunc t) = range t @@ -135,19 +145,19 @@ instance Range Dice where -- | Get the distribution of values from a given number of (identically -- distributed) values and the distribution of that value. -getDiceExperiment :: Integer -> Distribution -> D.Experiment [Integer] +getDiceExperiment :: Integer -> Distribution -> ExperimentList getDiceExperiment i = replicateM (fromInteger i) . from -- | Go through each operator on dice and modify the `Experiment` representing -- all possible collections of rolls, returning the `Experiment` produced on -- finding `Nothing`. -rangeDiceExperiment :: (MonadException m) => Experiment -> Maybe DieOpRecur -> D.Experiment [Integer] -> m (D.Experiment [Integer]) +rangeDiceExperiment :: (MonadException m) => Experiment -> Maybe DieOpRecur -> ExperimentList -> m ExperimentList rangeDiceExperiment _ Nothing is = return is rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor -- | Perform one dice operation on the given `Experiment`, possibly returning -- a modified experiment representing the distribution of dice rolls. -rangeDieOpExperiment :: MonadException m => Experiment -> DieOpOption -> D.Experiment [Integer] -> m (D.Experiment [Integer]) +rangeDieOpExperiment :: MonadException m => Experiment -> DieOpOption -> ExperimentList -> m ExperimentList rangeDieOpExperiment die (DieOpOptionLazy o) is = rangeDieOpExperiment die o is rangeDieOpExperiment _ (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is rangeDieOpExperiment die (Reroll rro cond lim) is = do @@ -167,7 +177,7 @@ rangeDieOpExperiment die (Reroll rro cond lim) is = do countTriggers limitValue = foldr (\i (c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i : xs')) (0, []) -- | Perform a keep/drop operation on the `Experiment` of dice rolls. -rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> D.Experiment [Integer] -> m (D.Experiment [Integer]) +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> ExperimentList -> m ExperimentList rangeDieOpExperimentKD kd (Where cond nb) is = do nbDis <- range nb return $ do @@ -194,9 +204,6 @@ rangeDieOpExperimentKD kd lhw is = do sortBy' = sortBy order getKeep = if kd == Keep then genericTake else genericDrop --- | Convenient alias for a experiments of lists of integers. -type ExperimentList = D.Experiment [Integer] - -- | Type class to get the overall range of a list of values. -- -- Only used within `DiceStats` as I have no interest in producing statistics on @@ -209,16 +216,10 @@ class RangeList a where rangeList' :: (MonadException m, PrettyShow a) => a -> m ExperimentList --- | Take a list of distributions of type a. For add each one, perform an --- experiment where the values in that distribution are prepended to the values --- in the rest of the distribution -spreadDistributions :: (Ord a) => [D.Experiment a] -> D.Experiment [a] -spreadDistributions = foldr (\d -> (<*>) ((:) <$> d)) (return []) - instance RangeList ListValuesBase where rangeList' (LVBList es) = do exprs <- mapM range es - return $ spreadDistributions exprs + return $ sequence exprs rangeList' (LVBParen (Paren lv)) = rangeList lv instance RangeList ListValues where @@ -239,7 +240,7 @@ rangeArgValue (AVListValues lv) = (LIList <$>) <$> rangeList lv rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Experiment j) rangeFunction fi exprs = do exprs' <- mapM rangeArgValue exprs - let params = first (funcInfoFunc fi) <$> toList (run $ spreadDistributions exprs') + let params = first (funcInfoFunc fi) <$> toList (run $ sequence exprs') from . D.fromList <$> foldAndIgnoreErrors params where foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 4cf08fb2..7d49b0a7 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -62,12 +62,16 @@ rollDice' e' t m = do rollDiceParser :: Parser (Message -> DatabaseDiscord ()) rollDiceParser = choice (try <$> options) where + -- Just the value is given to the command, no quote. justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () justEither (WErr x) = rollDice' (Just x) Nothing + -- Nothing is given to the command, a default case. nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () nothingAtAll (WErr _) = rollDice' Nothing Nothing + -- Both the value and the quote are present. bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) + -- Just the quote is given to the command. justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () justText (WErr x) = rollDice' Nothing (Just x) options = From 54521223f182f2bf74fe555ac98429c63e21eb00 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 19:28:33 +0000 Subject: [PATCH 57/58] some more explanations --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index a55847e6..76c94efa 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -32,6 +32,11 @@ data ListValues deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). +-- +-- A basic list value can be understood as one that is indivisible, and/or +-- atomic. They represent either a list value in parentheses, or a list of +-- expressions. Effectively what this is used for is so that these can be used +-- as dice side values. data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] deriving (Show) From 5edfa20fab65922c2149c56e8c9ac007e0eca922 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 5 Feb 2022 19:30:43 +0000 Subject: [PATCH 58/58] moved some stuff around slightly --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index afd46bc8..2560528c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -128,12 +128,8 @@ instance CanParse Die where lazyFunc <- (try (char '!') $> LazyDie) <|> return id lazyFunc <$> ( (CustomDie . LVBParen <$> try pars <|> Die . NBParen <$> pars) - <|> ( ( CustomDie - <$> pars - "could not parse list values for die" - ) - <|> ( Die <$> pars "could not parse base number for die" - ) + <|> ( (CustomDie <$> pars "could not parse list values for die") + <|> (Die <$> pars "could not parse base number for die") ) )