From 9076f8d6c8ab2e712adf1261aede2ed50339d60c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 18 Sep 2025 21:33:48 +0100 Subject: [PATCH 01/31] make Distribution use explicit --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 39 ++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 9e3bb20..600dcde 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -13,7 +13,6 @@ module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStat import Control.Monad import Control.Monad.Exception import Data.Bifunctor (Bifunctor (first)) -import Data.Distribution hiding (Distribution, Experiment, fromList) import qualified Data.Distribution as D import Data.List import qualified Data.Map as M @@ -39,21 +38,21 @@ 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) -getStats d = (modalOrder, expectation d, standardDeviation d) +getStats d = (modalOrder, D.expectation d, D.standardDeviation d) where - vals = toList d + vals = D.toList d modalOrder = fst <$> sortBy (\(_, r) (_, r') -> compare r' r) vals rangeExpr :: (MonadException m) => Expr -> m Distribution rangeExpr e = do ex <- range e - return $ run ex + return $ D.run ex rangeListValues :: (MonadException m) => ListValues -> m [Distribution] rangeListValues lv = do lve <- rangeList lv - let lvd = run lve - lvd' = toList lvd + let lvd = D.run lve + lvd' = D.toList lvd return $ D.fromList <$> zip' lvd' where head' [] = [] @@ -87,10 +86,10 @@ instance (RangeList a) => RangeList (MiscData a) where rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If a -> m (D.Experiment b) rangeIfExpr func (If b t f) = do b' <- range b - let mp = toMap $ run b' + let mp = D.toMap $ D.run b' canBeFalse = M.member 0 mp canBeTrue = not $ M.null $ M.filterWithKey (\k _ -> k /= 0) mp - emptyExp = from $ D.fromList @_ @Integer [] + emptyExp = D.from $ D.fromList @_ @Integer [] t' <- if canBeTrue then func t else return emptyExp f' <- if canBeFalse then func f else return emptyExp return $ @@ -125,7 +124,7 @@ instance Range Term where a' <- at b' <- range b -- If 0 is always the denominator, the distribution will be empty. - return $ getOperation Div <$> a' <*> from (assuming (/= 0) (run b')) + return $ getOperation Div <$> a' <*> D.from (D.assuming (/= 0) (D.run b')) foldel at (typ, b) = do a' <- at b' <- range b @@ -141,7 +140,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 <*> D.from (D.assuming (>= 0) (D.run d')) instance Range Func where range' (NoFunc t) = range t @@ -163,10 +162,10 @@ instance Range Die where return $ do nbV <- nbr - from $ uniform [1 .. nbV] + D.from $ D.uniform [1 .. nbV] range' (CustomDie lv) = do dievs <- rangeList lv - return $ dievs >>= from . uniform + return $ dievs >>= D.from . D.uniform instance Range Dice where range' (Dice b d mdor) = do @@ -174,14 +173,14 @@ instance Range Dice where d' <- range d let e = do diecount <- b' - getDiceExperiment diecount (run d') + getDiceExperiment diecount (D.run d') res <- rangeDiceExperiment d' mdor e 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 -> ExperimentList -getDiceExperiment i = replicateM (fromInteger i) . from +getDiceExperiment i = replicateM (fromInteger i) . D.from -- | Go through each operator on dice and modify the `Experiment` representing -- all possible collections of rolls, returning the `Experiment` produced on @@ -204,12 +203,12 @@ rangeDieOpExperiment die (Reroll rro cond lim) is = do let (count, cutdownRolls) = countTriggers limit rolls if count == 0 then return cutdownRolls - else (cutdownRolls ++) <$> getDiceExperiment count (run newDie) + else (cutdownRolls ++) <$> getDiceExperiment count (D.run newDie) where mkNewDie limitValue | rro = 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, []) + | otherwise = D.from $ D.assuming (\i -> not $ applyCompare cond i limitValue) (D.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 -> ExperimentList -> m ExperimentList @@ -265,7 +264,7 @@ instance RangeList ListValues where return $ do valNum <- nbd - getDiceExperiment valNum (run bd) + getDiceExperiment valNum (D.run bd) rangeList' (LVFunc fi avs) = rangeFunction fi avs rangeList' (ListValuesMisc m) = rangeList m rangeList' b@(LVVar _) = evaluationException "cannot find range of variable" [parseShow b] @@ -277,7 +276,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 $ sequence exprs') - from . D.fromList <$> foldAndIgnoreErrors params + let params = first (funcInfoFunc fi) <$> D.toList (D.run $ sequence exprs') + D.from . D.fromList <$> foldAndIgnoreErrors params where foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) From 5f0659e4dee2d5f0e28602db0f47c6a841dbb613 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 18 Sep 2025 21:34:32 +0100 Subject: [PATCH 02/31] make right folr actually recurse to the right --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 600dcde..6f9323e 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -279,4 +279,4 @@ rangeFunction fi exprs = do let params = first (funcInfoFunc fi) <$> D.toList (D.run $ sequence exprs') D.from . D.fromList <$> foldAndIgnoreErrors params where - foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) + foldAndIgnoreErrors = foldr (\(mv, p) mb -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> mb >>= \b -> return (v ++ b)) (return []) From 8e60413039df549b42752499b858958fb23bd900 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 18 Sep 2025 22:46:06 +0100 Subject: [PATCH 03/31] use a qualified do approach to always use a Distribution instead of Experiment this means that we don't build up a bunch of closures, we just build this map straight away means we have to create a bunch of alternative implementations for basic functions, but that's ok. --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 102 +++++++++--------- .../Plugins/Roll/Dice/DistributionMonad.hs | 24 +++++ tablebot.cabal | 1 + 3 files changed, 79 insertions(+), 48 deletions(-) create mode 100644 src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 6f9323e..5c95401 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE QualifiedDo #-} + -- | -- Module : Tablebot.Plugins.Roll.Dice.DiceStats -- Description : Get statistics on particular expressions. @@ -10,7 +12,6 @@ -- expressions. module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStats) where -import Control.Monad import Control.Monad.Exception import Data.Bifunctor (Bifunctor (first)) import qualified Data.Distribution as D @@ -22,18 +23,11 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions 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 +import qualified Tablebot.Plugins.Roll.Dice.DistributionMonad as DM + +type Experiment = D.Distribution Integer --- | Convenient alias for a experiments of lists of integers. -type ExperimentList = D.Experiment [Integer] +type ExperimentList = D.Distribution [Integer] -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. @@ -46,12 +40,12 @@ getStats d = (modalOrder, D.expectation d, D.standardDeviation d) rangeExpr :: (MonadException m) => Expr -> m Distribution rangeExpr e = do ex <- range e - return $ D.run ex + return $ ex rangeListValues :: (MonadException m) => ListValues -> m [Distribution] rangeListValues lv = do lve <- rangeList lv - let lvd = D.run lve + let lvd = lve lvd' = D.toList lvd return $ D.fromList <$> zip' lvd' where @@ -83,17 +77,17 @@ instance (RangeList a) => RangeList (MiscData a) where rangeList' (MiscVar l) = rangeList l rangeList' (MiscIf i) = rangeIfExpr rangeList i -rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If a -> m (D.Experiment b) +rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Distribution b)) -> If a -> m (D.Distribution b) rangeIfExpr func (If b t f) = do b' <- range b - let mp = D.toMap $ D.run b' + let mp = D.toMap b' canBeFalse = M.member 0 mp canBeTrue = not $ M.null $ M.filterWithKey (\k _ -> k /= 0) mp - emptyExp = D.from $ D.fromList @_ @Integer [] + emptyExp = D.fromList @_ @Integer [] t' <- if canBeTrue then func t else return emptyExp f' <- if canBeFalse then func f else return emptyExp return $ - do + DM.do b'' <- b' if b'' /= 0 then t' else f' @@ -111,7 +105,10 @@ instance (ParseShow typ, Range sub) => Range (BinOp sub typ) where foldel at (typ, b) = do a' <- at b' <- range b - return $ getOperation typ <$> a' <*> b' + return $ DM.do + param1 <- a' + param2 <- b' + DM.return $ getOperation typ param1 param2 instance Range Expr where range' (Expr e) = range e @@ -124,14 +121,20 @@ instance Range Term where a' <- at b' <- range b -- If 0 is always the denominator, the distribution will be empty. - return $ getOperation Div <$> a' <*> D.from (D.assuming (/= 0) (D.run b')) + return $ DM.do + param1 <- a' + param2 <- (D.assuming (/= 0) b') + DM.return $ getOperation Div param1 param2 foldel at (typ, b) = do a' <- at b' <- range b - return $ getOperation typ <$> a' <*> b' + return $ DM.do + param1 <- a' + param2 <- b' + DM.return $ getOperation typ param1 param2 instance Range Negation where - range' (Neg t) = fmap negate <$> range t + range' (Neg t) = (negate DM.<$>) <$> range t range' (NoNeg t) = range t instance Range Expo where @@ -140,14 +143,17 @@ instance Range Expo where d <- range t d' <- range e -- if the exponent is always negative, the distribution will be empty - return $ (^) <$> d <*> D.from (D.assuming (>= 0) (D.run d')) + return $ DM.do + param1 <- d + param2 <- D.assuming (>= 0) d' + DM.return $ param1 ^ param2 instance Range Func where range' (NoFunc t) = range t range' (Func fi avs) = rangeFunction fi avs instance Range NumBase where - range' (Value i) = return $ return i + range' (Value i) = return $ DM.return i range' (NBParen (Paren e)) = range e instance Range Base where @@ -160,27 +166,27 @@ instance Range Die where range' (Die nb) = do nbr <- range nb return $ - do + DM.do nbV <- nbr - D.from $ D.uniform [1 .. nbV] + D.uniform [1 .. nbV] range' (CustomDie lv) = do dievs <- rangeList lv - return $ dievs >>= D.from . D.uniform + return $ dievs DM.>>= D.uniform instance Range Dice where range' (Dice b d mdor) = do b' <- range b d' <- range d - let e = do + let e = DM.do diecount <- b' - getDiceExperiment diecount (D.run d') + getDiceExperiment diecount d' res <- rangeDiceExperiment d' mdor e - return $ sum <$> res + return $ sum DM.<$> res -- | Get the distribution of values from a given number of (identically -- distributed) values and the distribution of that value. getDiceExperiment :: Integer -> Distribution -> ExperimentList -getDiceExperiment i = replicateM (fromInteger i) . D.from +getDiceExperiment i d = DM.sequence $ replicate (fromInteger i) d -- | Go through each operator on dice and modify the `Experiment` representing -- all possible collections of rolls, returning the `Experiment` produced on @@ -196,27 +202,27 @@ 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 + return $ DM.do limit <- limd let newDie = mkNewDie limit rolls <- is let (count, cutdownRolls) = countTriggers limit rolls if count == 0 - then return cutdownRolls - else (cutdownRolls ++) <$> getDiceExperiment count (D.run newDie) + then DM.return cutdownRolls + else (cutdownRolls ++) DM.<$> getDiceExperiment count newDie where mkNewDie limitValue | rro = die - | otherwise = D.from $ D.assuming (\i -> not $ applyCompare cond i limitValue) (D.run die) + | otherwise = D.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 -> ExperimentList -> m ExperimentList rangeDieOpExperimentKD kd (Where cond nb) is = do nbDis <- range nb - return $ do + return $ DM.do wherelimit <- nbDis - filter (\i -> keepDrop $ applyCompare cond i wherelimit) <$> is + filter (\i -> keepDrop $ applyCompare cond i wherelimit) DM.<$> is where keepDrop | kd == Keep = id @@ -227,9 +233,9 @@ rangeDieOpExperimentKD kd lhw is = do Nothing -> whereException Just nb' -> do nbd <- range nb' - return $ do + return $ DM.do kdlh <- nbd - getKeep kdlh . sortBy' <$> is + (getKeep kdlh . sortBy') DM.<$> is where -- the below exception should never trigger - it is a hold over. it is -- present so that this thing type checks nicely. @@ -253,7 +259,7 @@ class (ParseShow a) => RangeList a where instance RangeList ListValuesBase where rangeList' (LVBList es) = do exprs <- mapM range es - return $ sequence exprs + return $ DM.sequence exprs rangeList' (LVBParen (Paren lv)) = rangeList lv instance RangeList ListValues where @@ -262,21 +268,21 @@ instance RangeList ListValues where nbd <- range nb bd <- range b return $ - do + DM.do valNum <- nbd - getDiceExperiment valNum (D.run bd) + getDiceExperiment valNum bd rangeList' (LVFunc fi avs) = rangeFunction fi avs rangeList' (ListValuesMisc m) = rangeList m rangeList' b@(LVVar _) = evaluationException "cannot find range of variable" [parseShow b] -rangeArgValue :: (MonadException m) => ArgValue -> m (D.Experiment ListInteger) -rangeArgValue (AVExpr e) = (LIInteger <$>) <$> range e -rangeArgValue (AVListValues lv) = (LIList <$>) <$> rangeList lv +rangeArgValue :: (MonadException m) => ArgValue -> m (D.Distribution ListInteger) +rangeArgValue (AVExpr e) = (LIInteger DM.<$>) <$> range e +rangeArgValue (AVListValues lv) = (LIList DM.<$>) <$> rangeList lv -rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Experiment j) +rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Distribution j) rangeFunction fi exprs = do exprs' <- mapM rangeArgValue exprs - let params = first (funcInfoFunc fi) <$> D.toList (D.run $ sequence exprs') - D.from . D.fromList <$> foldAndIgnoreErrors params + let params = first (funcInfoFunc fi) <$> D.toList (DM.sequence exprs') + D.fromList <$> foldAndIgnoreErrors params where foldAndIgnoreErrors = foldr (\(mv, p) mb -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> mb >>= \b -> return (v ++ b)) (return []) diff --git a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs new file mode 100644 index 0000000..70d1a37 --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE QualifiedDo #-} + +module Tablebot.Plugins.Roll.Dice.DistributionMonad where + +import Data.Distribution.Core +import Data.Ord (Ord) +import Data.Function (id) + +(>>=) :: (Ord b) => Distribution a -> (a -> Distribution b) -> Distribution b +(>>=) = andThen + +return :: a -> Distribution a +return = always + +(<$>) :: Ord b => (a -> b) -> Distribution a -> Distribution b +(<$>) = select + +traverse :: Ord b => (a -> Distribution b) -> [a] -> Distribution [b] +traverse _ [] = return [] +traverse f (a : as) = f a >>= \b -> (b :) <$> traverse f as + +sequence :: Ord a => [Distribution a] -> Distribution [a] +sequence = traverse id diff --git a/tablebot.cabal b/tablebot.cabal index 15c12c9..07acf60 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -78,6 +78,7 @@ library Tablebot.Plugins.Roll.Dice.DiceParsing Tablebot.Plugins.Roll.Dice.DiceStats Tablebot.Plugins.Roll.Dice.DiceStatsBase + Tablebot.Plugins.Roll.Dice.DistributionMonad Tablebot.Plugins.Roll.Plugin Tablebot.Plugins.Say Tablebot.Plugins.Shibe From 9550ae0ef580a2e46338c615a670ec5327247320 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 19 Sep 2025 18:09:32 +0100 Subject: [PATCH 04/31] don't carry around constraints you don't need to --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 2 +- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index c232f28..284fb31 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -72,7 +72,7 @@ data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] -- | The type for a binary operator between one or more `sub` values data BinOp sub typ where - BinOp :: (Operation typ) => sub -> [(typ, sub)] -> BinOp sub typ + BinOp :: sub -> [(typ, sub)] -> BinOp sub typ deriving instance (Show sub, Show typ) => Show (BinOp sub typ) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 5c95401..114a1e6 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -99,7 +99,7 @@ instance (RangeList a) => RangeList (Var a) where rangeList' (Var _ a) = rangeList a rangeList' (VarLazy _ a) = rangeList a -instance (ParseShow typ, Range sub) => Range (BinOp sub typ) where +instance (ParseShow typ, Operation typ, Range sub) => Range (BinOp sub typ) where range' (BinOp a tas) = foldl' foldel (range a) tas where foldel at (typ, b) = do From aa2d5e5730f281cc08f6ef40bf39642a7e860c54 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 22 Sep 2025 19:26:40 +0100 Subject: [PATCH 05/31] update upstream --- cabal.project | 2 +- stack.yaml | 2 +- stack.yaml.lock | 6 +++--- tablebot.cabal | 3 +++ 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index 8221172..5984dae 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git@github.com:L0neGamer/haskell-distribution.git - tag: 569d6452e4bffedb2c0d3795885fccdb22a4d29d + tag: 313eb7a280b010fda1e21876da4171503c76516f source-repository-package type: git diff --git a/stack.yaml b/stack.yaml index 398de29..3066bfb 100644 --- a/stack.yaml +++ b/stack.yaml @@ -47,7 +47,7 @@ extra-deps: - persistent-2.17.1.0 - svg-builder-0.1.1 - git: https://github.com/L0neGamer/haskell-distribution.git - commit: 569d6452e4bffedb2c0d3795885fccdb22a4d29d + commit: 313eb7a280b010fda1e21876da4171503c76516f - git: https://github.com/L0neGamer/duckling.git commit: f22e3cdd7b77977fe3e9ec75fccd9a0f79c47f97 diff --git a/stack.yaml.lock b/stack.yaml.lock index a53e483..bfea774 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -40,15 +40,15 @@ packages: original: hackage: svg-builder-0.1.1 - completed: - commit: 569d6452e4bffedb2c0d3795885fccdb22a4d29d + commit: 313eb7a280b010fda1e21876da4171503c76516f git: https://github.com/L0neGamer/haskell-distribution.git name: distribution pantry-tree: - sha256: df46a8ef68d35f55bdcf3d6c6e5578cad5680306a7bef4e52da8631cc171c1fc + sha256: b1d8fce7d72787aa7de1c14b65c6a5c633e079da2a1e77b129e0270574231301 size: 808 version: 1.1.1.1 original: - commit: 569d6452e4bffedb2c0d3795885fccdb22a4d29d + commit: 313eb7a280b010fda1e21876da4171503c76516f git: https://github.com/L0neGamer/haskell-distribution.git - completed: commit: f22e3cdd7b77977fe3e9ec75fccd9a0f79c47f97 diff --git a/tablebot.cabal b/tablebot.cabal index 07acf60..1d28177 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -154,6 +154,7 @@ library , exception-transformers , extra , filepath + , hashable , http-client , http-conduit , load-env @@ -214,6 +215,7 @@ executable tablebot-exe , exception-transformers , extra , filepath + , hashable , http-client , http-conduit , load-env @@ -276,6 +278,7 @@ test-suite tablebot-test , exception-transformers , extra , filepath + , hashable , http-client , http-conduit , load-env From 262ab31bba33a3439702ebe159b6d61a019e4cdb Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:12:42 +0100 Subject: [PATCH 06/31] add some useful derived instances --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 46 +++++++++++----------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 284fb31..50c48f6 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -23,29 +23,29 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) -- evaluated `varValue`. -- -- List variables have to be prefixed with `l_`. This really helps with parsing. -data Var a = Var {varName :: Text, varValue :: a} | VarLazy {varName :: Text, varValue :: a} deriving (Show) +data Var a = Var {varName :: Text, varValue :: a} | VarLazy {varName :: Text, varValue :: a} deriving (Show, Eq) -- | If the first value is truthy (non-zero or a non-empty list) then return -- the `thenValue`, else return the `elseValue`. -data If b = If {ifCond :: Expr, thenValue :: b, elseValue :: b} deriving (Show) +data If b = If {ifCond :: Expr, thenValue :: b, elseValue :: b} deriving (Show, Eq) -- | Either an If or a Var that returns a `b`. -data MiscData b = MiscIf (If b) | MiscVar (Var b) deriving (Show) +data MiscData b = MiscIf (If b) | MiscVar (Var b) deriving (Show, Eq) -- | An expression is just an Expr or a ListValues with a semicolon on the end. -- -- When evaluating, VarLazy expressions are handled with a special case - they -- are not evaluated until the value is first referenced. Otherwise, the value -- is evaluated as the statement is encountered -data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) +data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show, Eq) -- | A program is a series of `Statement`s followed by either a `ListValues` or -- an Expr. -data Program = Program [Statement] (Either ListValues Expr) deriving (Show) +data Program = Program [Statement] (Either ListValues Expr) deriving (Show, Eq) -- | The value of an argument given to a function. data ArgValue = AVExpr Expr | AVListValues ListValues - deriving (Show) + deriving (Show, Eq) -- | The type for list values. data ListValues @@ -59,7 +59,7 @@ data ListValues LVVar Text | -- | A misc list values expression. ListValuesMisc (MiscData ListValues) - deriving (Show) + deriving (Show, Eq) -- | The type for basic list values (that can be used as is for custom dice). -- @@ -68,13 +68,11 @@ data ListValues -- 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) + deriving (Show, Eq) -- | The type for a binary operator between one or more `sub` values -data BinOp sub typ where - BinOp :: sub -> [(typ, sub)] -> BinOp sub typ - -deriving instance (Show sub, Show typ) => Show (BinOp sub typ) +data BinOp sub typ = BinOp sub [(typ, sub)] + deriving (Show, Eq) -- | Convenience pattern for the empty list. pattern SingBinOp :: (Operation typ) => sub -> BinOp sub typ @@ -91,11 +89,11 @@ class Operation a where -- -- Represents either a misc expression or additive operations between terms. data Expr = ExprMisc (MiscData Expr) | Expr (BinOp Term ExprType) - deriving (Show) + deriving (Show, Eq) -- | The type of the additive expression, either addition or subtraction. data ExprType = Add | Sub - deriving (Show, Eq) + deriving (Show, Eq, Enum, Bounded) instance Operation ExprType where getOperation Sub = (-) @@ -103,11 +101,11 @@ instance Operation ExprType where -- | Represents multiplicative operations between (possible) negations. newtype Term = Term (BinOp Negation TermType) - deriving (Show) + deriving (Show, Eq) -- | The type of the additive expression, either addition or subtraction. data TermType = Multi | Div - deriving (Show, Eq) + deriving (Show, Eq, Enum, Bounded) instance Operation TermType where getOperation Multi = (*) @@ -115,27 +113,27 @@ instance Operation TermType where -- | The type representing a possibly negated value. data Negation = Neg Expo | NoNeg Expo - deriving (Show) + deriving (Show, Eq) -- | The type representing a value with exponentials. data Expo = Expo Func Expo | NoExpo Func - deriving (Show) + deriving (Show, Eq) -- | The type representing a single function application, or a base item. data Func = Func FuncInfo [ArgValue] | NoFunc Base - deriving (Show) + deriving (Show, Eq) -- | The type representing an integer value or an expression in brackets. data NumBase = NBParen (Paren Expr) | Value Integer - deriving (Show) + deriving (Show, Eq) -- | Container for a parenthesised value. newtype Paren a = Paren a - deriving (Show) + deriving (Show, Eq) -- | The type representing a numeric base value value or a dice value. data Base = NBase NumBase | DiceBase Dice | NumVar Text - deriving (Show) + deriving (Show, Eq) -- Dice Operations after this point @@ -185,7 +183,7 @@ data DieOpOption deriving (Show) -- | A type used to designate how the keep/drop option should work -data LowHighWhere = Low NumBase | High NumBase | Where AdvancedOrdering NumBase deriving (Show) +data LowHighWhere = Low NumBase | High NumBase | Where AdvancedOrdering NumBase deriving (Show, Eq) -- | Utility function to get the integer determining how many values to get -- given a `LowHighWhere`. If the given value is `Low` or `High`, then Just the @@ -201,7 +199,7 @@ isLow (Low _) = True isLow _ = False -- | Utility value for whether to keep or drop values. -data KeepDrop = Keep | Drop deriving (Show, Eq) +data KeepDrop = Keep | Drop deriving (Show, Eq, Enum, Bounded) -- | Utility type class for quickly promoting values. class Converter a b where From e907bbe92e0916fb2e1d0b3888ceb2de52e8c628 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:13:50 +0100 Subject: [PATCH 07/31] add some possibly over engineered types to enforce that lazy values cannot endlessly recurse also use a list instead of recursive maybes for DieOps --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 56 +++++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 50c48f6..1bebb8c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -137,18 +137,33 @@ data Base = NBase NumBase | DiceBase Dice | NumVar Text -- Dice Operations after this point +data Laziness = Lazy | Strict + -- | The type representing a simple N sided die or a custom die, or a lazy one -- of one of those values. -data Die = Die NumBase | CustomDie ListValuesBase | LazyDie Die deriving (Show) +data DieOf (l :: Laziness) where + Die :: NumBase -> DieOf l + CustomDie :: ListValuesBase -> DieOf l + LazyDie :: DieOf Strict -> DieOf Lazy + +deriving instance Show (DieOf l) +deriving instance Eq (DieOf l) + +data Die where + MkDie :: DieOf l -> Die + +deriving instance Show Die +instance Eq Die where + (==) (MkDie die1) (MkDie die2) = case (die1, die2) of + (Die n1, Die n2) -> n1 == n2 + (CustomDie lvb1, CustomDie lvb2) -> lvb1 == lvb2 + (LazyDie do1, LazyDie do2) -> do1 == do2 + _ -> False -- | The type representing a number of dice equal to the `Base` value, and -- possibly some die options. -data Dice = Dice Base Die (Maybe DieOpRecur) - deriving (Show) - --- | The type representing one or more die options. -data DieOpRecur = DieOpRecur DieOpOption (Maybe DieOpRecur) - deriving (Show) +data Dice = Dice NumBase Die [DieOpOption] + deriving (Show, Eq) -- | Some more advanced ordering options for things like `<=` and `/=`. data AdvancedOrdering = Not AdvancedOrdering | OrderingId Ordering | And [AdvancedOrdering] | Or [AdvancedOrdering] @@ -176,11 +191,26 @@ advancedOrderingMapping = (M.fromList lst, M.fromList $ swap <$> lst) -- | The type representing a die option; a reroll, a keep/drop operation, or -- lazily performing some other die option. -data DieOpOption - = Reroll {rerollOnce :: Bool, condition :: AdvancedOrdering, limit :: NumBase} - | DieOpOptionKD KeepDrop LowHighWhere - | DieOpOptionLazy DieOpOption - deriving (Show) +data DieOpOptionOf (l :: Laziness) where + Reroll :: {rerollOnce :: Bool, condition :: AdvancedOrdering, limit :: NumBase} + -> DieOpOptionOf l + DieOpOptionKD :: KeepDrop -> LowHighWhere -> DieOpOptionOf l + DieOpOptionLazy :: DieOpOptionOf Strict -> DieOpOptionOf Lazy + +deriving instance Show (DieOpOptionOf l) +deriving instance Eq (DieOpOptionOf l) + +data DieOpOption where + MkDieOpOption :: DieOpOptionOf l -> DieOpOption + +deriving instance Show DieOpOption +instance Eq DieOpOption where + (==) (MkDieOpOption doo1) (MkDieOpOption doo2) = case (doo1, doo2) of + (Reroll rro1 cond1 lim1, Reroll rro2 cond2 lim2) -> + rro1 == rro2 && cond1 == cond2 && lim1 == lim2 + (DieOpOptionKD kd1 lhw1, DieOpOptionKD kd2 lhw2) -> kd1 == kd2 && lhw1 == lhw2 + (DieOpOptionLazy dooo1, DieOpOptionLazy dooo2) -> dooo1 == dooo2 + _ -> False -- | A type used to designate how the keep/drop option should work data LowHighWhere = Low NumBase | High NumBase | Where AdvancedOrdering NumBase deriving (Show, Eq) @@ -240,7 +270,7 @@ instance Converter Dice Base where promote = DiceBase instance Converter Die Base where - promote d = promote $ Dice (promote (1 :: Integer)) d Nothing + promote d = promote $ Dice (promote (1 :: Integer)) d [] instance Converter [Integer] ListValues where promote = LVBase . LVBList . (promote <$>) From 3f70224434eedcb930c73ba8145eec70f97a405b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:16:00 +0100 Subject: [PATCH 08/31] reflect die and dieopoption changes in eval and stats, as well as misc other places --- src/Tablebot/Plugins/Roll/Dice.hs | 5 +- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 122 ++++++++++---------- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 62 +++++----- src/Tablebot/Plugins/Roll/Plugin.hs | 4 +- 4 files changed, 100 insertions(+), 93 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index c70d1e7..fd8d50b 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -83,7 +83,8 @@ module Tablebot.Plugins.Roll.Dice (evalProgram, evalInteger, evalList, ListValue import Tablebot.Plugins.Roll.Dice.DiceData ( Converter (promote), - Die (Die), + Die (..), + DieOf (..), Expr, ListValues (..), NumBase (Value), @@ -94,4 +95,4 @@ import Tablebot.Plugins.Roll.Dice.DiceParsing () -- | The default expression to evaluate if no expression is given. defaultRoll :: Expr -defaultRoll = promote (Die (Value 20)) +defaultRoll = promote (MkDie (Die (Value 20))) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index ab98561..257f0f1 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -234,31 +234,32 @@ instance IOEval Base where _ -> evaluationException ("could not find integer variable `" <> t <> "`") [] instance IOEval Die where - evalShow' ld@(LazyDie d) = do - (i, _) <- evalShow d - ds <- dieShow Nothing ld [(i, Nothing)] - return (i, ds) - evalShow' d@(CustomDie (LVBList es)) = do - e <- liftIO $ chooseOne es - (i, _) <- evalShow e - ds <- dieShow Nothing d [(i, Nothing)] - incRNGCount - return (i, ds) - evalShow' d@(CustomDie is) = do - (is', _) <- evalShowL is - i <- liftIO $ chooseOne (fst <$> is') - ds <- dieShow Nothing d [(i, Nothing)] - incRNGCount - return (i, ds) - evalShow' d@(Die b) = do - (bound, _) <- evalShow b - if bound < 1 - then evaluationException ("Cannot roll a < 1 sided die (" <> formatText Code (parseShow b) <> ")") [] - else do - i <- randomRIO (1, bound) - ds <- dieShow Nothing d [(i, Nothing)] - incRNGCount - return (i, ds) + evalShow' theDie@(MkDie aDie) = case aDie of + LazyDie d -> do + (i, _) <- evalShow (MkDie d) + ds <- dieShow Nothing (MkDie d) [(i, Nothing)] + return (i, ds) + CustomDie (LVBList es) -> do + e <- liftIO $ chooseOne es + (i, _) <- evalShow e + ds <- dieShow Nothing theDie [(i, Nothing)] + incRNGCount + return (i, ds) + CustomDie is -> do + (is', _) <- evalShowL is + i <- liftIO $ chooseOne (fst <$> is') + ds <- dieShow Nothing theDie [(i, Nothing)] + incRNGCount + return (i, ds) + Die b -> do + (bound, _) <- evalShow b + if bound < 1 + then evaluationException ("Cannot roll a < 1 sided die (" <> formatText Code (parseShow b) <> ")") [] + else do + i <- randomRIO (1, bound) + ds <- dieShow Nothing theDie [(i, Nothing)] + incRNGCount + return (i, ds) instance IOEval Dice where evalShow' dop = do @@ -296,25 +297,26 @@ evalDieOp (Dice b ds dopo) = do rs <- evalDieOp' dopo ds' vs return (sortBy sortByOption rs, crits) where - condenseDie (Die dBase) = do - (i, _) <- evalShow dBase - return (Die (Value i), Just (1, i)) - condenseDie (CustomDie is) = do - (is', _) <- evalShowL is - return (CustomDie (LVBList (promote . fst <$> is')), Nothing) - condenseDie (LazyDie d) = return (d, Nothing) + condenseDie (MkDie d) = case d of + Die dBase -> do + (i, _) <- evalShow dBase + return (MkDie (Die (Value i)), Just (1, i)) + CustomDie is -> do + (is', _) <- evalShowL is + return (MkDie (CustomDie (LVBList (promote . fst <$> is'))), Nothing) + LazyDie d' -> return (MkDie d', Nothing) sortByOption (e :| es, _) (f :| fs, _) | e == f = compare (length fs) (length es) | otherwise = compare e f -- | Utility function that processes a `Maybe DieOpRecur`, when given a die, and -- dice that have already been processed. -evalDieOp' :: Maybe DieOpRecur -> Die -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] -evalDieOp' Nothing _ is = return is -evalDieOp' (Just (DieOpRecur doo mdor)) die is = do - doo' <- processDOO doo +evalDieOp' :: [DieOpOption] -> Die -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] +evalDieOp' [] _ is = return is +evalDieOp' (MkDieOpOption doo : doos) die is = do + doo' <- processDOO is' <- evalDieOp'' doo' die is - evalDieOp' mdor die is' + evalDieOp' doos die is' where processLHW (Low i) = do (i', _) <- evalShow i @@ -325,32 +327,34 @@ evalDieOp' (Just (DieOpRecur doo mdor)) die is = do processLHW (Where o i) = do (i', _) <- evalShow i return (Where o (Value i')) - processDOO (DieOpOptionKD kd lhw) = do - lhw' <- processLHW lhw - return (DieOpOptionKD kd lhw') - processDOO (Reroll once o i) = do - (i', _) <- evalShow i - return (Reroll once o (Value i')) - processDOO (DieOpOptionLazy doo') = return doo' + processDOO = case doo of + DieOpOptionKD kd lhw -> do + lhw' <- processLHW lhw + return $ MkDieOpOption (DieOpOptionKD kd lhw') + Reroll once o i -> do + (i', _) <- evalShow i + return $ MkDieOpOption (Reroll once o (Value i')) + DieOpOptionLazy doo' -> return $ MkDieOpOption doo' -- | Utility function that processes a `DieOpOption`, when given a die, and dice -- that have already been processed. evalDieOp'' :: DieOpOption -> Die -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] -evalDieOp'' (DieOpOptionLazy doo) die is = evalDieOp'' doo die is -evalDieOp'' (DieOpOptionKD kd lhw) _ is = evalDieOpHelpKD kd lhw is -evalDieOp'' (Reroll once o i) die is = foldr rerollF (return []) is - where - rerollF g@(i', b) isRngCount' = do - is' <- isRngCount' - (iEval, _) <- evalShow i - if b && applyCompare o (NE.head i') iEval - then do - (v, _) <- evalShow die - let ret = (v <| i', b) - if once - then return (ret : is') - else rerollF ret (return is') - else return (g : is') +evalDieOp'' (MkDieOpOption doo) die is = case doo of + DieOpOptionLazy doo' -> evalDieOp'' (MkDieOpOption doo') die is + DieOpOptionKD kd lhw -> evalDieOpHelpKD kd lhw is + Reroll once o i -> foldr rerollF (return []) is + where + rerollF g@(i', b) isRngCount' = do + is' <- isRngCount' + (iEval, _) <- evalShow i + if b && applyCompare o (NE.head i') iEval + then do + (v, _) <- evalShow die + let ret = (v <| i', b) + if once + then return (ret : is') + else rerollF ret (return is') + else return (g : is') -- | Given a list of dice values, separate them into kept values and dropped values -- respectively. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 114a1e6..ae3347c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -162,16 +162,17 @@ instance Range Base where range' b@(NumVar _) = evaluationException "cannot find range of variable" [parseShow b] instance Range Die where - range' (LazyDie d) = range d - range' (Die nb) = do - nbr <- range nb - return $ - DM.do - nbV <- nbr - D.uniform [1 .. nbV] - range' (CustomDie lv) = do - dievs <- rangeList lv - return $ dievs DM.>>= D.uniform + range' (MkDie aDie) = case aDie of + LazyDie d -> range (MkDie d) + Die nb -> do + nbr <- range nb + return $ + DM.do + nbV <- nbr + D.uniform [1 .. nbV] + CustomDie lv -> do + dievs <- rangeList lv + return $ dievs DM.>>= D.uniform instance Range Dice where range' (Dice b d mdor) = do @@ -191,30 +192,31 @@ getDiceExperiment i d = DM.sequence $ replicate (fromInteger i) d -- | 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 -> ExperimentList -> m ExperimentList -rangeDiceExperiment _ Nothing is = return is -rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor +rangeDiceExperiment :: (MonadException m) => Experiment -> [DieOpOption] -> ExperimentList -> m ExperimentList +rangeDiceExperiment _ [] is = return is +rangeDiceExperiment die (doo : doos) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die doos -- | Perform one dice operation on the given `Experiment`, possibly returning -- a modified experiment representing the distribution of dice rolls. 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 - limd <- range lim - return $ DM.do - limit <- limd - let newDie = mkNewDie limit - rolls <- is - let (count, cutdownRolls) = countTriggers limit rolls - if count == 0 - then DM.return cutdownRolls - else (cutdownRolls ++) DM.<$> getDiceExperiment count newDie - where - mkNewDie limitValue - | rro = die - | otherwise = D.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, []) +rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of + (DieOpOptionLazy o) -> rangeDieOpExperiment die (MkDieOpOption o) is + (DieOpOptionKD kd lhw) -> rangeDieOpExperimentKD kd lhw is + (Reroll rro cond lim) -> do + limd <- range lim + return $ DM.do + limit <- limd + let newDie = mkNewDie limit + rolls <- is + let (count, cutdownRolls) = countTriggers limit rolls + if count == 0 + then DM.return cutdownRolls + else (cutdownRolls ++) DM.<$> getDiceExperiment count newDie + where + mkNewDie limitValue + | rro = die + | otherwise = D.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 -> ExperimentList -> m ExperimentList diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 348e0e8..a776f89 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -207,8 +207,8 @@ genchar = Command "genchar" (snd $ NE.head rpgSystems') (toCommand <$> NE.toList -- | List of supported genchar systems and the dice used to roll for them rpgSystems :: NE.NonEmpty (Text, ListValues) rpgSystems = - ("dnd", MultipleValues (Value 6) (DiceBase (Dice (NBase (Value 4)) (Die (Value 6)) (Just (DieOpRecur (DieOpOptionKD Drop (Low (Value 1))) Nothing))))) - NE.:| [("wfrp", MultipleValues (Value 8) (NBase (NBParen (Paren (Expr (BinOp (promote (Value 20)) [(Add, promote (Die (Value 10)))]))))))] + ("dnd", MultipleValues (Value 6) (DiceBase (Dice (Value 4) (MkDie (Die (Value 6))) [MkDieOpOption (DieOpOptionKD Drop (Low (Value 1)))]))) + NE.:| [("wfrp", MultipleValues (Value 8) (NBase (NBParen (Paren (Expr (BinOp (promote (Value 20)) [(Add, promote (MkDie (Die (Value 10))))]))))))] -- | Small help page for gen char. gencharHelp :: HelpPage From d33daef6e33c671d20871f5add34a6f497d69a4b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:16:26 +0100 Subject: [PATCH 09/31] add eq for FuncInfoBase (based on name alone) and a adjust show instance --- src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 41be9b8..73792ea 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -128,10 +128,13 @@ funcInfoInsert = FuncInfo "insert" [ATInteger, ATInteger, ATIntegerList] ATInteg -- including types, the function name, and the function itself. data FuncInfoBase j = FuncInfo {funcInfoName :: Text, funcInfoParameters :: [ArgType], funcReturnType :: ArgType, funcInfoFunc :: forall m. (MonadException m) => [ListInteger] -> m j} +instance Eq (FuncInfoBase j) where + (==) fib1 fib2 = funcInfoName fib1 == funcInfoName fib2 + type FuncInfo = FuncInfoBase Integer instance Show (FuncInfoBase j) where - show (FuncInfo fin ft frt _) = "FuncInfo " <> unpack fin <> " " <> show ft <> " " <> show frt + 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. From b8f09ba4a52802e4c1585d13963d064aee8fb02f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:20:02 +0100 Subject: [PATCH 10/31] straightforward parsing adjustments --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index e429021..9421e00 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -87,11 +87,9 @@ instance CanParse ListValues where do functionParser listFunctions LVFunc <|> (LVVar . ("l_" <>) <$> try (string "l_" *> variableName)) - <|> ListValuesMisc - <$> (pars >>= checkVar) - <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) - <|> LVBase - <$> pars + <|> (ListValuesMisc <$> (pars >>= checkVar)) + <|> (MultipleValues <$> (try (pars <* char '#')) <*> pars) + <|> (LVBase <$> pars) where checkVar (MiscVar l) | T.isPrefixOf "l_" (varName l) = return (MiscVar l) @@ -107,11 +105,7 @@ instance CanParse ListValuesBase where <* (char '}' "could not find closing brace for list") ) <|> 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 @@ -183,12 +177,9 @@ instance CanParse Expo where instance CanParse NumBase where pars = - (NBParen . unnest <$> pars) + (NBParen <$> pars) <|> Value <$> integer "could not parse integer" - where - unnest (Paren (Expr (SingBinOp (Term (SingBinOp (NoNeg (NoExpo (NoFunc (NBase (NBParen e)))))))))) = e - unnest e = e instance (CanParse a) => CanParse (Paren a) where pars = try (char '(') *> skipSpace *> (Paren <$> pars) <* skipSpace <* char ')' @@ -348,10 +339,10 @@ instance ParseShow Base where parseShow (NumVar t) = t instance ParseShow Die where - parseShow (Die b) = "d" <> parseShow b - parseShow (CustomDie lv) = "d" <> parseShow lv - -- parseShow (CustomDie is) = "d{" <> intercalate ", " (parseShow <$> is) <> "}" - parseShow (LazyDie d) = "d!" <> T.tail (parseShow d) + parseShow (MkDie die) = case die of + (Die b) -> "d" <> parseShow b + (CustomDie lv) -> "d" <> parseShow lv + (LazyDie d) -> "d!" <> T.tail (parseShow (MkDie d)) instance ParseShow Dice where parseShow (Dice b d dor) = parseShow b <> parseShow d <> helper' dor @@ -360,13 +351,14 @@ instance ParseShow Dice where fromLHW (Where o i) = "w" <> fromOrdering o <> parseShow i fromLHW (Low i) = "l" <> parseShow i fromLHW (High i) = "h" <> parseShow i - helper' Nothing = "" - helper' (Just (DieOpRecur dopo' dor')) = helper dopo' <> helper' dor' - helper (DieOpOptionLazy doo) = "!" <> helper doo - helper (Reroll True o i) = "ro" <> fromOrdering o <> parseShow i - helper (Reroll False o i) = "rr" <> fromOrdering o <> parseShow i - helper (DieOpOptionKD Keep lhw) = "k" <> fromLHW lhw - helper (DieOpOptionKD Drop lhw) = "d" <> fromLHW lhw + helper' [] = "" + helper' (dopo' : dor') = helper dopo' <> helper' dor' + helper (MkDieOpOption doo) = case doo of + DieOpOptionLazy dooo -> "!" <> helper (MkDieOpOption dooo) + Reroll True o i -> "ro" <> fromOrdering o <> parseShow i + Reroll False o i -> "rr" <> fromOrdering o <> parseShow i + DieOpOptionKD Keep lhw -> "k" <> fromLHW lhw + DieOpOptionKD Drop lhw -> "d" <> fromLHW lhw instance (ParseShow a) => ParseShow (Var a) where parseShow (Var t a) = "var " <> t <> " = " <> parseShow a From 7dfb6e2995a828806f77988eee50045efd7b8d0e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:21:02 +0100 Subject: [PATCH 11/31] if we can't parse a standalone die, try to parse a variable name instead --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 9421e00..16085bf 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -191,9 +191,8 @@ instance CanParse Base where (DiceBase <$> parseDice nb) <|> return (NBase nb) ) - <|> DiceBase - <$> parseDice (Value 1) - <|> (NumVar <$> try variableName) + <|> (DiceBase <$> try (parseDice (Value 1))) + <|> (NumVar <$> try variableName) instance CanParse Die where pars = do From d1eadc15f19fc8745ea804f45ca760c2dfea2acb Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:23:40 +0100 Subject: [PATCH 12/31] laziness parsing changes for die and dieopoption, and the numbase change for Dice --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 16085bf..dacf98a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -197,32 +197,32 @@ instance CanParse Base where instance CanParse Die where pars = do _ <- try (char 'd') "could not find 'd' for die" - 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") - ) - ) + optional (char '!') >>= \case + Just _ -> MkDie . LazyDie <$> dieTypes + Nothing -> MkDie <$> dieTypes + where + dieTypes :: Parser (DieOf Strict) + dieTypes = + ( (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 -- set of dice. parseDice :: NumBase -> Parser Dice -parseDice nb = parseDice' <*> return (NBase nb) +parseDice nb = parseDice' <&> ($ 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' :: Parser (NumBase -> Dice) parseDice' = do d <- (pars :: Parser Die) - mdor <- parseDieOpRecur + mdor <- many parseDieOpOption - ( 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) -- | Parse a `/=`, `<=`, `>=`, `<`, `=`, `>` as an `AdvancedOrdering`. parseAdvancedOrdering :: Parser AdvancedOrdering @@ -241,25 +241,22 @@ parseLowHigh = ((choice @[] $ char <$> "lhw") "could not parse high, low or helper 'w' = parseAdvancedOrdering >>= \o -> pars <&> Where o 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) -parseDieOpRecur = do - dopo <- optional parseDieOpOption - maybe (return Nothing) (\dopo' -> Just . DieOpRecur dopo' <$> parseDieOpRecur) dopo - -- | Parse a single die option. parseDieOpOption :: Parser DieOpOption parseDieOpOption = do - lazyFunc <- (try (char '!') $> DieOpOptionLazy) <|> return id - ( ( (try (string "ro") *> parseAdvancedOrdering >>= \o -> Reroll True o <$> pars) + optional (char '!') >>= \case + Nothing -> MkDieOpOption <$> dooParse + Just _ -> MkDieOpOption . DieOpOptionLazy <$> dooParse + where + dooParse :: Parser (DieOpOptionOf Strict) + dooParse = + ( (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) ) "could not parse keep/drop" ) - ) - <&> lazyFunc ) "could not parse dieOpOption - expecting one of the options described in the doc (call `help roll` to access)" From 6acd5bea58dc17eee2c6db3bd3f7ad42a994f581 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:24:05 +0100 Subject: [PATCH 13/31] add error message when the open bracket of a function call is missing --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index dacf98a..00f01c5 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -157,7 +157,12 @@ functionParser m mainCons = do 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 <* (string ")" "could not find closing bracket on function call") + es <- skipSpace *> + try (string "(" ("could not find opening bracket for function call: \"" <> T.unpack (funcInfoName fi) <> "\"")) *> + 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 From 78f67638c6cbeee5f7f8641db42f6f77bcc66040 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 20:25:23 +0100 Subject: [PATCH 14/31] add a roundtrip test to assert that almost everything that we can store as dice we show and parse back! --- package.yaml | 9 +++ tablebot.cabal | 10 ++++ test/Dice/RoundtripSpec.hs | 120 +++++++++++++++++++++++++++++++++++++ test/Spec.hs | 3 +- 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 test/Dice/RoundtripSpec.hs diff --git a/package.yaml b/package.yaml index 2993826..1362f74 100644 --- a/package.yaml +++ b/package.yaml @@ -126,3 +126,12 @@ tests: - -with-rtsopts=-N dependencies: - tablebot + - tasty + - tasty-discover + - tasty-hspec + - tasty-hedgehog + - hspec + - hedgehog + - hspec-hedgehog + build-tools: + - tasty-discover:tasty-discover diff --git a/tablebot.cabal b/tablebot.cabal index 1d28177..e8775d0 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -252,10 +252,13 @@ test-suite tablebot-test type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: + Dice.RoundtripSpec Paths_tablebot hs-source-dirs: test ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-tool-depends: + tasty-discover:tasty-discover build-depends: Chart , Chart-diagrams @@ -279,6 +282,9 @@ test-suite tablebot-test , extra , filepath , hashable + , hedgehog + , hspec + , hspec-hedgehog , http-client , http-conduit , load-env @@ -299,6 +305,10 @@ test-suite tablebot-test , scientific , split , tablebot + , tasty + , tasty-discover + , tasty-hedgehog + , tasty-hspec , template-haskell , text , text-icu diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs new file mode 100644 index 0000000..60fcaf0 --- /dev/null +++ b/test/Dice/RoundtripSpec.hs @@ -0,0 +1,120 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DataKinds #-} + +module Dice.RoundtripSpec where + +import Test.Hspec +import Hedgehog +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range +import Test.Hspec.Hedgehog () +import Data.Kind +import Text.Read +import Tablebot.Plugins.Roll.Dice.DiceParsing +import Tablebot.Plugins.Roll.Dice.DiceFunctions +import Tablebot.Utility.SmartParser.SmartParser +import Tablebot.Utility.Parser +import Text.Megaparsec (runParser, eof) +import Tablebot.Plugins.Roll.Dice.DiceData as Dice +import qualified Data.Text as T +import Control.Monad.IO.Class + +genExpr :: MonadGen m => m Expr +genExpr = + Gen.recursive Gen.choice [Expr <$> genBin genTerm] [Gen.subtermM genExpr $ \expr -> ExprMisc <$> genMisc False (pure expr)] + +genMisc :: MonadGen m => Bool -> m a -> m (MiscData a) +genMisc isList genA = Gen.choice [MiscIf <$> genIf genA, MiscVar <$> genVar isList genA] + +genIf :: MonadGen m => m a -> m (If a) +genIf genA = If <$> genExpr <*> genA <*> genA + +genVarName :: MonadGen m => m T.Text +genVarName = Gen.filterT (\var -> not $ any (`T.isPrefixOf` var) (integerFunctionsList <> listFunctionsList)) + $ Gen.text (Range.linear 3 10) (Gen.element ['a'..'z']) + +genVar :: MonadGen m => Bool -> m a -> m (Dice.Var a) +genVar isList genA = + Dice.Var + <$> ((if isList then ("l_" <>) else id) <$> genVarName) + <*> genA + +genBin :: (MonadGen m, Enum typ, Bounded typ) => m a -> m (BinOp a typ) +genBin genA = + let opCount = Range.exponential 0 4 + in BinOp + <$> genA + <*> Gen.list opCount ((,) <$> Gen.element [minBound..maxBound] <*> genA) + +genTerm :: MonadGen m => m Term +genTerm = Term <$> genBin genNeg + +genNeg :: MonadGen m => m Negation +genNeg = Gen.frequency [(2, NoNeg <$> genExpo), (1, Neg <$> genExpo)] + +genExpo :: MonadGen m => m Expo +genExpo = + Gen.recursive Gen.choice [NoExpo <$> genFunc] [Gen.subtermM genExpo (\expo -> (`Expo` expo) <$> genFunc )] + +genFunc :: MonadGen m => m Func +genFunc = Gen.frequency + [ (5, NoFunc <$> genBase) + -- , (1, Func <$> Gen.element integerFunctions <*> Gen.list (Range.linear 1 2) genArg) + ] + +genArg :: MonadGen m => m ArgValue +genArg = Gen.choice [AVExpr <$> genExpr, AVListValues <$> genListValues] + +genBase :: MonadGen m => m Base +genBase = Gen.frequency [(2, NBase <$> genNumBase), (2, DiceBase <$> genDice), (1, NumVar <$> genVarName)] + +genNumBase :: MonadGen m => m NumBase +genNumBase = + Gen.recursive Gen.choice [Value <$> Gen.integral (Range.linear 0 100)] [NBParen . Paren <$> genExpr] + +genDice :: MonadGen m => m Dice +genDice = Dice <$> genNumBase <*> genDie <*> Gen.list (Range.exponential 0 3) genDieOpOption + +genDie :: MonadGen m => m Die +genDie = Gen.frequency (fmap (fmap (fmap MkDie)) strictDie <> [(1, MkDie . LazyDie <$> Gen.frequency strictDie)]) + where + strictDie :: MonadGen m => [(Int, m (DieOf 'Strict))] + strictDie = [(3, Die <$> genNumBase), (1, CustomDie <$> genListValuesBase)] + +genDieOpOption :: MonadGen m => m DieOpOption +genDieOpOption = Gen.choice (fmap (fmap MkDieOpOption) strictDieOp <> [MkDieOpOption . DieOpOptionLazy <$> Gen.choice strictDieOp]) + where + strictDieOp :: MonadGen m => [m (DieOpOptionOf 'Strict)] + strictDieOp = + [ DieOpOptionKD <$> Gen.element [Keep, Drop] <*> Gen.frequency + [ (2, Gen.element [Low, High] <*> genNumBase) + , (1, Where <$> genAdvancedOrdering <*> genNumBase) + ] + , Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase + ] + +genAdvancedOrdering :: MonadGen m => m AdvancedOrdering +genAdvancedOrdering = Gen.element $ fst advancedOrderingMapping + +genListValuesBase :: MonadGen m => m ListValuesBase +genListValuesBase = Gen.choice + [ LVBList <$> Gen.list (Range.exponential 1 10) genExpr + , LVBParen . Paren <$> genListValues + ] + +genListValues :: MonadGen m => m ListValues +genListValues = Gen.frequency + [ (4, MultipleValues <$> genNumBase <*> genBase) + , (2, LVBase <$> genListValuesBase) + , (2, ListValuesMisc <$> genMisc True genListValues) + -- , (1, LVFunc <$> Gen.element listFunctions <*> Gen.list (Range.linear 1 2) genArg) + , (1, LVVar . ("l_" <>) <$> genVarName) + ] + +spec_roundtrip_dice :: Spec +spec_roundtrip_dice = do + it "roundtrip dice" $ do + dice <- forAll genExpr :: PropertyT IO Expr + liftIO $ print (parseShow dice) + Right dice === runParser (pars <* eof) "" (parseShow dice) + diff --git a/test/Spec.hs b/test/Spec.hs index cd4753f..70c55f5 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -1,2 +1 @@ -main :: IO () -main = putStrLn "Test suite not yet implemented" +{-# OPTIONS_GHC -F -pgmF tasty-discover #-} From 924630a59364b6e615d279736f8b8c4ad41bdf9f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 21:29:00 +0100 Subject: [PATCH 15/31] be more lenient in the if parse case --- 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 00f01c5..8587d10 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -113,7 +113,7 @@ binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) instance (CanParse b) => CanParse (If b) where pars = do - a <- string "if" *> skipSpace1 *> pars <* skipSpace1 + a <- try (string "if" *> skipSpace1) *> pars <* skipSpace1 t <- string "then" *> skipSpace1 *> pars <* skipSpace1 e <- string "else" *> skipSpace1 *> pars return $ If a t e From 234ce262e4360ab77b0abd62de5f1b35c1ac1214 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 24 Sep 2025 21:31:06 +0100 Subject: [PATCH 16/31] specify low/high in a better way --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 10 ++++---- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 8 +++---- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 8 +++---- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 23 ++++++++----------- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- test/Dice/RoundtripSpec.hs | 2 +- 6 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 1bebb8c..c02e181 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -212,20 +212,22 @@ instance Eq DieOpOption where (DieOpOptionLazy dooo1, DieOpOptionLazy dooo2) -> dooo1 == dooo2 _ -> False +data LowHigh = Low | High + deriving (Show, Eq, Enum, Bounded) + -- | A type used to designate how the keep/drop option should work -data LowHighWhere = Low NumBase | High NumBase | Where AdvancedOrdering NumBase deriving (Show, Eq) +data LowHighWhere = LH LowHigh NumBase | Where AdvancedOrdering NumBase deriving (Show, Eq) -- | Utility function to get the integer determining how many values to get -- given a `LowHighWhere`. If the given value is `Low` or `High`, then Just the -- NumBase contained is returned. Else, Nothing is returned. getValueLowHigh :: LowHighWhere -> Maybe NumBase -getValueLowHigh (Low i) = Just i -getValueLowHigh (High i) = Just i +getValueLowHigh (LH _ i) = Just i getValueLowHigh (Where _ _) = Nothing -- | Returns whether the given `LowHighWhere` is `Low` or not. isLow :: LowHighWhere -> Bool -isLow (Low _) = True +isLow (LH Low _) = True isLow _ = False -- | Utility value for whether to keep or drop values. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 257f0f1..3b9186a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -318,12 +318,12 @@ evalDieOp' (MkDieOpOption doo : doos) die is = do is' <- evalDieOp'' doo' die is evalDieOp' doos die is' where - processLHW (Low i) = do + processLHW (LH Low i) = do (i', _) <- evalShow i - return (Low (Value i')) - processLHW (High i) = do + return (LH Low (Value i')) + processLHW (LH High i) = do (i', _) <- evalShow i - return (High (Value i')) + return (LH High (Value i')) processLHW (Where o i) = do (i', _) <- evalShow i return (Where o (Value i')) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 8587d10..6379594 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -241,8 +241,8 @@ parseAdvancedOrdering = (try (choice opts) "could not parse an ordering") >> parseLowHigh :: Parser LowHighWhere parseLowHigh = ((choice @[] $ char <$> "lhw") "could not parse high, low or where") >>= helper where - helper 'h' = High <$> pars - helper 'l' = Low <$> pars + helper 'h' = LH High <$> pars + helper 'l' = LH Low <$> pars helper 'w' = parseAdvancedOrdering >>= \o -> pars <&> Where o helper c = failure' (T.singleton c) (S.fromList ["h", "l", "w"]) @@ -350,8 +350,8 @@ instance ParseShow Dice where where fromOrdering ao = M.findWithDefault "??" ao $ snd advancedOrderingMapping fromLHW (Where o i) = "w" <> fromOrdering o <> parseShow i - fromLHW (Low i) = "l" <> parseShow i - fromLHW (High i) = "h" <> parseShow i + fromLHW (LH Low i) = "l" <> parseShow i + fromLHW (LH High i) = "h" <> parseShow i helper' [] = "" helper' (dopo' : dor') = helper dopo' <> helper' dor' helper (MkDieOpOption doo) = case doo of diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index ae3347c..eff1588 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -229,21 +229,16 @@ rangeDieOpExperimentKD kd (Where cond nb) is = do keepDrop | kd == Keep = id | otherwise = not -rangeDieOpExperimentKD kd lhw is = do - let nb = getValueLowHigh lhw - case nb of - Nothing -> whereException - Just nb' -> do - nbd <- range nb' - return $ DM.do - kdlh <- nbd - (getKeep kdlh . sortBy') DM.<$> is +rangeDieOpExperimentKD kd (LH lw nb) is = do + nbd <- range nb + return $ DM.do + kdlh <- nbd + (getKeep kdlh . sortBy') DM.<$> 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 + order = case lw of + Low -> id + High -> flip + sortBy' = sortBy (order compare) getKeep = if kd == Keep then genericTake else genericDrop -- | Type class to get the overall range of a list of values. diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index a776f89..4abcb2b 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -207,7 +207,7 @@ genchar = Command "genchar" (snd $ NE.head rpgSystems') (toCommand <$> NE.toList -- | List of supported genchar systems and the dice used to roll for them rpgSystems :: NE.NonEmpty (Text, ListValues) rpgSystems = - ("dnd", MultipleValues (Value 6) (DiceBase (Dice (Value 4) (MkDie (Die (Value 6))) [MkDieOpOption (DieOpOptionKD Drop (Low (Value 1)))]))) + ("dnd", MultipleValues (Value 6) (DiceBase (Dice (Value 4) (MkDie (Die (Value 6))) [MkDieOpOption (DieOpOptionKD Drop (LH Low (Value 1)))]))) NE.:| [("wfrp", MultipleValues (Value 8) (NBase (NBParen (Paren (Expr (BinOp (promote (Value 20)) [(Add, promote (MkDie (Die (Value 10))))]))))))] -- | Small help page for gen char. diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs index 60fcaf0..0f58b30 100644 --- a/test/Dice/RoundtripSpec.hs +++ b/test/Dice/RoundtripSpec.hs @@ -87,7 +87,7 @@ genDieOpOption = Gen.choice (fmap (fmap MkDieOpOption) strictDieOp <> [MkDieOpOp strictDieOp :: MonadGen m => [m (DieOpOptionOf 'Strict)] strictDieOp = [ DieOpOptionKD <$> Gen.element [Keep, Drop] <*> Gen.frequency - [ (2, Gen.element [Low, High] <*> genNumBase) + [ (2, LH <$> Gen.element [Low, High] <*> genNumBase) , (1, Where <$> genAdvancedOrdering <*> genNumBase) ] , Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase From 296a0eeee3721a26990e63aaa9b09341791a6774 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 21:40:51 +0100 Subject: [PATCH 17/31] add SortedList, a type for sorted values, and use that for much more efficient dice stats --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 37 ++++++----- .../Plugins/Roll/Dice/DistributionMonad.hs | 5 ++ src/Tablebot/Plugins/Roll/Dice/SortedList.hs | 66 +++++++++++++++++++ tablebot.cabal | 1 + 4 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 src/Tablebot/Plugins/Roll/Dice/SortedList.hs diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index eff1588..7c81749 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -24,10 +24,13 @@ import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) import Tablebot.Utility.Exception (catchBot) import qualified Tablebot.Plugins.Roll.Dice.DistributionMonad as DM +import Tablebot.Plugins.Roll.Dice.SortedList as SL type Experiment = D.Distribution Integer -type ExperimentList = D.Distribution [Integer] +type ExperimentList = D.Distribution (SortedList Integer) + +type DistributionList = D.Distribution [Integer] -- | Get the most common values, the mean, and the standard deviation of a given -- distribution. @@ -182,12 +185,13 @@ instance Range Dice where diecount <- b' getDiceExperiment diecount d' res <- rangeDiceExperiment d' mdor e - return $ sum DM.<$> res + return $! sum DM.<$> res -- | Get the distribution of values from a given number of (identically -- distributed) values and the distribution of that value. getDiceExperiment :: Integer -> Distribution -> ExperimentList -getDiceExperiment i d = DM.sequence $ replicate (fromInteger i) d +getDiceExperiment i d = + DM.sequenceSL (replicate (fromInteger i) d) -- | Go through each operator on dice and modify the `Experiment` representing -- all possible collections of rolls, returning the `Experiment` produced on @@ -210,13 +214,13 @@ rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of rolls <- is let (count, cutdownRolls) = countTriggers limit rolls if count == 0 - then DM.return cutdownRolls - else (cutdownRolls ++) DM.<$> getDiceExperiment count newDie + then DM.return rolls + else (cutdownRolls <>) DM.<$> getDiceExperiment count newDie where mkNewDie limitValue | rro = die | otherwise = D.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, []) + countTriggers limitValue = foldr (\i ~(c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i `SL.insert` xs')) (0, mempty) -- | Perform a keep/drop operation on the `Experiment` of dice rolls. rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> ExperimentList -> m ExperimentList @@ -224,7 +228,7 @@ rangeDieOpExperimentKD kd (Where cond nb) is = do nbDis <- range nb return $ DM.do wherelimit <- nbDis - filter (\i -> keepDrop $ applyCompare cond i wherelimit) DM.<$> is + SL.filter (\i -> keepDrop $ applyCompare cond i wherelimit) DM.<$> is where keepDrop | kd == Keep = id @@ -233,13 +237,14 @@ rangeDieOpExperimentKD kd (LH lw nb) is = do nbd <- range nb return $ DM.do kdlh <- nbd - (getKeep kdlh . sortBy') DM.<$> is + (keepF . lowHighF (fromInteger kdlh)) DM.<$> is where - order = case lw of - Low -> id - High -> flip - sortBy' = sortBy (order compare) - getKeep = if kd == Keep then genericTake else genericDrop + keepF = case kd of + Keep -> fst + Drop -> snd + lowHighF = case lw of + Low -> splitL + High -> splitR -- | Type class to get the overall range of a list of values. -- @@ -248,10 +253,10 @@ rangeDieOpExperimentKD kd (LH lw nb) is = do class (ParseShow a) => RangeList a where -- | Try and get the `DistributionList` of the given value, throwing a -- `MonadException` on failure. - rangeList :: (MonadException m, ParseShow a) => a -> m ExperimentList + rangeList :: (MonadException m, ParseShow a) => a -> m DistributionList rangeList a = propagateException (parseShow a) (rangeList' a) - rangeList' :: (MonadException m, ParseShow a) => a -> m ExperimentList + rangeList' :: (MonadException m, ParseShow a) => a -> m DistributionList instance RangeList ListValuesBase where rangeList' (LVBList es) = do @@ -267,7 +272,7 @@ instance RangeList ListValues where return $ DM.do valNum <- nbd - getDiceExperiment valNum bd + SL.toList DM.<$> getDiceExperiment valNum bd rangeList' (LVFunc fi avs) = rangeFunction fi avs rangeList' (ListValuesMisc m) = rangeList m rangeList' b@(LVVar _) = evaluationException "cannot find range of variable" [parseShow b] diff --git a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs index 70d1a37..d03a6ec 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs @@ -6,6 +6,7 @@ module Tablebot.Plugins.Roll.Dice.DistributionMonad where import Data.Distribution.Core import Data.Ord (Ord) import Data.Function (id) +import Tablebot.Plugins.Roll.Dice.SortedList as SL (>>=) :: (Ord b) => Distribution a -> (a -> Distribution b) -> Distribution b (>>=) = andThen @@ -22,3 +23,7 @@ traverse f (a : as) = f a >>= \b -> (b :) <$> traverse f as sequence :: Ord a => [Distribution a] -> Distribution [a] sequence = traverse id + +sequenceSL :: (Ord a) => [Distribution a] -> Distribution (SortedList a) +sequenceSL [] = return mempty +sequenceSL (da : das) = da >>= \a -> SL.insert a <$> sequenceSL das diff --git a/src/Tablebot/Plugins/Roll/Dice/SortedList.hs b/src/Tablebot/Plugins/Roll/Dice/SortedList.hs new file mode 100644 index 0000000..e23ba32 --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/SortedList.hs @@ -0,0 +1,66 @@ + +module Tablebot.Plugins.Roll.Dice.SortedList + ( SortedList + , toList + , fromList + , insert + , Tablebot.Plugins.Roll.Dice.SortedList.filter + , splitL + , splitR + ) where + +import qualified Data.Map.Strict as M +import qualified Data.Foldable +import Data.Bifunctor + +newtype SortedList a = MkSortedList (M.Map a Int) + deriving (Eq, Ord, Show) + +fromList :: Ord a => [a] -> SortedList a +fromList = MkSortedList . M.fromListWith (+) . fmap (, 1) + +toList :: SortedList a -> [a] +toList (MkSortedList m) = do + (a, incidence) <- M.toAscList m + replicate incidence a + +insert :: Ord a => a -> SortedList a -> SortedList a +insert a (MkSortedList m) = MkSortedList $ M.insertWith (+) a 1 m + +filter :: (a -> Bool) -> SortedList a -> SortedList a +filter p (MkSortedList m) = MkSortedList $ M.filterWithKey (\k _ -> p k) m + +splitR :: Ord a => Int -> SortedList a -> (SortedList a, SortedList a) +splitR i sl@(MkSortedList m) + | i <= 0 = (mempty, sl) + | otherwise = + bimap (MkSortedList . M.fromDescList) (MkSortedList . M.fromDescList) $ + splitCount i (M.toDescList m) + +splitL :: Ord a => Int -> SortedList a -> (SortedList a, SortedList a) +splitL i sl@(MkSortedList m) + | i <= 0 = (mempty, sl) + | otherwise = + bimap (MkSortedList . M.fromAscList) (MkSortedList . M.fromAscList) $ + splitCount i (M.toAscList m) + +-- | Split the list by using the int values associated with each value as a count. +splitCount :: Int -> [(a, Int)] -> ([(a, Int)], [(a, Int)]) +splitCount 0 as = ([], as) +splitCount _ [] = ([], []) +splitCount i (aTup@(a, i'):as) + | i >= i' = first (aTup :) $ splitCount (i - i') as + | otherwise = ([(a, i)],(a,i' - i):as) + +instance Foldable SortedList where + foldMap f = foldMap f . toList + sum (MkSortedList m) = M.foldlWithKey' + (\acc k incidence -> acc + k * (fromIntegral incidence)) 0 m + toList = Tablebot.Plugins.Roll.Dice.SortedList.toList + length (MkSortedList m) = sum m + +instance Ord a => Semigroup (SortedList a) where + (MkSortedList m1) <> (MkSortedList m2) = MkSortedList (M.unionWith (+) m1 m2) + +instance Ord a => Monoid (SortedList a) where + mempty = MkSortedList M.empty diff --git a/tablebot.cabal b/tablebot.cabal index e8775d0..16db8c6 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -79,6 +79,7 @@ library Tablebot.Plugins.Roll.Dice.DiceStats Tablebot.Plugins.Roll.Dice.DiceStatsBase Tablebot.Plugins.Roll.Dice.DistributionMonad + Tablebot.Plugins.Roll.Dice.SortedList Tablebot.Plugins.Roll.Plugin Tablebot.Plugins.Say Tablebot.Plugins.Shibe From e20cf9224a4b46d73c07a0d8c170a4c184d39a7d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 20:19:35 +0100 Subject: [PATCH 18/31] massive cleanups --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 22 ++++++++----------- .../Plugins/Roll/Dice/DistributionMonad.hs | 6 +++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 7c81749..a30c3c1 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, rangeListValues, getStats) where import Control.Monad.Exception -import Data.Bifunctor (Bifunctor (first)) +import Data.Bifunctor import qualified Data.Distribution as D import Data.List import qualified Data.Map as M @@ -26,9 +26,7 @@ import Tablebot.Utility.Exception (catchBot) import qualified Tablebot.Plugins.Roll.Dice.DistributionMonad as DM import Tablebot.Plugins.Roll.Dice.SortedList as SL -type Experiment = D.Distribution Integer - -type ExperimentList = D.Distribution (SortedList Integer) +type DistributionSortedList = D.Distribution (SortedList Integer) type DistributionList = D.Distribution [Integer] @@ -62,15 +60,13 @@ rangeListValues lv = do -- -- 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 (ParseShow a) => Range a where - -- | Try and get the `Experiment` of the given value, throwing a + -- | Try and get the `Distribution` of the given value, throwing a -- `MonadException` on failure. - range :: (MonadException m, ParseShow a) => a -> m Experiment + range :: (MonadException m, ParseShow a) => a -> m Distribution range a = propagateException (parseShow a) (range' a) - range' :: (MonadException m, ParseShow a) => a -> m Experiment + range' :: (MonadException m, ParseShow a) => a -> m Distribution instance (Range a) => Range (MiscData a) where range' (MiscVar l) = range l @@ -189,20 +185,20 @@ 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 -> ExperimentList +getDiceExperiment :: Integer -> Distribution -> DistributionSortedList getDiceExperiment i d = DM.sequenceSL (replicate (fromInteger i) d) -- | 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 -> [DieOpOption] -> ExperimentList -> m ExperimentList +rangeDiceExperiment :: (MonadException m) => Distribution -> [DieOpOption] -> DistributionSortedList -> m DistributionSortedList rangeDiceExperiment _ [] is = return is rangeDiceExperiment die (doo : doos) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die doos -- | Perform one dice operation on the given `Experiment`, possibly returning -- a modified experiment representing the distribution of dice rolls. -rangeDieOpExperiment :: (MonadException m) => Experiment -> DieOpOption -> ExperimentList -> m ExperimentList +rangeDieOpExperiment :: (MonadException m) => Distribution -> DieOpOption -> DistributionSortedList -> m DistributionSortedList rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of (DieOpOptionLazy o) -> rangeDieOpExperiment die (MkDieOpOption o) is (DieOpOptionKD kd lhw) -> rangeDieOpExperimentKD kd lhw is @@ -223,7 +219,7 @@ rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of countTriggers limitValue = foldr (\i ~(c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i `SL.insert` xs')) (0, mempty) -- | Perform a keep/drop operation on the `Experiment` of dice rolls. -rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> ExperimentList -> m ExperimentList +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> DistributionSortedList -> m DistributionSortedList rangeDieOpExperimentKD kd (Where cond nb) is = do nbDis <- range nb return $ DM.do diff --git a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs index d03a6ec..6d1a7f0 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs @@ -6,6 +6,7 @@ module Tablebot.Plugins.Roll.Dice.DistributionMonad where import Data.Distribution.Core import Data.Ord (Ord) import Data.Function (id) +import Data.Monoid import Tablebot.Plugins.Roll.Dice.SortedList as SL (>>=) :: (Ord b) => Distribution a -> (a -> Distribution b) -> Distribution b @@ -19,9 +20,10 @@ return = always traverse :: Ord b => (a -> Distribution b) -> [a] -> Distribution [b] traverse _ [] = return [] -traverse f (a : as) = f a >>= \b -> (b :) <$> traverse f as +traverse f (a : as) = + f a >>= \b -> (b :) <$> traverse f as -sequence :: Ord a => [Distribution a] -> Distribution [a] +sequence :: (Ord a) => [Distribution a] -> Distribution [a] sequence = traverse id sequenceSL :: (Ord a) => [Distribution a] -> Distribution (SortedList a) From 15b4e7c41fdad4c478febc37f10f2f30869fff1f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 20:28:00 +0100 Subject: [PATCH 19/31] move methods to the top level --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index a30c3c1..1717574 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -56,16 +56,16 @@ rangeListValues lv = do getTails xs = first (drop 1) <$> xs zip' xs = getHeads xs : zip' (getTails xs) +-- | Try and get the `Distribution` of the given value, throwing a +-- `MonadException` on failure. +range :: (MonadException m, Range a, ParseShow a) => a -> m Distribution +range a = propagateException (parseShow a) (range' a) + -- | 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. class (ParseShow a) => Range a where - -- | Try and get the `Distribution` of the given value, throwing a - -- `MonadException` on failure. - range :: (MonadException m, ParseShow a) => a -> m Distribution - range a = propagateException (parseShow a) (range' a) - range' :: (MonadException m, ParseShow a) => a -> m Distribution instance (Range a) => Range (MiscData a) where @@ -242,16 +242,16 @@ rangeDieOpExperimentKD kd (LH lw nb) is = do Low -> splitL High -> splitR +-- | Try and get the `DistributionList` of the given value, throwing a +-- `MonadException` on failure. +rangeList :: (MonadException m, RangeList a, ParseShow a) => a -> m DistributionList +rangeList a = propagateException (parseShow a) (rangeList' a) + -- | 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 (ParseShow a) => RangeList a where - -- | Try and get the `DistributionList` of the given value, throwing a - -- `MonadException` on failure. - rangeList :: (MonadException m, ParseShow a) => a -> m DistributionList - rangeList a = propagateException (parseShow a) (rangeList' a) - rangeList' :: (MonadException m, ParseShow a) => a -> m DistributionList instance RangeList ListValuesBase where From f910fdf546e4cfa3eb2895e6ce01c8195c160960 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 20:29:45 +0100 Subject: [PATCH 20/31] cleanup test a bit --- test/Dice/RoundtripSpec.hs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs index 0f58b30..55ecf39 100644 --- a/test/Dice/RoundtripSpec.hs +++ b/test/Dice/RoundtripSpec.hs @@ -8,16 +8,13 @@ import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import Test.Hspec.Hedgehog () -import Data.Kind -import Text.Read -import Tablebot.Plugins.Roll.Dice.DiceParsing +import Tablebot.Plugins.Roll.Dice.DiceParsing () import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Utility.SmartParser.SmartParser import Tablebot.Utility.Parser import Text.Megaparsec (runParser, eof) import Tablebot.Plugins.Roll.Dice.DiceData as Dice import qualified Data.Text as T -import Control.Monad.IO.Class genExpr :: MonadGen m => m Expr genExpr = @@ -115,6 +112,5 @@ spec_roundtrip_dice :: Spec spec_roundtrip_dice = do it "roundtrip dice" $ do dice <- forAll genExpr :: PropertyT IO Expr - liftIO $ print (parseShow dice) Right dice === runParser (pars <* eof) "" (parseShow dice) From 1ed3a804e9a104385c9eb919c9e88dc7a0e06cd4 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 20:52:52 +0100 Subject: [PATCH 21/31] properly add function calls to roundtrip generators --- test/Dice/RoundtripSpec.hs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs index 55ecf39..115e73d 100644 --- a/test/Dice/RoundtripSpec.hs +++ b/test/Dice/RoundtripSpec.hs @@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DataKinds #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE LambdaCase #-} module Dice.RoundtripSpec where @@ -15,6 +17,7 @@ import Tablebot.Utility.Parser import Text.Megaparsec (runParser, eof) import Tablebot.Plugins.Roll.Dice.DiceData as Dice import qualified Data.Text as T +import Data.Traversable genExpr :: MonadGen m => m Expr genExpr = @@ -56,9 +59,16 @@ genExpo = genFunc :: MonadGen m => m Func genFunc = Gen.frequency [ (5, NoFunc <$> genBase) - -- , (1, Func <$> Gen.element integerFunctions <*> Gen.list (Range.linear 1 2) genArg) + , (1, functionGen Func integerFunctions) ] +functionGen :: (Foldable f, MonadGen m) => (FuncInfoBase j -> [ArgValue] -> r) -> f (FuncInfoBase j) -> m r +functionGen cons functions = Gen.element functions >>= \func@(FuncInfo {..}) -> + cons func <$> for funcInfoParameters (\case + ATInteger -> AVExpr <$> genExpr + ATIntegerList -> AVListValues <$> genListValues + ) + genArg :: MonadGen m => m ArgValue genArg = Gen.choice [AVExpr <$> genExpr, AVListValues <$> genListValues] @@ -100,13 +110,15 @@ genListValuesBase = Gen.choice ] genListValues :: MonadGen m => m ListValues -genListValues = Gen.frequency - [ (4, MultipleValues <$> genNumBase <*> genBase) - , (2, LVBase <$> genListValuesBase) - , (2, ListValuesMisc <$> genMisc True genListValues) - -- , (1, LVFunc <$> Gen.element listFunctions <*> Gen.list (Range.linear 1 2) genArg) - , (1, LVVar . ("l_" <>) <$> genVarName) - ] +genListValues = + Gen.recursive Gen.choice + [ LVVar . ("l_" <>) <$> genVarName + ] + [ MultipleValues <$> genNumBase <*> genBase + , LVBase <$> genListValuesBase + , ListValuesMisc <$> genMisc True genListValues + , functionGen LVFunc listFunctions + ] spec_roundtrip_dice :: Spec spec_roundtrip_dice = do From 125acf65adfed7f56344adaab6ee49236dc06f36 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 21:22:14 +0100 Subject: [PATCH 22/31] remove hashable --- tablebot.cabal | 3 --- 1 file changed, 3 deletions(-) diff --git a/tablebot.cabal b/tablebot.cabal index 16db8c6..3780c7d 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -155,7 +155,6 @@ library , exception-transformers , extra , filepath - , hashable , http-client , http-conduit , load-env @@ -216,7 +215,6 @@ executable tablebot-exe , exception-transformers , extra , filepath - , hashable , http-client , http-conduit , load-env @@ -282,7 +280,6 @@ test-suite tablebot-test , exception-transformers , extra , filepath - , hashable , hedgehog , hspec , hspec-hedgehog From 1ab6abcfbb2b0011832fca4dc6bae7369a0e87d1 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 21:27:56 +0100 Subject: [PATCH 23/31] ormolu --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 9 +- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 43 ++--- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 3 +- .../Plugins/Roll/Dice/DistributionMonad.hs | 8 +- src/Tablebot/Plugins/Roll/Dice/SortedList.hs | 59 +++---- test/Dice/RoundtripSpec.hs | 149 ++++++++++-------- 6 files changed, 144 insertions(+), 127 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index c02e181..d3b3512 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -147,12 +147,14 @@ data DieOf (l :: Laziness) where LazyDie :: DieOf Strict -> DieOf Lazy deriving instance Show (DieOf l) + deriving instance Eq (DieOf l) data Die where MkDie :: DieOf l -> Die deriving instance Show Die + instance Eq Die where (==) (MkDie die1) (MkDie die2) = case (die1, die2) of (Die n1, Die n2) -> n1 == n2 @@ -192,18 +194,21 @@ advancedOrderingMapping = (M.fromList lst, M.fromList $ swap <$> lst) -- | The type representing a die option; a reroll, a keep/drop operation, or -- lazily performing some other die option. data DieOpOptionOf (l :: Laziness) where - Reroll :: {rerollOnce :: Bool, condition :: AdvancedOrdering, limit :: NumBase} - -> DieOpOptionOf l + Reroll :: + {rerollOnce :: Bool, condition :: AdvancedOrdering, limit :: NumBase} -> + DieOpOptionOf l DieOpOptionKD :: KeepDrop -> LowHighWhere -> DieOpOptionOf l DieOpOptionLazy :: DieOpOptionOf Strict -> DieOpOptionOf Lazy deriving instance Show (DieOpOptionOf l) + deriving instance Eq (DieOpOptionOf l) data DieOpOption where MkDieOpOption :: DieOpOptionOf l -> DieOpOption deriving instance Show DieOpOption + instance Eq DieOpOption where (==) (MkDieOpOption doo1) (MkDieOpOption doo2) = case (doo1, doo2) of (Reroll rro1 cond1 lim1, Reroll rro2 cond2 lim2) -> diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 6379594..37065fc 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -157,12 +157,13 @@ functionParser m mainCons = do fi <- try (choice (string <$> functionNames) >>= \t -> return (m M.! t)) "could not find function" let ft = funcInfoParameters fi - es <- skipSpace *> - try (string "(" ("could not find opening bracket for function call: \"" <> T.unpack (funcInfoName fi) <> "\"")) *> - skipSpace *> - parseArgValues ft - <* skipSpace - <* (string ")" "could not find closing bracket on function call") + es <- + skipSpace + *> try (string "(" ("could not find opening bracket for function call: \"" <> T.unpack (funcInfoName fi) <> "\"")) + *> 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 @@ -207,12 +208,12 @@ instance CanParse Die where Nothing -> MkDie <$> dieTypes where dieTypes :: Parser (DieOf Strict) - dieTypes = + dieTypes = ( (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") + ) + ) -- | Given a `NumBase` (the value on the front of a set of dice), construct a -- set of dice. @@ -253,17 +254,17 @@ parseDieOpOption = do Nothing -> MkDieOpOption <$> dooParse Just _ -> MkDieOpOption . DieOpOptionLazy <$> dooParse where - dooParse :: Parser (DieOpOptionOf Strict) - dooParse = - ( (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) + dooParse :: Parser (DieOpOptionOf Strict) + dooParse = + ( (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) + ) + "could not parse keep/drop" ) - "could not parse keep/drop" - ) - ) - "could not parse dieOpOption - expecting one of the options described in the doc (call `help roll` to access)" + ) + "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 diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 1717574..c162565 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -21,10 +21,9 @@ import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) -import Tablebot.Utility.Exception (catchBot) - import qualified Tablebot.Plugins.Roll.Dice.DistributionMonad as DM import Tablebot.Plugins.Roll.Dice.SortedList as SL +import Tablebot.Utility.Exception (catchBot) type DistributionSortedList = D.Distribution (SortedList Integer) diff --git a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs index 6d1a7f0..d045bfe 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs @@ -1,12 +1,12 @@ -{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE QualifiedDo #-} +{-# LANGUAGE NoImplicitPrelude #-} module Tablebot.Plugins.Roll.Dice.DistributionMonad where import Data.Distribution.Core -import Data.Ord (Ord) import Data.Function (id) import Data.Monoid +import Data.Ord (Ord) import Tablebot.Plugins.Roll.Dice.SortedList as SL (>>=) :: (Ord b) => Distribution a -> (a -> Distribution b) -> Distribution b @@ -15,10 +15,10 @@ import Tablebot.Plugins.Roll.Dice.SortedList as SL return :: a -> Distribution a return = always -(<$>) :: Ord b => (a -> b) -> Distribution a -> Distribution b +(<$>) :: (Ord b) => (a -> b) -> Distribution a -> Distribution b (<$>) = select -traverse :: Ord b => (a -> Distribution b) -> [a] -> Distribution [b] +traverse :: (Ord b) => (a -> Distribution b) -> [a] -> Distribution [b] traverse _ [] = return [] traverse f (a : as) = f a >>= \b -> (b :) <$> traverse f as diff --git a/src/Tablebot/Plugins/Roll/Dice/SortedList.hs b/src/Tablebot/Plugins/Roll/Dice/SortedList.hs index e23ba32..c2f690f 100644 --- a/src/Tablebot/Plugins/Roll/Dice/SortedList.hs +++ b/src/Tablebot/Plugins/Roll/Dice/SortedList.hs @@ -1,66 +1,69 @@ +module Tablebot.Plugins.Roll.Dice.SortedList + ( SortedList, + toList, + fromList, + insert, + Tablebot.Plugins.Roll.Dice.SortedList.filter, + splitL, + splitR, + ) +where -module Tablebot.Plugins.Roll.Dice.SortedList - ( SortedList - , toList - , fromList - , insert - , Tablebot.Plugins.Roll.Dice.SortedList.filter - , splitL - , splitR - ) where - -import qualified Data.Map.Strict as M -import qualified Data.Foldable import Data.Bifunctor +import qualified Data.Foldable +import qualified Data.Map.Strict as M newtype SortedList a = MkSortedList (M.Map a Int) deriving (Eq, Ord, Show) -fromList :: Ord a => [a] -> SortedList a -fromList = MkSortedList . M.fromListWith (+) . fmap (, 1) +fromList :: (Ord a) => [a] -> SortedList a +fromList = MkSortedList . M.fromListWith (+) . fmap (,1) toList :: SortedList a -> [a] toList (MkSortedList m) = do (a, incidence) <- M.toAscList m replicate incidence a -insert :: Ord a => a -> SortedList a -> SortedList a +insert :: (Ord a) => a -> SortedList a -> SortedList a insert a (MkSortedList m) = MkSortedList $ M.insertWith (+) a 1 m -filter :: (a -> Bool) -> SortedList a -> SortedList a +filter :: (a -> Bool) -> SortedList a -> SortedList a filter p (MkSortedList m) = MkSortedList $ M.filterWithKey (\k _ -> p k) m -splitR :: Ord a => Int -> SortedList a -> (SortedList a, SortedList a) +splitR :: (Ord a) => Int -> SortedList a -> (SortedList a, SortedList a) splitR i sl@(MkSortedList m) | i <= 0 = (mempty, sl) | otherwise = - bimap (MkSortedList . M.fromDescList) (MkSortedList . M.fromDescList) $ - splitCount i (M.toDescList m) + bimap (MkSortedList . M.fromDescList) (MkSortedList . M.fromDescList) $ + splitCount i (M.toDescList m) -splitL :: Ord a => Int -> SortedList a -> (SortedList a, SortedList a) +splitL :: (Ord a) => Int -> SortedList a -> (SortedList a, SortedList a) splitL i sl@(MkSortedList m) | i <= 0 = (mempty, sl) | otherwise = - bimap (MkSortedList . M.fromAscList) (MkSortedList . M.fromAscList) $ - splitCount i (M.toAscList m) + bimap (MkSortedList . M.fromAscList) (MkSortedList . M.fromAscList) $ + splitCount i (M.toAscList m) -- | Split the list by using the int values associated with each value as a count. splitCount :: Int -> [(a, Int)] -> ([(a, Int)], [(a, Int)]) splitCount 0 as = ([], as) splitCount _ [] = ([], []) -splitCount i (aTup@(a, i'):as) +splitCount i (aTup@(a, i') : as) | i >= i' = first (aTup :) $ splitCount (i - i') as - | otherwise = ([(a, i)],(a,i' - i):as) + | otherwise = ([(a, i)], (a, i' - i) : as) instance Foldable SortedList where foldMap f = foldMap f . toList - sum (MkSortedList m) = M.foldlWithKey' - (\acc k incidence -> acc + k * (fromIntegral incidence)) 0 m + sum (MkSortedList m) = + M.foldlWithKey' + (\acc k incidence -> acc + k * (fromIntegral incidence)) + 0 + m toList = Tablebot.Plugins.Roll.Dice.SortedList.toList length (MkSortedList m) = sum m -instance Ord a => Semigroup (SortedList a) where +instance (Ord a) => Semigroup (SortedList a) where (MkSortedList m1) <> (MkSortedList m2) = MkSortedList (M.unionWith (+) m1 m2) -instance Ord a => Monoid (SortedList a) where +instance (Ord a) => Monoid (SortedList a) where mempty = MkSortedList M.empty diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs index 115e73d..b05cde7 100644 --- a/test/Dice/RoundtripSpec.hs +++ b/test/Dice/RoundtripSpec.hs @@ -1,128 +1,137 @@ -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DataKinds #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module Dice.RoundtripSpec where -import Test.Hspec +import qualified Data.Text as T +import Data.Traversable import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range -import Test.Hspec.Hedgehog () -import Tablebot.Plugins.Roll.Dice.DiceParsing () +import Tablebot.Plugins.Roll.Dice.DiceData as Dice import Tablebot.Plugins.Roll.Dice.DiceFunctions -import Tablebot.Utility.SmartParser.SmartParser +import Tablebot.Plugins.Roll.Dice.DiceParsing () import Tablebot.Utility.Parser -import Text.Megaparsec (runParser, eof) -import Tablebot.Plugins.Roll.Dice.DiceData as Dice -import qualified Data.Text as T -import Data.Traversable +import Tablebot.Utility.SmartParser.SmartParser +import Test.Hspec +import Test.Hspec.Hedgehog () +import Text.Megaparsec (eof, runParser) -genExpr :: MonadGen m => m Expr +genExpr :: (MonadGen m) => m Expr genExpr = Gen.recursive Gen.choice [Expr <$> genBin genTerm] [Gen.subtermM genExpr $ \expr -> ExprMisc <$> genMisc False (pure expr)] -genMisc :: MonadGen m => Bool -> m a -> m (MiscData a) +genMisc :: (MonadGen m) => Bool -> m a -> m (MiscData a) genMisc isList genA = Gen.choice [MiscIf <$> genIf genA, MiscVar <$> genVar isList genA] -genIf :: MonadGen m => m a -> m (If a) +genIf :: (MonadGen m) => m a -> m (If a) genIf genA = If <$> genExpr <*> genA <*> genA -genVarName :: MonadGen m => m T.Text -genVarName = Gen.filterT (\var -> not $ any (`T.isPrefixOf` var) (integerFunctionsList <> listFunctionsList)) - $ Gen.text (Range.linear 3 10) (Gen.element ['a'..'z']) +genVarName :: (MonadGen m) => m T.Text +genVarName = + Gen.filterT (\var -> not $ any (`T.isPrefixOf` var) (integerFunctionsList <> listFunctionsList)) $ + Gen.text (Range.linear 3 10) (Gen.element ['a' .. 'z']) -genVar :: MonadGen m => Bool -> m a -> m (Dice.Var a) +genVar :: (MonadGen m) => Bool -> m a -> m (Dice.Var a) genVar isList genA = - Dice.Var - <$> ((if isList then ("l_" <>) else id) <$> genVarName) - <*> genA + Dice.Var + <$> ((if isList then ("l_" <>) else id) <$> genVarName) + <*> genA genBin :: (MonadGen m, Enum typ, Bounded typ) => m a -> m (BinOp a typ) genBin genA = let opCount = Range.exponential 0 4 - in BinOp - <$> genA - <*> Gen.list opCount ((,) <$> Gen.element [minBound..maxBound] <*> genA) + in BinOp + <$> genA + <*> Gen.list opCount ((,) <$> Gen.element [minBound .. maxBound] <*> genA) -genTerm :: MonadGen m => m Term +genTerm :: (MonadGen m) => m Term genTerm = Term <$> genBin genNeg -genNeg :: MonadGen m => m Negation +genNeg :: (MonadGen m) => m Negation genNeg = Gen.frequency [(2, NoNeg <$> genExpo), (1, Neg <$> genExpo)] -genExpo :: MonadGen m => m Expo -genExpo = - Gen.recursive Gen.choice [NoExpo <$> genFunc] [Gen.subtermM genExpo (\expo -> (`Expo` expo) <$> genFunc )] +genExpo :: (MonadGen m) => m Expo +genExpo = + Gen.recursive Gen.choice [NoExpo <$> genFunc] [Gen.subtermM genExpo (\expo -> (`Expo` expo) <$> genFunc)] -genFunc :: MonadGen m => m Func -genFunc = Gen.frequency - [ (5, NoFunc <$> genBase) - , (1, functionGen Func integerFunctions) - ] +genFunc :: (MonadGen m) => m Func +genFunc = + Gen.frequency + [ (5, NoFunc <$> genBase), + (1, functionGen Func integerFunctions) + ] functionGen :: (Foldable f, MonadGen m) => (FuncInfoBase j -> [ArgValue] -> r) -> f (FuncInfoBase j) -> m r -functionGen cons functions = Gen.element functions >>= \func@(FuncInfo {..}) -> - cons func <$> for funcInfoParameters (\case - ATInteger -> AVExpr <$> genExpr - ATIntegerList -> AVListValues <$> genListValues - ) - -genArg :: MonadGen m => m ArgValue +functionGen cons functions = + Gen.element functions >>= \func@(FuncInfo {..}) -> + cons func + <$> for + funcInfoParameters + ( \case + ATInteger -> AVExpr <$> genExpr + ATIntegerList -> AVListValues <$> genListValues + ) + +genArg :: (MonadGen m) => m ArgValue genArg = Gen.choice [AVExpr <$> genExpr, AVListValues <$> genListValues] -genBase :: MonadGen m => m Base +genBase :: (MonadGen m) => m Base genBase = Gen.frequency [(2, NBase <$> genNumBase), (2, DiceBase <$> genDice), (1, NumVar <$> genVarName)] -genNumBase :: MonadGen m => m NumBase -genNumBase = +genNumBase :: (MonadGen m) => m NumBase +genNumBase = Gen.recursive Gen.choice [Value <$> Gen.integral (Range.linear 0 100)] [NBParen . Paren <$> genExpr] -genDice :: MonadGen m => m Dice +genDice :: (MonadGen m) => m Dice genDice = Dice <$> genNumBase <*> genDie <*> Gen.list (Range.exponential 0 3) genDieOpOption -genDie :: MonadGen m => m Die +genDie :: (MonadGen m) => m Die genDie = Gen.frequency (fmap (fmap (fmap MkDie)) strictDie <> [(1, MkDie . LazyDie <$> Gen.frequency strictDie)]) where - strictDie :: MonadGen m => [(Int, m (DieOf 'Strict))] - strictDie = [(3, Die <$> genNumBase), (1, CustomDie <$> genListValuesBase)] + strictDie :: (MonadGen m) => [(Int, m (DieOf 'Strict))] + strictDie = [(3, Die <$> genNumBase), (1, CustomDie <$> genListValuesBase)] -genDieOpOption :: MonadGen m => m DieOpOption +genDieOpOption :: (MonadGen m) => m DieOpOption genDieOpOption = Gen.choice (fmap (fmap MkDieOpOption) strictDieOp <> [MkDieOpOption . DieOpOptionLazy <$> Gen.choice strictDieOp]) where - strictDieOp :: MonadGen m => [m (DieOpOptionOf 'Strict)] - strictDieOp = - [ DieOpOptionKD <$> Gen.element [Keep, Drop] <*> Gen.frequency - [ (2, LH <$> Gen.element [Low, High] <*> genNumBase) - , (1, Where <$> genAdvancedOrdering <*> genNumBase) + strictDieOp :: (MonadGen m) => [m (DieOpOptionOf 'Strict)] + strictDieOp = + [ DieOpOptionKD + <$> Gen.element [Keep, Drop] + <*> Gen.frequency + [ (2, LH <$> Gen.element [Low, High] <*> genNumBase), + (1, Where <$> genAdvancedOrdering <*> genNumBase) + ], + Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase ] - , Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase - ] -genAdvancedOrdering :: MonadGen m => m AdvancedOrdering +genAdvancedOrdering :: (MonadGen m) => m AdvancedOrdering genAdvancedOrdering = Gen.element $ fst advancedOrderingMapping -genListValuesBase :: MonadGen m => m ListValuesBase -genListValuesBase = Gen.choice - [ LVBList <$> Gen.list (Range.exponential 1 10) genExpr - , LVBParen . Paren <$> genListValues - ] +genListValuesBase :: (MonadGen m) => m ListValuesBase +genListValuesBase = + Gen.choice + [ LVBList <$> Gen.list (Range.exponential 1 10) genExpr, + LVBParen . Paren <$> genListValues + ] -genListValues :: MonadGen m => m ListValues +genListValues :: (MonadGen m) => m ListValues genListValues = - Gen.recursive Gen.choice + Gen.recursive + Gen.choice [ LVVar . ("l_" <>) <$> genVarName - ] - [ MultipleValues <$> genNumBase <*> genBase - , LVBase <$> genListValuesBase - , ListValuesMisc <$> genMisc True genListValues - , functionGen LVFunc listFunctions - ] + ] + [ MultipleValues <$> genNumBase <*> genBase, + LVBase <$> genListValuesBase, + ListValuesMisc <$> genMisc True genListValues, + functionGen LVFunc listFunctions + ] spec_roundtrip_dice :: Spec spec_roundtrip_dice = do it "roundtrip dice" $ do dice <- forAll genExpr :: PropertyT IO Expr Right dice === runParser (pars <* eof) "" (parseShow dice) - From 843110c079358737060870a9479748c5f64f543c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 22:24:47 +0100 Subject: [PATCH 24/31] defer calculation until later, and use Endo to collect these modifications --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index c162565..8957407 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -15,8 +15,10 @@ module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStat import Control.Monad.Exception import Data.Bifunctor import qualified Data.Distribution as D +import Data.Foldable import Data.List import qualified Data.Map as M +import Data.Monoid import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval import Tablebot.Plugins.Roll.Dice.DiceFunctions @@ -179,8 +181,8 @@ instance Range Dice where let e = DM.do diecount <- b' getDiceExperiment diecount d' - res <- rangeDiceExperiment d' mdor e - return $! sum DM.<$> res + adjustDice <- rangeDiceExperiment d' mdor + return $! sum DM.<$> adjustDice e -- | Get the distribution of values from a given number of (identically -- distributed) values and the distribution of that value. @@ -191,19 +193,19 @@ getDiceExperiment i d = -- | 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 -> [DieOpOption] -> DistributionSortedList -> m DistributionSortedList -rangeDiceExperiment _ [] is = return is -rangeDiceExperiment die (doo : doos) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die doos +rangeDiceExperiment :: (MonadException m) => Distribution -> [DieOpOption] -> m (DistributionSortedList -> DistributionSortedList) +rangeDiceExperiment die = + fmap (appEndo . fold) . traverse (rangeDieOpExperiment die) -- | Perform one dice operation on the given `Experiment`, possibly returning -- a modified experiment representing the distribution of dice rolls. -rangeDieOpExperiment :: (MonadException m) => Distribution -> DieOpOption -> DistributionSortedList -> m DistributionSortedList -rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of - (DieOpOptionLazy o) -> rangeDieOpExperiment die (MkDieOpOption o) is - (DieOpOptionKD kd lhw) -> rangeDieOpExperimentKD kd lhw is +rangeDieOpExperiment :: (MonadException m) => Distribution -> DieOpOption -> m (Endo DistributionSortedList) +rangeDieOpExperiment die (MkDieOpOption doo) = case doo of + (DieOpOptionLazy o) -> rangeDieOpExperiment die (MkDieOpOption o) + (DieOpOptionKD kd lhw) -> rangeDieOpExperimentKD kd lhw (Reroll rro cond lim) -> do limd <- range lim - return $ DM.do + return $ Endo $ \is -> DM.do limit <- limd let newDie = mkNewDie limit rolls <- is @@ -218,19 +220,19 @@ rangeDieOpExperiment die (MkDieOpOption doo) is = case doo of countTriggers limitValue = foldr (\i ~(c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i `SL.insert` xs')) (0, mempty) -- | Perform a keep/drop operation on the `Experiment` of dice rolls. -rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> DistributionSortedList -> m DistributionSortedList -rangeDieOpExperimentKD kd (Where cond nb) is = do +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> m (Endo DistributionSortedList) +rangeDieOpExperimentKD kd (Where cond nb) = do nbDis <- range nb - return $ DM.do + return $ Endo $ \is -> DM.do wherelimit <- nbDis SL.filter (\i -> keepDrop $ applyCompare cond i wherelimit) DM.<$> is where keepDrop | kd == Keep = id | otherwise = not -rangeDieOpExperimentKD kd (LH lw nb) is = do +rangeDieOpExperimentKD kd (LH lw nb) = do nbd <- range nb - return $ DM.do + return $ Endo $ \is -> DM.do kdlh <- nbd (keepF . lowHighF (fromInteger kdlh)) DM.<$> is where From a0cf4628fec015e9398ac6c2eebcbc9722a002fe Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 25 Sep 2025 23:19:36 +0100 Subject: [PATCH 25/31] clean up comments and some functions --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 8957407..097ea7e 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -10,7 +10,7 @@ -- -- This plugin generates statistics based on the values of dice in given -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStats) where +module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, getStats) where import Control.Monad.Exception import Data.Bifunctor @@ -40,22 +40,7 @@ getStats d = (modalOrder, D.expectation d, D.standardDeviation d) modalOrder = fst <$> sortBy (\(_, r) (_, r') -> compare r' r) vals rangeExpr :: (MonadException m) => Expr -> m Distribution -rangeExpr e = do - ex <- range e - return $ ex - -rangeListValues :: (MonadException m) => ListValues -> m [Distribution] -rangeListValues lv = do - lve <- rangeList lv - let lvd = lve - lvd' = D.toList lvd - return $ D.fromList <$> zip' lvd' - where - head' [] = [] - head' (x : _) = [x] - getHeads xs = (\(xs', p) -> (,p) <$> head' xs') =<< xs - getTails xs = first (drop 1) <$> xs - zip' xs = getHeads xs : zip' (getTails xs) +rangeExpr = range -- | Try and get the `Distribution` of the given value, throwing a -- `MonadException` on failure. @@ -190,15 +175,14 @@ getDiceExperiment :: Integer -> Distribution -> DistributionSortedList getDiceExperiment i d = DM.sequenceSL (replicate (fromInteger i) d) --- | Go through each operator on dice and modify the `Experiment` representing --- all possible collections of rolls, returning the `Experiment` produced on --- finding `Nothing`. +-- | Go through each operator on dice and modify the distribution of values +-- based on those operations. rangeDiceExperiment :: (MonadException m) => Distribution -> [DieOpOption] -> m (DistributionSortedList -> DistributionSortedList) rangeDiceExperiment die = fmap (appEndo . fold) . traverse (rangeDieOpExperiment die) --- | Perform one dice operation on the given `Experiment`, possibly returning --- a modified experiment representing the distribution of dice rolls. +-- | Perform one dice operation on a set of values, returning +-- a modified distribution of dice rolls. rangeDieOpExperiment :: (MonadException m) => Distribution -> DieOpOption -> m (Endo DistributionSortedList) rangeDieOpExperiment die (MkDieOpOption doo) = case doo of (DieOpOptionLazy o) -> rangeDieOpExperiment die (MkDieOpOption o) @@ -219,7 +203,7 @@ rangeDieOpExperiment die (MkDieOpOption doo) = case doo of | otherwise = D.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 `SL.insert` xs')) (0, mempty) --- | Perform a keep/drop operation on the `Experiment` of dice rolls. +-- | Perform a keep/drop operation on the dice rolls. rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> m (Endo DistributionSortedList) rangeDieOpExperimentKD kd (Where cond nb) = do nbDis <- range nb @@ -284,4 +268,10 @@ rangeFunction fi exprs = do let params = first (funcInfoFunc fi) <$> D.toList (DM.sequence exprs') D.fromList <$> foldAndIgnoreErrors params where - foldAndIgnoreErrors = foldr (\(mv, p) mb -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> mb >>= \b -> return (v ++ b)) (return []) + foldAndIgnoreErrors = foldr foldrFunc (return []) + foldrFunc :: (MonadException m) => (m a, t) -> m [(a, t)] -> m [(a, t)] + foldrFunc (mv, p) mb = do + -- try to execute the result of each function, and if it throws an + -- exception we cancel out the exception and say that it never happened + v <- catchBot (pure . (,p) <$> mv) (\_ -> pure []) + (v <>) <$> mb From b0c221b3dcfb7b3c704170798c50eff99af70430 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 26 Sep 2025 10:18:16 +0100 Subject: [PATCH 26/31] share multi dice distributions --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 34 ++++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 097ea7e..49a91a0 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -163,17 +163,24 @@ instance Range Dice where range' (Dice b d mdor) = do b' <- range b d' <- range d - let e = DM.do - diecount <- b' - getDiceExperiment diecount d' adjustDice <- rangeDiceExperiment d' mdor - return $! sum DM.<$> adjustDice e - --- | Get the distribution of values from a given number of (identically --- distributed) values and the distribution of that value. -getDiceExperiment :: Integer -> Distribution -> DistributionSortedList -getDiceExperiment i d = - DM.sequenceSL (replicate (fromInteger i) d) + return $! sum DM.<$> adjustDice (getDiceDistrbutionFrom b' d') + +-- | Share previous distributions, since often when we're calculating sets of +-- dice it's not going to be each die has unrelated values; if we calculate the +-- lower values then let's calculate the higher values with those lower values. +getDiceDistrbutionFrom :: + Distribution -> -- distribution of number of dice + Distribution -> -- distribution of a die + DistributionSortedList +getDiceDistrbutionFrom dieNumber die = + dieNumber DM.>>= \dieCount -> + M.findWithDefault (DM.sequenceSL (genericReplicate dieCount die)) dieCount allDistributions + where + maximumRoll = maybe 0 fst $ M.lookupMax $ D.toMap dieNumber + allDistributions = M.fromList $ takeWhile ((<= maximumRoll) . fst) (zip [0 ..] $ allDistributions' (DM.return mempty)) + where + allDistributions' prev = prev : allDistributions' (die DM.>>= \a -> SL.insert a DM.<$> prev) -- | Go through each operator on dice and modify the distribution of values -- based on those operations. @@ -196,7 +203,7 @@ rangeDieOpExperiment die (MkDieOpOption doo) = case doo of let (count, cutdownRolls) = countTriggers limit rolls if count == 0 then DM.return rolls - else (cutdownRolls <>) DM.<$> getDiceExperiment count newDie + else (cutdownRolls <>) DM.<$> getDiceDistrbutionFrom (DM.return count) newDie where mkNewDie limitValue | rro = die @@ -250,10 +257,7 @@ instance RangeList ListValues where rangeList' (MultipleValues nb b) = do nbd <- range nb bd <- range b - return $ - DM.do - valNum <- nbd - SL.toList DM.<$> getDiceExperiment valNum bd + return $ SL.toList DM.<$> getDiceDistrbutionFrom nbd bd rangeList' (LVFunc fi avs) = rangeFunction fi avs rangeList' (ListValuesMisc m) = rangeList m rangeList' b@(LVVar _) = evaluationException "cannot find range of variable" [parseShow b] From 655310fd16a545c7e62c3d67c5c1397cb64d4d04 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 3 Jan 2026 21:30:18 +0000 Subject: [PATCH 27/31] add -Wall --- package.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.yaml b/package.yaml index 1362f74..96641a5 100644 --- a/package.yaml +++ b/package.yaml @@ -112,6 +112,7 @@ executables: ghc-options: - -threaded - -rtsopts + - -Wall - "\"-with-rtsopts=-Iw10 -N\"" dependencies: - tablebot @@ -124,6 +125,7 @@ tests: - -threaded - -rtsopts - -with-rtsopts=-N + - -Wall dependencies: - tablebot - tasty From 990ee28b65382c19970d7601bffa4220fb58e995 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 3 Jan 2026 21:30:34 +0000 Subject: [PATCH 28/31] reformat dice stats drawing --- src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 09c5f52..071c870 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -67,7 +67,13 @@ distributionRenderable d = toRenderable $ do 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_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 @Integer @Double) (barNames d) pts let pb' = set plot_bars_spacing (BarsFixGap 10 5) pb @@ -79,7 +85,7 @@ distributionRenderable d = toRenderable $ do 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) + ds' = M.unionsWith (++) $ M.map (: []) . applyAll (insertEmpty <$> allIntegers) <$> ds pts = second (fromRational . (* 100) <$>) <$> M.toList ds' r = (fst $ M.findMin ds', fst $ M.findMax ds') applyAll [] = id From 6727e783b033ae140c85e6ac657eb7663beb871d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 3 Jan 2026 21:30:48 +0000 Subject: [PATCH 29/31] reverse order of operations for dice experiment --- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 49a91a0..bc7c230 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -186,7 +186,7 @@ getDiceDistrbutionFrom dieNumber die = -- based on those operations. rangeDiceExperiment :: (MonadException m) => Distribution -> [DieOpOption] -> m (DistributionSortedList -> DistributionSortedList) rangeDiceExperiment die = - fmap (appEndo . fold) . traverse (rangeDieOpExperiment die) + fmap (appEndo . fold . reverse) . traverse (rangeDieOpExperiment die) -- | Perform one dice operation on a set of values, returning -- a modified distribution of dice rolls. From f02ba40796a4d0199b8f4d4ca33acb8d7bbe503e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 3 Jan 2026 21:30:59 +0000 Subject: [PATCH 30/31] extract out dice generators --- test/Dice/Gen.hs | 132 +++++++++++++++++++++++++++++++++++++ test/Dice/RoundtripSpec.hs | 122 +--------------------------------- 2 files changed, 133 insertions(+), 121 deletions(-) create mode 100644 test/Dice/Gen.hs diff --git a/test/Dice/Gen.hs b/test/Dice/Gen.hs new file mode 100644 index 0000000..500e1fa --- /dev/null +++ b/test/Dice/Gen.hs @@ -0,0 +1,132 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Dice.Gen where + +import qualified Data.Text as T +import Data.Traversable (for) +import Hedgehog (MonadGen) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range +import Tablebot.Plugins.Roll.Dice.DiceData as Dice +import Tablebot.Plugins.Roll.Dice.DiceFunctions + ( ArgType (ATInteger, ATIntegerList), + FuncInfoBase (..), + integerFunctions, + integerFunctionsList, + listFunctions, + listFunctionsList, + ) + +genExpr :: (MonadGen m) => m Expr +genExpr = + Gen.recursive Gen.choice [Expr <$> genBin genTerm] [Gen.subtermM genExpr $ \expr -> ExprMisc <$> genMisc False (pure expr)] + +genMisc :: (MonadGen m) => Bool -> m a -> m (MiscData a) +genMisc isList genA = Gen.choice [MiscIf <$> genIf genA, MiscVar <$> genVar isList genA] + +genIf :: (MonadGen m) => m a -> m (If a) +genIf genA = If <$> genExpr <*> genA <*> genA + +genVarName :: (MonadGen m) => m T.Text +genVarName = + Gen.filterT (\var -> not $ any (`T.isPrefixOf` var) (integerFunctionsList <> listFunctionsList)) $ + Gen.text (Range.linear 3 10) (Gen.element ['a' .. 'z']) + +genVar :: (MonadGen m) => Bool -> m a -> m (Dice.Var a) +genVar isList genA = + Dice.Var + <$> ((if isList then ("l_" <>) else id) <$> genVarName) + <*> genA + +genBin :: (MonadGen m, Enum typ, Bounded typ) => m a -> m (BinOp a typ) +genBin genA = + let opCount = Range.exponential 0 4 + in BinOp + <$> genA + <*> Gen.list opCount ((,) <$> Gen.element [minBound .. maxBound] <*> genA) + +genTerm :: (MonadGen m) => m Term +genTerm = Term <$> genBin genNeg + +genNeg :: (MonadGen m) => m Negation +genNeg = Gen.frequency [(2, NoNeg <$> genExpo), (1, Neg <$> genExpo)] + +genExpo :: (MonadGen m) => m Expo +genExpo = + Gen.recursive Gen.choice [NoExpo <$> genFunc] [Gen.subtermM genExpo (\expo -> (`Expo` expo) <$> genFunc)] + +genFunc :: (MonadGen m) => m Func +genFunc = + Gen.frequency + [ (5, NoFunc <$> genBase), + (1, functionGen Func integerFunctions) + ] + +functionGen :: (Foldable f, MonadGen m) => (FuncInfoBase j -> [ArgValue] -> r) -> f (FuncInfoBase j) -> m r +functionGen cons functions = + Gen.element functions >>= \func@(FuncInfo {..}) -> + cons func + <$> for + funcInfoParameters + ( \case + ATInteger -> AVExpr <$> genExpr + ATIntegerList -> AVListValues <$> genListValues + ) + +genArg :: (MonadGen m) => m ArgValue +genArg = Gen.choice [AVExpr <$> genExpr, AVListValues <$> genListValues] + +genBase :: (MonadGen m) => m Base +genBase = Gen.frequency [(2, NBase <$> genNumBase), (2, DiceBase <$> genDice), (1, NumVar <$> genVarName)] + +genNumBase :: (MonadGen m) => m NumBase +genNumBase = + Gen.recursive Gen.choice [Value <$> Gen.integral (Range.linear 0 100)] [NBParen . Paren <$> genExpr] + +genDice :: (MonadGen m) => m Dice +genDice = Dice <$> genNumBase <*> genDie <*> Gen.list (Range.exponential 0 3) genDieOpOption + +genDie :: (MonadGen m) => m Die +genDie = Gen.frequency (fmap (fmap (fmap MkDie)) strictDie <> [(1, MkDie . LazyDie <$> Gen.frequency strictDie)]) + where + strictDie :: (MonadGen m) => [(Int, m (DieOf 'Strict))] + strictDie = [(3, Die <$> genNumBase), (1, CustomDie <$> genListValuesBase)] + +genDieOpOption :: (MonadGen m) => m DieOpOption +genDieOpOption = Gen.choice (fmap (fmap MkDieOpOption) strictDieOp <> [MkDieOpOption . DieOpOptionLazy <$> Gen.choice strictDieOp]) + where + strictDieOp :: (MonadGen m) => [m (DieOpOptionOf 'Strict)] + strictDieOp = + [ DieOpOptionKD + <$> Gen.element [Keep, Drop] + <*> Gen.frequency + [ (2, LH <$> Gen.element [Low, High] <*> genNumBase), + (1, Where <$> genAdvancedOrdering <*> genNumBase) + ], + Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase + ] + +genAdvancedOrdering :: (MonadGen m) => m AdvancedOrdering +genAdvancedOrdering = Gen.element $ fst advancedOrderingMapping + +genListValuesBase :: (MonadGen m) => m ListValuesBase +genListValuesBase = + Gen.choice + [ LVBList <$> Gen.list (Range.exponential 1 10) genExpr, + LVBParen . Paren <$> genListValues + ] + +genListValues :: (MonadGen m) => m ListValues +genListValues = + Gen.recursive + Gen.choice + [ LVVar . ("l_" <>) <$> genVarName + ] + [ MultipleValues <$> genNumBase <*> genBase, + LVBase <$> genListValuesBase, + ListValuesMisc <$> genMisc True genListValues, + functionGen LVFunc listFunctions + ] diff --git a/test/Dice/RoundtripSpec.hs b/test/Dice/RoundtripSpec.hs index b05cde7..1aa5f7e 100644 --- a/test/Dice/RoundtripSpec.hs +++ b/test/Dice/RoundtripSpec.hs @@ -1,17 +1,8 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} - module Dice.RoundtripSpec where -import qualified Data.Text as T -import Data.Traversable +import Dice.Gen import Hedgehog -import qualified Hedgehog.Gen as Gen -import qualified Hedgehog.Range as Range import Tablebot.Plugins.Roll.Dice.DiceData as Dice -import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Plugins.Roll.Dice.DiceParsing () import Tablebot.Utility.Parser import Tablebot.Utility.SmartParser.SmartParser @@ -19,117 +10,6 @@ import Test.Hspec import Test.Hspec.Hedgehog () import Text.Megaparsec (eof, runParser) -genExpr :: (MonadGen m) => m Expr -genExpr = - Gen.recursive Gen.choice [Expr <$> genBin genTerm] [Gen.subtermM genExpr $ \expr -> ExprMisc <$> genMisc False (pure expr)] - -genMisc :: (MonadGen m) => Bool -> m a -> m (MiscData a) -genMisc isList genA = Gen.choice [MiscIf <$> genIf genA, MiscVar <$> genVar isList genA] - -genIf :: (MonadGen m) => m a -> m (If a) -genIf genA = If <$> genExpr <*> genA <*> genA - -genVarName :: (MonadGen m) => m T.Text -genVarName = - Gen.filterT (\var -> not $ any (`T.isPrefixOf` var) (integerFunctionsList <> listFunctionsList)) $ - Gen.text (Range.linear 3 10) (Gen.element ['a' .. 'z']) - -genVar :: (MonadGen m) => Bool -> m a -> m (Dice.Var a) -genVar isList genA = - Dice.Var - <$> ((if isList then ("l_" <>) else id) <$> genVarName) - <*> genA - -genBin :: (MonadGen m, Enum typ, Bounded typ) => m a -> m (BinOp a typ) -genBin genA = - let opCount = Range.exponential 0 4 - in BinOp - <$> genA - <*> Gen.list opCount ((,) <$> Gen.element [minBound .. maxBound] <*> genA) - -genTerm :: (MonadGen m) => m Term -genTerm = Term <$> genBin genNeg - -genNeg :: (MonadGen m) => m Negation -genNeg = Gen.frequency [(2, NoNeg <$> genExpo), (1, Neg <$> genExpo)] - -genExpo :: (MonadGen m) => m Expo -genExpo = - Gen.recursive Gen.choice [NoExpo <$> genFunc] [Gen.subtermM genExpo (\expo -> (`Expo` expo) <$> genFunc)] - -genFunc :: (MonadGen m) => m Func -genFunc = - Gen.frequency - [ (5, NoFunc <$> genBase), - (1, functionGen Func integerFunctions) - ] - -functionGen :: (Foldable f, MonadGen m) => (FuncInfoBase j -> [ArgValue] -> r) -> f (FuncInfoBase j) -> m r -functionGen cons functions = - Gen.element functions >>= \func@(FuncInfo {..}) -> - cons func - <$> for - funcInfoParameters - ( \case - ATInteger -> AVExpr <$> genExpr - ATIntegerList -> AVListValues <$> genListValues - ) - -genArg :: (MonadGen m) => m ArgValue -genArg = Gen.choice [AVExpr <$> genExpr, AVListValues <$> genListValues] - -genBase :: (MonadGen m) => m Base -genBase = Gen.frequency [(2, NBase <$> genNumBase), (2, DiceBase <$> genDice), (1, NumVar <$> genVarName)] - -genNumBase :: (MonadGen m) => m NumBase -genNumBase = - Gen.recursive Gen.choice [Value <$> Gen.integral (Range.linear 0 100)] [NBParen . Paren <$> genExpr] - -genDice :: (MonadGen m) => m Dice -genDice = Dice <$> genNumBase <*> genDie <*> Gen.list (Range.exponential 0 3) genDieOpOption - -genDie :: (MonadGen m) => m Die -genDie = Gen.frequency (fmap (fmap (fmap MkDie)) strictDie <> [(1, MkDie . LazyDie <$> Gen.frequency strictDie)]) - where - strictDie :: (MonadGen m) => [(Int, m (DieOf 'Strict))] - strictDie = [(3, Die <$> genNumBase), (1, CustomDie <$> genListValuesBase)] - -genDieOpOption :: (MonadGen m) => m DieOpOption -genDieOpOption = Gen.choice (fmap (fmap MkDieOpOption) strictDieOp <> [MkDieOpOption . DieOpOptionLazy <$> Gen.choice strictDieOp]) - where - strictDieOp :: (MonadGen m) => [m (DieOpOptionOf 'Strict)] - strictDieOp = - [ DieOpOptionKD - <$> Gen.element [Keep, Drop] - <*> Gen.frequency - [ (2, LH <$> Gen.element [Low, High] <*> genNumBase), - (1, Where <$> genAdvancedOrdering <*> genNumBase) - ], - Reroll <$> Gen.element [True, False] <*> genAdvancedOrdering <*> genNumBase - ] - -genAdvancedOrdering :: (MonadGen m) => m AdvancedOrdering -genAdvancedOrdering = Gen.element $ fst advancedOrderingMapping - -genListValuesBase :: (MonadGen m) => m ListValuesBase -genListValuesBase = - Gen.choice - [ LVBList <$> Gen.list (Range.exponential 1 10) genExpr, - LVBParen . Paren <$> genListValues - ] - -genListValues :: (MonadGen m) => m ListValues -genListValues = - Gen.recursive - Gen.choice - [ LVVar . ("l_" <>) <$> genVarName - ] - [ MultipleValues <$> genNumBase <*> genBase, - LVBase <$> genListValuesBase, - ListValuesMisc <$> genMisc True genListValues, - functionGen LVFunc listFunctions - ] - spec_roundtrip_dice :: Spec spec_roundtrip_dice = do it "roundtrip dice" $ do From 92f97b7103fe5c15076f20ee8e6625b3250fb5aa Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 3 Jan 2026 21:31:10 +0000 Subject: [PATCH 31/31] .cabal changes --- tablebot.cabal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tablebot.cabal b/tablebot.cabal index 3780c7d..d1459e3 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -192,7 +192,7 @@ executable tablebot-exe Paths_tablebot hs-source-dirs: app - ghc-options: -threaded -rtsopts "-with-rtsopts=-Iw10 -N" + ghc-options: -threaded -rtsopts -Wall "-with-rtsopts=-Iw10 -N" build-depends: Chart , Chart-diagrams @@ -251,11 +251,12 @@ test-suite tablebot-test type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: + Dice.Gen Dice.RoundtripSpec Paths_tablebot hs-source-dirs: test - ghc-options: -threaded -rtsopts -with-rtsopts=-N + ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall build-tool-depends: tasty-discover:tasty-discover build-depends: