diff --git a/cabal.project b/cabal.project index 82211722..5984daee 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/package.yaml b/package.yaml index 2993826d..96641a50 100644 --- a/package.yaml +++ b/package.yaml @@ -112,6 +112,7 @@ executables: ghc-options: - -threaded - -rtsopts + - -Wall - "\"-with-rtsopts=-Iw10 -N\"" dependencies: - tablebot @@ -124,5 +125,15 @@ tests: - -threaded - -rtsopts - -with-rtsopts=-N + - -Wall dependencies: - tablebot + - tasty + - tasty-discover + - tasty-hspec + - tasty-hedgehog + - hspec + - hedgehog + - hspec-hedgehog + build-tools: + - tasty-discover:tasty-discover diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index c70d1e7c..fd8d50be 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/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index c232f28f..d3b3512e 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 :: (Operation typ) => 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,42 +113,59 @@ 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 +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] @@ -178,30 +193,50 @@ 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 + +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) +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. -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 @@ -242,7 +277,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 <$>) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index ab98561e..3b9186a0 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,61 +297,64 @@ 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 + 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')) - 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/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 41be9b8e..73792ea5 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. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index e4290212..37065fc1 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 @@ -119,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 @@ -163,7 +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 *> 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 @@ -183,12 +183,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 ')' @@ -200,39 +197,38 @@ 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 _ <- 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 @@ -246,32 +242,29 @@ 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"]) --- | 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) - <|> (try (string "rr") *> parseAdvancedOrdering >>= \o -> Reroll False o <$> pars) - <|> ( ( ((try (char 'k') *> parseLowHigh) <&> DieOpOptionKD Keep) - <|> ((try (char 'd') *> parseLowHigh) <&> DieOpOptionKD Drop) + 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" ) - "could not parse keep/drop" - ) - ) - <&> lazyFunc - ) - "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 @@ -348,25 +341,26 @@ 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 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 - 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 + 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 + 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 diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 9e3bb206..bc7c2309 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. @@ -8,73 +10,49 @@ -- -- 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 import Control.Monad.Exception -import Data.Bifunctor (Bifunctor (first)) -import Data.Distribution hiding (Distribution, Experiment, fromList) +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 import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) +import qualified Tablebot.Plugins.Roll.Dice.DistributionMonad as DM +import Tablebot.Plugins.Roll.Dice.SortedList as SL 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 +type DistributionSortedList = D.Distribution (SortedList Integer) --- | Convenient alias for a experiments of lists of integers. -type ExperimentList = D.Experiment [Integer] +type DistributionList = D.Distribution [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 - -rangeListValues :: (MonadException m) => ListValues -> m [Distribution] -rangeListValues lv = do - lve <- rangeList lv - let lvd = run lve - lvd' = toList lvd - return $ D.fromList <$> zip' lvd' - where - head' [] = [] - head' (x : _) = [x] - getHeads xs = (\(xs', p) -> (,p) <$> head' xs') =<< xs - getTails xs = first (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. +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. --- --- 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 - -- `MonadException` on failure. - range :: (MonadException m, ParseShow a) => a -> m Experiment - 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 @@ -84,17 +62,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 = toMap $ run b' + let mp = D.toMap b' canBeFalse = M.member 0 mp canBeTrue = not $ M.null $ M.filterWithKey (\k _ -> k /= 0) mp - emptyExp = 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' @@ -106,13 +84,16 @@ 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 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 @@ -125,14 +106,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' <*> from (assuming (/= 0) (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 @@ -141,14 +128,17 @@ 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 $ 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 @@ -157,104 +147,109 @@ 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 $ - do - nbV <- nbr - from $ uniform [1 .. nbV] - range' (CustomDie lv) = do - dievs <- rangeList lv - return $ dievs >>= from . 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 b' <- range b d' <- range d - let e = do - diecount <- b' - getDiceExperiment diecount (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 - --- | 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 - --- | 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 $ 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 (run newDie) + adjustDice <- rangeDiceExperiment d' mdor + 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 - 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, []) - --- | 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 + 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. +rangeDiceExperiment :: (MonadException m) => Distribution -> [DieOpOption] -> m (DistributionSortedList -> DistributionSortedList) +rangeDiceExperiment die = + fmap (appEndo . fold . reverse) . traverse (rangeDieOpExperiment die) + +-- | 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) + (DieOpOptionKD kd lhw) -> rangeDieOpExperimentKD kd lhw + (Reroll rro cond lim) -> do + limd <- range lim + return $ Endo $ \is -> DM.do + limit <- limd + let newDie = mkNewDie limit + rolls <- is + let (count, cutdownRolls) = countTriggers limit rolls + if count == 0 + then DM.return rolls + else (cutdownRolls <>) DM.<$> getDiceDistrbutionFrom (DM.return 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 `SL.insert` xs')) (0, mempty) + +-- | 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 - return $ do + return $ Endo $ \is -> DM.do wherelimit <- nbDis - filter (\i -> keepDrop $ applyCompare cond i wherelimit) <$> is + SL.filter (\i -> keepDrop $ applyCompare cond i wherelimit) DM.<$> is where 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 $ do - kdlh <- nbd - getKeep kdlh . sortBy' <$> is +rangeDieOpExperimentKD kd (LH lw nb) = do + nbd <- range nb + return $ Endo $ \is -> DM.do + kdlh <- nbd + (keepF . lowHighF (fromInteger kdlh)) 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 - getKeep = if kd == Keep then genericTake else genericDrop + keepF = case kd of + Keep -> fst + Drop -> snd + lowHighF = case lw of + 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 ExperimentList - 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 exprs <- mapM range es - return $ sequence exprs + return $ DM.sequence exprs rangeList' (LVBParen (Paren lv)) = rangeList lv instance RangeList ListValues where @@ -262,22 +257,25 @@ instance RangeList ListValues where rangeList' (MultipleValues nb b) = do nbd <- range nb bd <- range b - return $ - do - valNum <- nbd - getDiceExperiment valNum (run 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] -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) <$> toList (run $ sequence exprs') - 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 -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> 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 diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs index 09c5f529..071c870a 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 diff --git a/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs new file mode 100644 index 00000000..d045bfe7 --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/DistributionMonad.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE QualifiedDo #-} +{-# LANGUAGE NoImplicitPrelude #-} + +module Tablebot.Plugins.Roll.Dice.DistributionMonad where + +import Data.Distribution.Core +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 +(>>=) = 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 + +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 00000000..c2f690f5 --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/SortedList.hs @@ -0,0 +1,69 @@ +module Tablebot.Plugins.Roll.Dice.SortedList + ( SortedList, + toList, + fromList, + insert, + Tablebot.Plugins.Roll.Dice.SortedList.filter, + splitL, + splitR, + ) +where + +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) + +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/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 348e0e8e..4abcb2b3 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 (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. gencharHelp :: HelpPage diff --git a/stack.yaml b/stack.yaml index 398de291..3066bfbe 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 a53e483f..bfea7740 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 15c12c95..d1459e36 100644 --- a/tablebot.cabal +++ b/tablebot.cabal @@ -78,6 +78,8 @@ library Tablebot.Plugins.Roll.Dice.DiceParsing 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 @@ -190,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 @@ -249,10 +251,14 @@ 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: Chart , Chart-diagrams @@ -275,6 +281,9 @@ test-suite tablebot-test , exception-transformers , extra , filepath + , hedgehog + , hspec + , hspec-hedgehog , http-client , http-conduit , load-env @@ -295,6 +304,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/Gen.hs b/test/Dice/Gen.hs new file mode 100644 index 00000000..500e1fad --- /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 new file mode 100644 index 00000000..1aa5f7e1 --- /dev/null +++ b/test/Dice/RoundtripSpec.hs @@ -0,0 +1,17 @@ +module Dice.RoundtripSpec where + +import Dice.Gen +import Hedgehog +import Tablebot.Plugins.Roll.Dice.DiceData as Dice +import Tablebot.Plugins.Roll.Dice.DiceParsing () +import Tablebot.Utility.Parser +import Tablebot.Utility.SmartParser.SmartParser +import Test.Hspec +import Test.Hspec.Hedgehog () +import Text.Megaparsec (eof, runParser) + +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) diff --git a/test/Spec.hs b/test/Spec.hs index cd4753fc..70c55f52 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 #-}