Nomyx-Language-0.2.0: src/Language/Nomyx/Rule.hs
{-# LANGUAGE DeriveDataTypeable, GADTs, ScopedTypeVariables, TupleSections, FlexibleInstances#-}
-- | Basic rules examples.
module Language.Nomyx.Rule where
import Prelude hiding (foldr)
import Language.Nomyx.Expression
import Language.Nomyx.Definition
import Data.Typeable
import Control.Monad.State hiding (forM_)
import Data.Maybe
import Data.Time hiding (getCurrentTime)
import Control.Arrow
import Control.Applicative
import Data.Lens
import Data.Foldable hiding (and, mapM_)
import Data.Boolean
-- | This rule will activate automatically any new rule.
autoActivate :: RuleFunc
autoActivate = voidRule $ onEvent_ (RuleEv Proposed) (activateRule_ . _rNumber . ruleData)
-- | This rule will forbid any new rule to delete the rule in parameter
--immutableRule :: RuleNumber -> RuleFunc
--immutableRule rn = return $ Meta f where
-- f r = do
-- protectedRule <- getRule rn
-- case protectedRule of
-- Just pr -> case _rRuleFunc r of
-- RuleRule paramRule -> paramRule pr
-- _ -> return $ BoolResp True
-- Nothing -> return $ BoolResp True
-- | A rule will be always legal
legal :: RuleFunc
legal = return $ Meta (\_ -> return $ BoolResp True)
-- | A rule will be always illegal
illegal :: RuleFunc
illegal = return $ Meta (\_ -> return $ BoolResp False)
-- | active metarules are automatically used to evaluate a given rule
checkWithMetarules :: Rule -> Nomex BoolResp
checkWithMetarules rule = do
rs <- getActiveRules
(metas :: [Rule -> Nomex BoolResp]) <- mapMaybeM maybeMetaRule rs
let (evals :: [Nomex BoolResp]) = map (\meta -> meta rule) metas
foldr (&&*) true evals
maybeMetaRule :: Rule -> Nomex (Maybe (Rule -> Nomex BoolResp))
maybeMetaRule Rule {_rRuleFunc = rule} = do
meta <- rule
case meta of
(Meta m) -> return $ Just m
_ -> return Nothing
-- | any new rule will be activate if the rule in parameter returns True
onRuleProposed :: (Rule -> Nomex BoolResp) -> RuleFunc
onRuleProposed r = voidRule $ onEvent_ (RuleEv Proposed) $ \(RuleData rule) -> do
resp <- r rule
case resp of
BoolResp b -> activateOrReject rule b
MsgResp m -> onMessageOnce m $ (activateOrReject rule) . messageData
data ForAgainst = For | Against deriving (Typeable, Enum, Show, Eq, Bounded, Read)
type Vote = (PlayerNumber, Maybe ForAgainst)
type CountVotes = [Vote] -> Maybe Bool
data VoteData = VoteData { msgEnd :: Event (Message Bool),
voteVar :: ArrayVar PlayerNumber ForAgainst,
inputNumbers :: [EventNumber],
assessFunction :: CountVotes}
type Assessor a = StateT VoteData Nomex a
-- | Performs a vote for a rule on all players. The provided function is used to count the votes.
-- the assessors allows to configure how and when the vote will be assessed. The assessors can be chained.
voteWith :: CountVotes -> Assessor () -> Rule -> Nomex BoolResp
voteWith assessFunction assessors rule = do
pns <- getAllPlayerNumbers
let rn = show $ _rNumber rule
let msgEnd = Message ("Result of votes for " ++ rn) :: Event(Message Bool)
--create an array variable to store the votes.
voteVar <- newArrayVar ("Votes for rule " ++ rn) pns
let askPlayer pn = onInputChoiceOnce ("Vote for rule " ++ rn) [For, Against] (putArrayVar voteVar pn) pn
inputs <- mapM askPlayer pns
let voteData = VoteData msgEnd voteVar inputs assessFunction
evalStateT assessors voteData
cleanVote voteData
return $ MsgResp msgEnd
-- | assess the vote on every new vote with the assess function, and as soon as the vote has an issue (positive of negative), sends a signal
assessOnEveryVotes :: Assessor ()
assessOnEveryVotes = do
(VoteData msgEnd voteVar _ assess) <- get
lift $ do
msgVotes <- getArrayVarMessage voteVar
onMessage msgVotes $ \(MessageData votes) -> maybeWhen (assess votes) $ sendMessage msgEnd
-- | assess the vote with the assess function when time is reached, and sends a signal with the issue (positive of negative)
--
assessOnTimeLimit :: UTCTime -> Assessor ()
assessOnTimeLimit time = do
(VoteData msgEnd voteVar _ assess) <- get
lift $ do
onEvent_ (Time time) $ \_ -> do
votes <- getArrayVarData voteVar
let result = assess $ getOnlyVoters $ votes
when (result == Nothing) $ outputAll "Vote: Quorum not reached, rule is rejected"
sendMessage msgEnd $ fromMaybe False $ result
-- | assess the vote with the assess function when time is elapsed, and sends a signal with the issue (positive of negative)
assessOnTimeDelay :: NominalDiffTime -> Assessor ()
assessOnTimeDelay delay = do
t <- addUTCTime delay <$> lift getCurrentTime
assessOnTimeLimit t
-- | assess the vote only when every body voted. An error is generated if the assessing function returns Nothing.
assessWhenEverybodyVoted :: Assessor ()
assessWhenEverybodyVoted = do
(VoteData msgEnd voteVar _ assess) <- get
lift $ do
msgVotes <- getArrayVarMessage voteVar
onMessage msgVotes $ \(MessageData votes) -> when (length (voters votes) == length votes) $
sendMessage msgEnd $ fromJust $ assess $ votes
-- | players that did not voted are counted as negative.
noStatusQuo :: [Vote] -> [Vote]
noStatusQuo = map noVoteCountAsAgainst where
noVoteCountAsAgainst :: Vote -> Vote
noVoteCountAsAgainst (a, Nothing) = (a, Just Against)
noVoteCountAsAgainst a = a
-- | clean events and variables necessary for the vote
cleanVote :: VoteData -> Nomex ()
cleanVote (VoteData msgEnd voteVar inputsNumber _) = onMessage msgEnd$ \_ -> do
delAllEvents msgEnd
delArrayVar voteVar
mapM_ delEvent inputsNumber
assessOnlyVoters :: CountVotes -> CountVotes
assessOnlyVoters assess vs = assess $ map (second Just) $ voters vs
-- | a quorum is the neccessary number of voters for the validity of the vote
quorum :: Int -> [Vote] -> Bool
quorum q vs = (length $ voters vs) >= q
-- | adds a quorum to an assessing function
withQuorum :: CountVotes -> Int -> CountVotes
withQuorum assess q vs = if (quorum q vs) then assess vs else Nothing
-- | assess the vote results according to a unanimity (everybody votes for)
unanimity :: [Vote] -> Maybe Bool
unanimity votes = voteQuota (length votes) votes
-- | assess the vote results according to an absolute majority (half voters plus one, no quorum is needed)
majority :: [Vote] -> Maybe Bool
majority votes = voteQuota ((length votes) `div` 2 + 1) votes
-- | assess the vote results according to a majority of x (in %)
majorityWith :: Int -> [Vote] -> Maybe Bool
majorityWith x votes = voteQuota ((length votes) * x `div` 100 + 1) votes
-- | assess the vote results according to a necessary number of positive votes
numberPositiveVotes :: Int -> [Vote] -> Maybe Bool
numberPositiveVotes = voteQuota
-- | helper function for assessement functions
voteQuota :: Int -> [Vote] -> Maybe Bool
voteQuota quotaFor votes
| nbFor votes >= quotaFor = Just True
| nbAgainst votes > (length votes) - quotaFor = Just False
| otherwise = Nothing
-- | get the number of positive votes and negative votes
nbFor, nbAgainst :: [Vote] -> Int
nbFor = length . filter ((== Just For) . snd)
nbAgainst = length . filter ((== Just Against) . snd)
-- | get only those who voted
voters :: [(PlayerNumber, Maybe ForAgainst)] -> [(PlayerNumber, ForAgainst)]
voters vs = catMaybes $ map voter vs where
voter (pn, Just fa) = Just (pn, fa)
voter (_, Nothing) = Nothing
-- | get only those who voted
getOnlyVoters :: [(PlayerNumber, Maybe ForAgainst)] -> [(PlayerNumber, Maybe ForAgainst)]
getOnlyVoters vs = map (second Just) $ voters vs
-- | activate or reject a rule
activateOrReject :: Rule -> Bool -> Nomex ()
activateOrReject r b = if b then activateRule_ (_rNumber r) else rejectRule_ (_rNumber r)
-- | perform an action for each current players, new players and leaving players
forEachPlayer :: (PlayerNumber -> Nomex ()) -> (PlayerNumber -> Nomex ()) -> (PlayerNumber -> Nomex ()) -> Nomex ()
forEachPlayer action actionWhenArrive actionWhenLeave = do
pns <- getAllPlayerNumbers
mapM_ action pns
onEvent_ (Player Arrive) $ \(PlayerData p) -> actionWhenArrive $ _playerNumber p
onEvent_ (Player Leave) $ \(PlayerData p) -> actionWhenLeave $ _playerNumber p
-- | perform the same action for each players, including new players
forEachPlayer_ :: (PlayerNumber -> Nomex ()) -> Nomex ()
forEachPlayer_ action = forEachPlayer action action (\_ -> return ())
-- | create a value initialized for each players
--manages players joining and leaving
createValueForEachPlayer :: Int -> V [(Int, Int)] -> Nomex ()
createValueForEachPlayer initialValue var = do
pns <- getAllPlayerNumbers
v <- newVar_ (varName var) $ map (,initialValue::Int) pns
forEachPlayer (\_-> return ())
(\p -> modifyVar v ((p, initialValue) : ))
(\p -> modifyVar v $ filter $ (/= p) . fst)
-- | create a value initialized for each players initialized to zero
--manages players joining and leaving
createValueForEachPlayer_ :: V [(Int, Int)] -> Nomex ()
createValueForEachPlayer_ = createValueForEachPlayer 0
modifyValueOfPlayer :: PlayerNumber -> V [(Int, Int)] -> (Int -> Int) -> Nomex ()
modifyValueOfPlayer pn var f = modifyVar var $ map $ (\(a,b) -> if a == pn then (a, f b) else (a,b))
modifyAllValues :: V [(Int, Int)] -> (Int -> Int) -> Nomex ()
modifyAllValues var f = modifyVar var $ map $ second f
-- | Player p cannot propose anymore rules
noPlayPlayer :: PlayerNumber -> RuleFunc
noPlayPlayer p = return $ Meta $ \r -> return $ BoolResp $ (_rProposedBy r) /= p
-- | a rule can autodelete itself (generaly after having performed some actions)
autoDelete :: Nomex ()
autoDelete = getSelfRuleNumber >>= suppressRule_
-- | All rules from player p are erased:
eraseAllRules :: PlayerNumber -> Nomex Bool
eraseAllRules p = do
rs <- getRules
let myrs = filter ((== p) . getL rProposedBy) rs
res <- mapM (suppressRule . _rNumber) myrs
return $ and res
oneDay :: NominalDiffTime
oneDay = 60 * 60 * 24
maybeWhen :: Maybe a -> (a -> Nomex ()) -> Nomex ()
maybeWhen = forM_