packages feed

MazesOfMonad-1.0.7: src/MoresmauJP/Core/Screen.hs

-- | Console handling code
-- (c) JP Moresmau 2009
module MoresmauJP.Core.Screen where


import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe

import MoresmauJP.Util.Random

-- | a Screen represent a certain UI state, with possible actions
data Screen a = Screen {
        actions::[Action a] -- ^ possible action in that screen
        }

-- | Action doable in the ui
data Action a = Action {
        actionName::String -- ^ the name of the action (what to type)
        , actionDescription::String -- ^ the description (showed when using help)
        , actionFunction::(ActionFunction a) -- ^ the function to call
        }

-- | a Monad transformer, wrapping state (a), IO and Random
type ScreenT a b= (RandT (StateT a IO)) b 

-- | Screen Message to display
type ScreenMessage = String

-- | Screen Messages
type ScreenMessages = [ScreenMessage]

-- | add a ScreenMessage to the current messages
addScreenMessage :: (MonadWriter ScreenMessages m,Monad m)=> ScreenMessage -> m ()
addScreenMessage msg=tell [msg]
        
-- | A Monad Transformer, ScreenT with GameState and returning Widgets
type GSWScreenT a= ScreenT (GameState a) (Widget a)

-- | The real Monad Transformer used: allows to write messages, get random number, stores
-- a state of a GameState containing a, returning Widgets
type WScreenT a=WriterT ScreenMessages (RandT (StateT (GameState a) IO)) (Widget a)

instance (Monad m) =>  MonadRandom (WriterT ScreenMessages (RandT m)) where
        getRandomRange =lift . getRandomRange
        getSplit = lift getSplit


-- | An Action function get its arguments typed in the GUI and works in a WScreenT
type ActionFunction a = [String] -> WScreenT a

-- | GameState: the data (a) and the current screen
data GameState a = GameState {gsData::a -- ^ the data
        , screen::Maybe (Screen a) -- ^ the current screen
        }

-- | Widgets are the actual ui component returned by an action
data Widget a= 
        -- | simple line of text
        WText {
                wtext::String -- ^ line of text
                }
        -- | list of items
        | WList {witems :: [String] -- ^ lines of text
                } 
        -- | ask for the user to type in a string
        | WInput {
                witems :: [String] -- ^ lines of text
                , wact::(String -> WScreenT a) -- ^ handler action
                }
        -- | choose an item from a list
        | WCombo {
                witems :: [String] -- ^ lines of text
                ,wlist :: [String] -- ^ list items
                ,wact :: (String -> WScreenT a) -- ^ handler action
                }
        -- | asks a yes/no question 
        | WCheck {
                witems :: [String]  -- ^ lines of text
                ,wquestion :: String -- ^ question 
                ,wdefault :: Bool -- ^ default answer 
                ,wbact :: (Bool -> WScreenT a) -- ^ handler action
                }
        -- | placeholder for no action 
        | WNothing

-- | a widget and a gamestate
type ScreenState a = (Widget a,GameState a)

-- | get a Combo widget that uses the show method of the objects passed to automatically build the list
getShowCombo :: Show b => [String] -- ^ lines of text
        -> [b] -- ^ objects to display in list 
        ->  ((ComboResult b) -> WScreenT a) -- ^ handler action (gets the selected object as parameter
        -> Widget a -- ^ the resulting widget
getShowCombo= getMappedCombo show

-- | get a Combo widget that uses an arbitrary method of the objects passed to automatically build the list
getMappedCombo :: (b-> String) -- ^ the method to use to translate the object in a string from the menu
        -> [String] -- ^ lines of text
        -> [b] -- ^ objects to display in list 
        ->  ((ComboResult b) -> WScreenT a) -- ^ handler action (gets the selected object as parameter
        -> Widget a -- ^ the resulting widget
getMappedCombo myShow s objs af=
        let 
                objWithNames=map (\x-> (x,myShow x)) objs
                af2=(\s2 -> do 
                        if null s2
                                then
                                        af Empty
                                else
                                        do
                                        let objChosen=listToMaybe (map fst (filter (\x-> (snd x) == s2) objWithNames))
                                        case objChosen of
                                                Just oc-> af (Exact oc)
                                                Nothing-> af (Unknown s2)
                                                )
        in (WCombo s (map snd objWithNames) af2)
                
-- | checks if the rest of the command line is already an option from the menu
getPretypedWidget :: Widget a -- ^ the original widget
        -> [String] -- ^ the rest of the parameters typed by the user
        -> WScreenT a -- ^ the result
getPretypedWidget wc@(WCombo _ choices af) (typed:_) = do
        -- see if the next parameter is a number corresponding to one of the items in the list
        let chosen=filter (\(x,_)-> x==typed) (zipWith (\a b -> ((show a),b)) [1..] choices)
        if (null chosen)
                then return wc
                -- we have a match, run the action
                else af (snd $ head chosen)
getPretypedWidget w _=return w
                
removeWithName :: [Action a] -> [Action b] -> [Action a]
removeWithName aa ab=
        let names=(map actionName aa) \\ (map actionName ab)
        in filter (\a -> elem (actionName a) names) aa

-- | result from a user action
data ComboResult a= Empty -- ^ no result, nothing chosen
         | Unknown String -- ^ unknown result (typed something not in a list)
         | Exact a -- ^ proper choice
        deriving (Show,Read)
        
-- | start a UI loop
start :: ScreenState a -- ^ initial state
        -> IO(a) -- ^ result
start (w,gs)=do
        GameState s2 _ <- ioRandT (commandLoop2 w) gs
        return s2
        
-- | internal UI loop
commandLoop2 :: Widget a -- ^ current widget to render 
        -> GSWScreenT a -- ^ screen monad we run in
commandLoop2 w = do
        af<- liftIO $ renderWidget w
        scr <- gets screen
        if (isJust scr)
                then
                        if isJust af
                                then
                                        do
                                                (w2,msgs) <- runWriterT $ fromJust af
                                                when (not $ null msgs) (liftIO $ (mapM_ putStrLn msgs))        
                                                commandLoop2 w2
                                else                
                                        do
                                                liftIO $ putStr ">"
                                                input <- liftIO $ getLine
                                                let cmds = words input
                                                if null cmds
                                                        then
                                                                commandLoop2 WNothing
                                                        else
                                                                do
                                                                        let (cmd:_)=cmds
                                                                        let af2 = getAction (map Data.Char.toLower cmd) (actions $ fromJust scr)
                                                                        (w2,msgs)<- runWriterT (af2 cmds)
                                                                        when (not $ null msgs) (liftIO $ (mapM_ putStrLn msgs))        
                                                                        commandLoop2 w2
                else
                        return WNothing

-- | render a widget onto the console
renderWidget :: Widget a -- ^ the widget to render        
        -> IO(Maybe(WScreenT a)) -- ^ the result (may be empty)
renderWidget (WNothing)= do
        return Nothing
renderWidget (WText s)= do
        putStrLn s
        return Nothing
renderWidget (WList ss)=do
        mapM_ putStrLn ss
        return Nothing
renderWidget (WInput ss1 af)=do
        mapM_ putStrLn ss1
        input <- getLine
        return (Just $ af input)
renderWidget (WCheck ss1 s def af)=do
        mapM_ putStrLn ss1
        let choices=if def then " (Y/n)" else " (y/N)"
        putStrLn (s ++choices)
        cmds <- getArgs
        let ch=if null cmds
                then def
                else (map toUpper (head cmds))=="Y"
        return (Just (af ch))
renderWidget (WCombo ss1 ss2 af)=do
        mapM_ putStrLn ss1
        let choices=zipWith (\a b -> ((show a),b)) [1..] ss2
        mapM_ putStrLn (map (\(a,b) -> a ++ ": "++b) choices)
        cmds <- getArgs
        let chosen=if null cmds
                then [("","")]
                else filter (\(x,_)-> x==(head cmds)) choices
        if null chosen
                then return (Just $ af "")
                else return (Just $ af (snd $ head chosen))
                        
-- | get arguments typed in at the command line        
getArgs :: IO([String])        -- ^ the resulting arguments                        
getArgs = do
        input <- getLine
        return (words input)

-- | show help with available commands                        
help :: Bool -- ^ display help on system commands too? 
        -> ActionFunction a -- ^ the handler function 
help withSystem _ = do
        let        f (Action s1 s2 _)= (s1++": "++s2)
        let        sysLines= if withSystem
                        then (map f systemActions)
                        else []
        gs <- get
        let acts=actions $ fromJust $ screen gs
        let wl=WList (sort ( sysLines ++
                        (map f acts)))
        return (wl) 

-- | default handler for unknown actions
unknown :: ActionFunction a
unknown args = return (WText ("I do not understand the command " ++ (head args)))

-- | quit the game action
quit :: ActionFunction a
quit _ = do
        modify (\gs-> gs{screen=Nothing})
        return (WText ("Bye bye, hope you enjoyed the game!")) 

-- | when several actions could fit what the user typed, display the list of full names
choice :: [String] -- ^ possible actions
        -> ActionFunction a -- ^ resulting handler
choice ss _ = return (WList ss)

-- | back action
backAction :: Screen a -- ^ Screen to go back to 
        -> Action a -- ^ resulting action
backAction sc=Action "back" "Go back to main screen" (back sc)

-- | back handler
back :: Screen a -- ^ Screen to go back to 
        -> ActionFunction a -- ^ result
back sc _ =do
        (GameState a _) <- get
        put (GameState a (Just sc))
        return (WText "Back")

-- | default system actions
systemActions :: [Action a]
systemActions = [Action "help" "Provides help on available actions" (help True)
        ,Action "?" "Provides help on available actions" (help True)
        ,Action "quit" "Exit the game" quit]
                                
-- | get action from what the user typed and the possible actions
getAction :: String -- ^ the first word the user typed 
        -> [Action a] -- ^ the possible actions 
        -> ActionFunction a -- ^ the chosen action handler
getAction "help" _ = help True
getAction cmd acts =
        let        
                filt=(filter (\x-> isPrefixOf cmd (map Data.Char.toLower (actionName x))))
                possible=(filt systemActions) ++ (filt acts)
                l = length possible
        in
                if l==0 then
                        unknown
                else if l==1 then
                        actionFunction $ head possible
                else 
                        choice (map (\(Action s1 _ _)-> s1) possible)

-- | combine a widget with maybe another
combineMaybeWidget :: Widget a -- ^ initial widget 
        -> Maybe (Widget a) -- ^ next widget 
        -> Widget a -- ^ resulting widget
combineMaybeWidget w Nothing = w
combineMaybeWidget w1 (Just w2) =combineWidget w1 w2
        
-- | combine a widget with another        
combineWidget :: Widget a -- ^ initial widget 
        -> Widget a -- ^ next widget 
        -> Widget a -- ^ resulting widget
combineWidget WNothing a=a
combineWidget a WNothing=a
combineWidget (WText s1) (WText s2)=WList [s1,s2]
combineWidget (WText s1) (WList ss2)=WList (s1:ss2)
combineWidget (WText s1) (WInput ss1 ss2)=WInput (s1:ss1) ss2
combineWidget (WText s1) (WCheck ss1 ss2 ss3 ss4)=WCheck (s1:ss1) ss2 ss3 ss4
combineWidget (WText s1) (WCombo ss1 ss2 af)=WCombo (s1:ss1) ss2 af
combineWidget (WList ss1) (WText s2)=WList (ss1++[s2])
combineWidget (WList ss1) (WList ss2)=WList (ss1++ss2)
combineWidget (WList ss1) (WInput ss2 ss3)=WInput (ss1++ss2) ss3
combineWidget (WList s1) (WCheck ss1 ss2 ss3 ss4)=WCheck (s1++ss1) ss2 ss3 ss4
combineWidget (WList s1) (WCombo ss1 ss2 af)=WCombo (s1++ss1) ss2 af
combineWidget _ _=error "combineWidget: undefined combination"