packages feed

roguestar-gl-0.2.1: src/Driver.lhs

\section{Driver}

\begin{code}
module Driver
    (DriverObject,
     driverNoop,
     newDriverObject,
     driverRead,
     driverGetAnswer,
     driverGetTable,
     driverAction)
    where

import Data.Maybe
import Control.Monad
import Data.IORef
import Data.List
import System.IO
import Tables
import RSAGL.Time
import Control.Concurrent

data RoguestarEngineState = RoguestarEngineState { 
    restate_tables :: [RoguestarTable], 
    restate_answers :: [(String,String)] }

data DriverData = DriverData {
	driver_engine_input_lines :: [String],
	driver_engine_input_line_fragment :: String,
	driver_engine_output_lines :: [String],
	driver_engine_state :: RoguestarEngineState,
	driver_dones :: Integer }

newtype DriverObject = DriverObject (IORef DriverData)

newDriverObject :: IO DriverObject
newDriverObject = liftM DriverObject $ newIORef initial_driver_data

initial_driver_data :: DriverData
initial_driver_data = DriverData {
    driver_engine_input_lines = [],
    driver_engine_input_line_fragment = [],
    driver_engine_output_lines = [],
    driver_engine_state = RoguestarEngineState [] [],
    driver_dones = 0 }

driverGet :: DriverObject -> (DriverData -> a) -> IO a
driverGet (DriverObject ioref) f = liftM f $ readIORef ioref

modifyDriver :: DriverObject -> (DriverData -> DriverData) -> IO () 
modifyDriver (DriverObject ioref) f = modifyIORef ioref f
\end{code}

\begin{code}
driverNoop :: DriverObject -> IO ()
driverNoop driver_object = driverWrite driver_object "noop\n"
\end{code}

\subsection{Getting Protocol Answers}

\texttt{driverGetAnswer} retrieves an answer from the engine.  An answer is a single string.

\begin{code}
driverGetAnswer :: DriverObject -> String -> IO (Maybe String)
driverGetAnswer driver_object query =
    do driverQuery driver_object [query]
       driverGet driver_object (lookup query . restate_answers . driver_engine_state)
\end{code}

\subsection{Getting Protocol Tables}

\testtt{driverGetTable} retrieves a table from the engine.  These tables make up a kind of simple relational database.

\begin{code}
driverGetTable :: DriverObject -> String -> String -> IO (Maybe RoguestarTable)
driverGetTable driver_object the_table_name the_table_id =
    do driverQuery driver_object [the_table_name,the_table_id]
       driverGet driver_object (find (\x -> table_name x == the_table_name && table_id x == the_table_id) . restate_tables . driver_engine_state)
\end{code}

\texttt{driverQuery} transmits a read-only query against the state of the engine.

\begin{code}
driverQuery :: DriverObject -> [String] -> IO ()
driverQuery driver_object query = driverWrite driver_object $ "game query " ++ unwords query ++ "\n"
\end{code}

\begin{code}
driverAction :: DriverObject -> [String] -> IO ()
driverAction driver_object strs = 
    do modifyDriver driver_object $ \driver -> driver { driver_engine_state = RoguestarEngineState [] []}
       driverWrite driver_object $ "game action " ++ unwords strs ++ "\n"
\end{code}

\texttt{driverWrite} writes the specified command to standard output, automatically interleaving calls to read.
\texttt{driverWrite} will never write the same string twice between calls to \texttt{driverReset}.

\begin{code}
driverWrite :: DriverObject -> String -> IO ()
driverWrite driver_object str = 
    do already_sent <- driverGet driver_object (elem str . driver_engine_output_lines)
       unless already_sent $
           do modifyDriver driver_object $ \driver -> driver { driver_engine_output_lines=str:driver_engine_output_lines driver }
              driverWrite_ driver_object str
       driverRead driver_object

driverWrite_ :: DriverObject -> String -> IO ()
driverWrite_ driver_object "" = 
    do hFlush stdout
       driverRead driver_object
driverWrite_ driver_object str = 
    do driverRead driver_object
       putChar $ head str
       --hPutChar stderr $ head str -- uncomment to see everything we write in stderr
       driverWrite_ driver_object $ tail str
\end{code}

\texttt{maybeRead} reads one character if it is available.

\begin{code}
maybeRead :: IO (Maybe Char)
maybeRead = do ready <- hReady stdin
	       case ready of
			  False -> return Nothing
			  True -> do char <- getChar
				     return $ Just char
\end{code}

\texttt{driverRead} reads, parses, and stores data from the engine.

\begin{code}
driverRead :: DriverObject -> IO ()
driverRead driver_object = 
    do maybe_next_char <- maybeRead
       case maybe_next_char of
           Nothing -> return ()
           Just '\n' -> do modifyDriver driver_object
                               (\driver -> driver { driver_engine_input_lines= 
                                                    (reverse $ driver_engine_input_line_fragment driver) :
                                                    driver_engine_input_lines driver,
                                                    driver_engine_input_line_fragment=""});
                           driverUpdate driver_object
           Just next_char -> modifyDriver driver_object 
                                          (\driver -> driver { driver_engine_input_line_fragment=(next_char : 
                                                                                                  driver_engine_input_line_fragment driver) })
       when (isJust maybe_next_char) $ do --hPutChar stderr $ fromJust maybe_next_char -- uncomment to see everything we read in stderr
                                          driverRead driver_object
\end{code}

\texttt{driverUpdate} parses stored information in the driver, generating answers and tables.

\begin{code}
driverUpdate :: DriverObject -> IO ()
driverUpdate driver_object =
    do head_line <- driverGet driver_object (head . driver_engine_input_lines)
       when (head_line == "over") $ interpretText driver_object

data DriverInterpretationState = DINeutral
			       | DIScanningTable RoguestarTable
			       | DIError
				 deriving (Eq,Show)

interpretText :: DriverObject -> IO ()
interpretText driver_object = 
    do final_state <- foldM (interpretLine driver_object) DINeutral =<< driverGet driver_object (reverse . driver_engine_input_lines)
       when (final_state /= DINeutral) $ do hPutStr stderr "interpretText concluded in a non-neutral state, which was:"
					    hPutStr stderr (show final_state)
					    modifyDriver driver_object $ \driver -> initial_driver_data { 
					        driver_dones = driver_dones driver,
						driver_engine_state = driver_engine_state driver }
       modifyDriver driver_object $ \driver -> driver { driver_engine_input_lines = [] }

modifyEngineState :: DriverObject -> (RoguestarEngineState -> RoguestarEngineState) -> IO ()
modifyEngineState driver_object f = modifyDriver driver_object $ \driver -> driver { driver_engine_state = f $ driver_engine_state driver } 

interpretLine :: DriverObject -> DriverInterpretationState -> String -> IO DriverInterpretationState
interpretLine _ DIError _ = return DIError

interpretLine _ di_state "" = do return di_state -- ignore empty lines

interpretLine _ _ str | (head $ words str) == "protocol-error:" = 
    do hPutStr stderr str
       return DIError

interpretLine _ _ str | (head $ words str) == "error:" = 
    do hPutStr stderr str
       return DIError

interpretLine driver_object DINeutral "done" =
    do modifyDriver driver_object $ \driver -> driver { driver_engine_state = RoguestarEngineState [] [],
		   			                driver_engine_output_lines = [],
					                driver_dones = driver_dones driver + 1 }
       return DINeutral

interpretLine _ di_state "done" = 
    do hPutStr stderr  ("client-side protocol error: unexpected \"done\" in " ++ (show di_state) ++ " state.")
       return DIError

interpretLine _ DINeutral "over" = return DINeutral

interpretLine _ (DIScanningTable {}) "over" = 
    do hPutStr stderr "client-side protocol error: 'over' issued while reading a data table"
       return DIError

interpretLine driver_object DINeutral str | (head $ words str) == "answer:" && (length $ words str) == 3 =
    do modifyEngineState driver_object (\engine_state -> engine_state { restate_answers = (words str !! 1,words str !! 2):restate_answers engine_state })
       return DINeutral

interpretLine _ DINeutral str | (head $ words str) == "begin-table" && (length $ words str) > 3 = 
    do t <- getTime
       let table_start_data = words str
       return $ DIScanningTable $ RoguestarTable {
						  table_created = t,
				                  table_name = table_start_data !! 1,
		                                  table_id = table_start_data !! 2,
		                                  table_header = drop 3 table_start_data,
                                                  table_data = []}

interpretLine _ _ str | (head $ words str) == "begin-table" =
    do hPutStr stderr "client-side protocol error: incomplete begin-table header"
       return DIError

interpretLine driver_object (DIScanningTable table) str | (head $ words str) == "end-table" =
  do modifyEngineState driver_object (\engine_state -> engine_state { restate_tables = (table { table_data=reverse $ table_data table}):restate_tables engine_state })
     return DINeutral

interpretLine _ (DIScanningTable table) str = 
    let table_row = words str
        in (if length table_row == (length $ table_header table)
	then return $ DIScanningTable (table { table_data = table_row : table_data table })
	else do hPutStr stderr "client-side protocol error: malformed table row"
		return DIError )

interpretLine _ _ str = 
    do hPutStr stderr str
       return DINeutral

\end{code}