module Language.Nomyx.Vote where
import Prelude hiding (foldr)
import Language.Nomyx.Expression
import Language.Nomyx.Events
import Language.Nomyx.Variables
import Language.Nomyx.Outputs
import Language.Nomyx.Inputs
import Language.Nomyx.Players
import Language.Nomyx.Rules
import Data.Typeable
import Control.Monad.State hiding (forM_)
import Data.Maybe
import Data.Time hiding (getCurrentTime)
import Data.Traversable hiding (mapM)
import Control.Arrow
import Control.Applicative
import Data.List
import Data.Function
import qualified Data.Map as M
import Control.Monad.Error (MonadError(..))
data VoteType a = ExclusiveVote (Maybe (Alts a))
| NonExclusiveVote [Alts a]
class (Eq (Alts a), Show (Alts a), Ord (Alts a), Typeable a) => Votable a where
data Alts a
alts :: [Alts a]
quota :: Alts a -> Int -> Int -> Int
name :: a -> String
exclusiveWinner :: a -> Maybe (Alts a, Alts a)
exclusiveWinner _ = Nothing
type ForAgainst = Alts RuleInfo
instance Votable RuleInfo where
data Alts RuleInfo = For | Against deriving (Typeable, Enum, Show, Eq, Bounded, Read, Ord)
alts = [For, Against]
quota For q _ = q
quota Against q vs = vs q + 1
name r = "rule " ++ (show $ _rNumber r)
exclusiveWinner _ = Just (For, Against)
type Vote a = (PlayerNumber, Maybe (Alts a))
type VoteResult a = VoteStats a -> [Alts a]
data VoteStats a = VoteStats { voteCounts :: M.Map (Maybe (Alts a)) Int,
voteFinished :: Bool}
data VoteData a = VoteData { msgEnd :: Msg [Alts a],
voteVar :: ArrayVar PlayerNumber (Alts a),
inputNumbers :: [EventNumber],
assessFunction :: VoteResult a,
timeLimit :: Maybe UTCTime}
type Assessor a = StateT (VoteData a) Nomex ()
voteWith :: (Votable a) => VoteResult a
-> Assessor a
-> a
-> [Alts a]
-> Nomex (Msg [Alts a])
voteWith countVotes assessors toVote als = do
pns <- liftEffect getAllPlayerNumbers
let toVoteName = name toVote
let msgEnd = Msg ("Result of votes for " ++ toVoteName) :: Msg [Alts a]
(voteVar :: ArrayVar PlayerNumber (Alts a)) <- newArrayVar_ ("Votes for " ++ toVoteName) pns
let askPlayer pn = onInputRadioOnce ("Vote for " ++ toVoteName ++ ":") als (putArrayVar_ voteVar pn) pn
inputs <- mapM askPlayer pns
let voteData = VoteData msgEnd voteVar inputs countVotes Nothing
evalStateT assessors voteData
displayVoteVar Nothing ("On-going vote for " ++ toVoteName ++ ":") voteVar
displayVoteResult toVoteName voteData
cleanVote voteData
return msgEnd
voteWith_ :: (Votable a) => VoteResult a -> Assessor a -> a -> Nomex (Msg [Alts a])
voteWith_ assessFunction assessors toVote = voteWith assessFunction assessors toVote alts
assessOnEveryVote :: (Votable a) => Assessor a
assessOnEveryVote = do
(VoteData msgEnd voteVar _ assess _) <- get
lift $ void $ onMsgVarEvent voteVar $ f assess msgEnd where
f assess msgEnd (VUpdated votes) = do
let res = assess $ getVoteStats votes False
unless (null res) $ sendMessage msgEnd res
f _ _ _ = return ()
assessOnTimeLimit :: (Votable a) => UTCTime -> Assessor a
assessOnTimeLimit time = do
(VoteData msgEnd voteVar i assess _) <- get
put (VoteData msgEnd voteVar i assess (Just time))
lift $ void $ onEvent_ (timeEvent time) $ \_ -> do
votes <- getMsgVarData_ voteVar
sendMessage msgEnd (assess $ getVoteStats votes True)
assessOnTimeDelay :: (Votable a) => NominalDiffTime -> Assessor a
assessOnTimeDelay delay = do
t <- addUTCTime delay <$> lift (liftEffect getCurrentTime)
assessOnTimeLimit t
assessWhenEverybodyVoted :: (Votable a) => Assessor a
assessWhenEverybodyVoted = do
(VoteData msgEnd voteVar _ assess _) <- get
lift $ void $ onMsgVarEvent voteVar $ f assess msgEnd where
f assess msgEnd (VUpdated votes) = when (all isJust (map snd votes)) $ sendMessage msgEnd $ assess $ getVoteStats votes True
f _ _ _ = return ()
cleanVote :: (Votable a) => VoteData a -> Nomex EventNumber
cleanVote (VoteData msgEnd voteVar inputsNumber _ _) = onMessageOnce msgEnd $ \_ -> do
delMsgVar voteVar
mapM_ delEvent inputsNumber
quorum :: (Votable a) => Int -> VoteStats a -> Bool
quorum q vs = (voted vs) >= q
withQuorum :: (Votable a) => VoteResult a -> Int -> VoteResult a
withQuorum assess q vs = if quorum q vs then assess vs else []
unanimity :: (Votable a) => VoteStats a -> [Alts a]
unanimity vs = voteQuota (nbVoters vs) vs
majority :: (Votable a) => VoteStats a -> [Alts a]
majority vs = voteQuota ((nbVoters vs) `div` 2 + 1) vs
majorityWith :: (Votable a) => Int -> VoteStats a -> [Alts a]
majorityWith x vs = voteQuota ((nbVoters vs) * x `div` 100 + 1) vs
numberVotes :: (Votable a) => Int -> VoteStats a -> [Alts a]
numberVotes = voteQuota
firstXBest :: forall a. (Votable a) => Int -> VoteStats a -> [Alts a]
firstXBest x votes = map fst $ takeInGroups x sortedVotesWithExAequoGroups where
voted (Just a, n) = Just (a, n)
voted (Nothing, _) = Nothing
sortedVotesWithExAequoGroups :: [[(Alts a, Int)]]
sortedVotesWithExAequoGroups = groupBy ((==) `on` snd) $ sortWith snd $ mapMaybe voted $ M.assocs (voteCounts votes)
takeInGroups :: Int -> [[a]] -> [a]
takeInGroups 0 _ = []
takeInGroups n grps = if grpsSize <= n then head grps ++ takeInGroups (n grpsSize) (tail grps) else head grps where
grpsSize = length $ head grps
firstBest :: (Votable a) => VoteStats a -> [Alts a]
firstBest = firstXBest 1
sortWith :: Ord b => (a -> b) -> [a] -> [a]
sortWith f = sortBy (\x y -> compare (f x) (f y))
voteQuota :: forall a. (Votable a) => Int -> VoteStats a -> [Alts a]
voteQuota q votes = case exclusiveWinner (undefined :: a) of
Nothing -> catMaybes $ M.keys $ M.filter (>= q) (voteCounts votes)
Just a -> maybeToList $ exclusiveVoteQuota q votes a
exclusiveVoteQuota :: (Votable a) => Int -> VoteStats a -> (Alts a, Alts a) -> Maybe (Alts a)
exclusiveVoteQuota q votes (for, against)
| M.findWithDefault 0 (Just for) vs >= q = Just for
| M.findWithDefault 0 (Just against) vs > (nbVoters votes) q = Just against
| otherwise = Nothing
where vs = voteCounts votes
nbVoters :: (Votable a) => VoteStats a -> Int
nbVoters vs
| voteFinished vs = (totalVoters vs) (notVoted vs)
| otherwise = totalVoters vs
totalVoters, voted, notVoted :: (Votable a) => VoteStats a -> Int
totalVoters vs = M.foldr (+) 0 (voteCounts vs)
notVoted vs = fromMaybe 0 $ M.lookup Nothing (voteCounts vs)
voted vs = (totalVoters vs) (notVoted vs)
getVoteStats :: (Votable a) => [Vote a] -> Bool -> VoteStats a
getVoteStats vs voteFinished = VoteStats
{voteCounts = M.fromList $ counts (snd <$> vs),
voteFinished = voteFinished}
counts :: (Eq a, Ord a) => [a] -> [(a, Int)]
counts as = map (head &&& length) (group $ sort as)
displayVoteVar :: (Votable a) => (Maybe PlayerNumber) -> String -> ArrayVar PlayerNumber (Alts a) -> Nomex EventNumber
displayVoteVar mpn title mv = displayVar' mpn mv (showOnGoingVote title)
showChoice :: (Votable a) => Maybe (Alts a) -> String
showChoice (Just a) = show a
showChoice Nothing = "Not Voted"
showChoices :: (Votable a) => [Alts a] -> String
showChoices [] = "no result"
showChoices cs = intercalate ", " $ map show cs
showOnGoingVote :: (Votable a) => String -> [(PlayerNumber, Maybe (Alts a))] -> NomexNE String
showOnGoingVote title listVotes = do
list <- mapM showVote listVotes
return $ title ++ "\n" ++ concatMap (\(name, vote) -> name ++ "\t" ++ vote ++ "\n") list
showFinishedVote :: (Votable a) => [(PlayerNumber, Maybe (Alts a))] -> NomexNE String
showFinishedVote l = do
let voted = filter (\(_, r) -> isJust r) l
votes <- mapM showVote voted
return $ intercalate ", " $ map (\(name, vote) -> name ++ ": " ++ vote) votes
showVote :: (Votable a) => (PlayerNumber, Maybe (Alts a)) -> NomexNE (String, String)
showVote (pn, v) = do
name <- showPlayer pn
return (name, showChoice v)
displayVoteResult :: (Votable a) => String -> VoteData a -> Nomex OutputNumber
displayVoteResult toVoteName (VoteData msgEnd voteVar _ _ _) = onMessage msgEnd $ \result -> do
vs <- getMsgVarData_ voteVar
votes <- liftEffect $ showFinishedVote vs
void $ outputAll_ $ "Vote result for " ++ toVoteName ++ ": " ++ showChoices result ++ " (" ++ votes ++ ")"
onRuleProposed :: (RuleInfo -> Nomex (Msg [ForAgainst]) ) -> Rule
onRuleProposed f = void $ onEvent_ (ruleEvent Proposed) $ \rule -> do
resp <- f rule
void $ onMessageOnce resp $ (activateOrReject rule) . (== [For])
data Referendum = Referendum String deriving (Typeable)
instance Votable Referendum where
data Alts Referendum = Yes | No deriving (Enum, Show, Eq, Bounded, Read, Ord)
alts = [Yes, No]
quota Yes q _ = q
quota No q vs = vs q + 1
name (Referendum n) = "referendum on " ++ n
referendum :: String -> Nomex () -> Rule
referendum name action = do
msg <- voteWith_ (majority `withQuorum` 2) (assessOnEveryVote >> assessOnTimeDelay oneDay) (Referendum name)
void $ onMessageOnce msg resolution where
resolution [Yes] = do
outputAll_ "Positive result of referendum"
action
resolution [No] = void $ outputAll_ "Negative result of referendum"
resolution [] = void $ outputAll_ "No result for referendum"
resolution _ = throwError "Impossible result for referendum"
data Election = Election String deriving (Typeable)
instance Votable Election where
data Alts Election = Candidate {candidate :: PlayerInfo}
alts = map (\n -> Candidate (PlayerInfo n "" Nothing)) [1..]
quota _ q _ = q
name (Election n) = "elections on " ++ n
instance Show (Alts Election) where
show (Candidate (PlayerInfo _ name _)) = name
instance Eq (Alts Election) where
(Candidate (PlayerInfo pn1 _ _)) == (Candidate (PlayerInfo pn2 _ _)) = pn1 == pn2
instance Ord (Alts Election) where
compare (Candidate (PlayerInfo pn1 _ _)) (Candidate (PlayerInfo pn2 _ _)) = compare pn1 pn2
elections :: String -> [PlayerInfo] -> (PlayerNumber -> Nomex ()) -> Nomex ()
elections name pns action = do
msg <- voteWith majority (assessOnTimeDelay oneDay) (Election name) (Candidate <$> pns)
void $ onMessageOnce msg resolution where
resolution [Candidate pi] = do
outputAll_ $ "Result of elections: player(s) " ++ (show $ _playerName pi) ++ " won!"
action $ _playerNumber pi
resolution [] = void $ outputAll_ "Result of elections: nobody won!"
resolution _ = throwError "Impossible result for elections"
voteEvent :: UTCTime -> [PlayerNumber] -> Event ([Maybe Bool])
voteEvent time pns = sequenceA $ map (singleVote time) pns
singleVote :: UTCTime -> PlayerNumber -> Event (Maybe Bool)
singleVote timeLimit pn = (Just <$> inputRadio pn "Vote for "[True, False] True) <|> (Nothing <$ timeEvent timeLimit)
vote :: UTCTime -> [PlayerNumber] -> Event Bool
vote timeLimit pns = unanimity' <$> (voteEvent timeLimit pns)
unanimity' :: [Maybe Bool] -> Bool
unanimity' = all (== Just True)
callVote :: UTCTime -> Nomex ()
callVote t = do
pns <- liftEffect getAllPlayerNumbers
void $ onEventOnce (vote t pns) (outputAll_ . show)