ansi-terminal-game

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 19ea926788fba9a248fc93ffe5815b131a5e35f6
parent 347632ef69d32fbdd8af2e8d81e9e7eea8c479b7
Author: Francesco Ariis <fa-ml@ariis.it>
Date:   Tue, 28 Feb 2023 20:18:54 +0100

Move logic function to `Either r s`

Diffstat:
MNEWS | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mansi-terminal-game.cabal | 5+++--
Mexample/Alone.hs | 20+++++++++-----------
Mexample/Balls.hs | 30+++++++++++++++---------------
Mexample/MainBalls.hs | 14++++++++++++--
Mexample/MainHotReload.hs | 2+-
Msrc/Terminal/Game.hs | 2+-
Msrc/Terminal/Game/Layer/Imperative.hs | 167++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/Terminal/Game/Layer/Object/IO.hs | 12++++--------
Msrc/Terminal/Game/Layer/Object/Interface.hs | 15+++++++++------
Msrc/Terminal/Game/Layer/Object/Narrate.hs | 4+---
Msrc/Terminal/Game/Layer/Object/Primitive.hs | 2+-
Msrc/Terminal/Game/Layer/Object/Record.hs | 1+
Msrc/Terminal/Game/Layer/Object/Test.hs | 4+---
Mtest/Terminal/Game/Layer/ImperativeSpec.hs | 33+++++++++++++++++++++------------
Atest/records/alone-record-left.gr | 0
Mtest/records/alone-record-test.gr | 0
Mtest/records/balls-dims.gr | 0
Mtest/records/balls-slow.gr | 0
19 files changed, 232 insertions(+), 130 deletions(-)

diff --git a/NEWS b/NEWS @@ -1,6 +1,57 @@ next ---- +tl;dr and migration guide: +- This version of ansi-terminal-games has a new signature for logic + function: + gLogicFunction :: GEnv -> s -> Event -> Either r s +- Notice the `Either r s`: `Left` means “game is over”; `Right`means + “game continues. +- To migrate a project to 1.9.0.0 you should: + - Adjust logic function to incorporate those changes. + - Get rid of your `quitFunction` in your `Game`. + - Modify every `Game s` to `Game s ()`. + +Breaking changes: +- This version changes the logic function from + gLogicFunction :: GEnv -> s -> Event -> s + to + gLogicFunction :: GEnv -> s -> Event -> Either r s + `Either r s` is a way to explicitly state whether the game is over + not not. If you return `Left $ …` then the game will stop, if you + return `Right $ …` your game will continue. +- the `r` stands for `result` and is present in the type constructor + too: + Game s r -- A game with state `s` which will, + -- upon exit, return a result `r`. +- Usually r is () (as simple games do not care about end results, + they just quit to terminal). But there are cases (games embedded + in a larger program, a set of minigames, high scores) where you + want to return something and this is the way to do it. +- Many other functions have had a slight change of signature to + accomodate this change + playGame :: Game s r -> IO r + narrateGame :: Game s r -> GRec -> IO () + testGame :: Game s -> GRec -> Either s r + I will spend some second on the test function. A game tested in + a pure environment will end in two ways: a) by reaching Left + (proper end game) b) by exhausting the input stream. + In case b) we cannot return a result `r`, but just a half-baked + ingame state. This is very useful for testing purposes. + A trick that I do is this: record events with `recordGame` and + then press Ctrl-C midgame. This way the stream is cut and I will + get a `Right` state when running `testGame`. I can then analyse + the resulting state. +- Functions have been deleted too + playGame :: Game s -> IO s + is no more +- And new functions were introduced: + playGame_ :: Game s r -> IO () -- discard result +- The change was suggested by Gergő Érdi, whom I thank. + The rationale was to improve ergonomics for the game-makes, I + welcome feedback from you. + +Other changes: - Clarified KeyPress and Tick behaviour. tl;dr: *all* keypresses are recorded and fed to your game-logic function. If your played manages to type the Divine Comedy in the space of a Tick, all those characters diff --git a/ansi-terminal-game.cabal b/ansi-terminal-game.cabal @@ -21,8 +21,9 @@ build-type: Simple extra-source-files: README, NEWS, AUTHORS, - test/records/alone-record-test.gr - test/records/balls-dims.gr + test/records/alone-record-test.gr, + test/records/alone-record-left.gr, + test/records/balls-dims.gr, test/records/balls-slow.gr cabal-version: >=1.10 diff --git a/example/Alone.hs b/example/Alone.hs @@ -8,14 +8,12 @@ import Terminal.Game import qualified Data.Tuple as T -- game specification -aloneInARoom :: Game MyState +aloneInARoom :: Game MyState () aloneInARoom = Game 13 -- ticks per second - (MyState (10, 10) - Stop False) -- init state + (MyState (10, 10) Stop) -- init state (\_ s e -> logicFun s e) -- logic function (\r s -> centerFull r $ drawFun s) -- draw function - gsQuit -- quit function sizeCheck :: IO () sizeCheck = let (w, h) = T.swap . snd $ boundaries @@ -25,8 +23,7 @@ sizeCheck = let (w, h) = T.swap . snd $ boundaries -- Types data MyState = MyState { gsCoord :: Coords, - gsMove :: Move, - gsQuit :: Bool } + gsMove :: Move } deriving (Show, Eq) data Move = N | S | E | W | Stop @@ -38,10 +35,11 @@ boundaries = ((1, 1), (24, 80)) ------------------------------------------------------------------------------- -- Logic -logicFun :: MyState -> Event -> MyState -logicFun gs (KeyPress 'q') = gs { gsQuit = True } -logicFun gs Tick = gs { gsCoord = pos (gsMove gs) (gsCoord gs) } -logicFun gs (KeyPress c) = gs { gsMove = move (gsMove gs) c } +logicFun :: MyState -> Event -> Either () MyState +logicFun _ (KeyPress 'q') = Left () +logicFun gs Tick = Right $ gs { gsCoord = pos (gsMove gs) + (gsCoord gs) } +logicFun gs (KeyPress c) = Right $ gs { gsMove = move (gsMove gs) c } -- SCI movement move :: Move -> Char -> Move @@ -75,7 +73,7 @@ pos m oldcs | oob newcs = oldcs -- Draw drawFun :: MyState -> Plane -drawFun (MyState (r, c) _ _) = +drawFun (MyState (r, c) _) = blankPlane mw mh & (1, 1) % box mw mh '.' & (2, 2) % box (mw-2) (mh-2) ' ' & diff --git a/example/Balls.hs b/example/Balls.hs @@ -117,33 +117,35 @@ dtimer (_, i) = word . show $ i -- Game data GState = GState { gen :: StdGen, - quit :: Bool, timer :: Timer, balls :: [Ball], bslow :: Bool } -- pSlow is not used in game, it is there just -- for the test suite -fireworks :: StdGen -> Game GState -fireworks g = Game tps istate lfun dfun qfun +fireworks :: StdGen -> Game GState Int +fireworks g = Game tps istate lfun dfun where tps = 60 istate :: GState - istate = GState g False (ctimer tps) [] False + istate = GState g (ctimer tps) [] False ------------------------------------------------------------------------------- -- Logic -lfun :: GEnv -> GState -> Event -> GState +-- The `Int` in `Either Int Gstate` is: number of balls +-- on screen at the end of the game. +lfun :: GEnv -> GState -> Event -> Either Int GState lfun e s (KeyPress 's') = let g = gen s ds = eTermDims e (b, g1) = genBall g ds - in s { gen = g1, - balls = b : balls s } -lfun _ s (KeyPress 'q') = s { quit = True } -lfun _ s (KeyPress _) = s + s' = s { gen = g1, + balls = b : balls s } + in Right s' +lfun _ s (KeyPress 'q') = Left $ length (balls s) +lfun _ s (KeyPress _) = Right s lfun r s Tick = let ds = eTermDims r @@ -151,12 +153,10 @@ lfun r s Tick = ps' = M.mapMaybe (modPar ds) ps bs = eFPS r < 30 - in s { timer = ltimer (timer s), - balls = filter (isIn ds) ps', - bslow = bs } - -qfun :: GState -> Bool -qfun s = quit s + s' = s { timer = ltimer (timer s), + balls = filter (isIn ds) ps', + bslow = bs } + in Right s' ------------------------------------------------------------------------------- -- Draw diff --git a/example/MainBalls.hs b/example/MainBalls.hs @@ -7,6 +7,16 @@ import Terminal.Game -- Balls Main module. The meat of the game is in `examples/Balls.hs` main :: IO () -main = getStdGen >>= \g -> - playGame (fireworks g) +main = do + g <- getStdGen + r <- playGame (fireworks g) + -- We use game result `r` (how many balls were on + -- screen) and feed it to another function. + -- This could be useful to upload high scores to + -- a site, or for a game embedded in a larger pro- + -- gram, etc. + putStrLn (bye i) + where + bye wi = "See you later!\nYou left the game with " ++ + show wi ++ " balls on screen." diff --git a/example/MainHotReload.hs b/example/MainHotReload.hs @@ -65,7 +65,7 @@ import Terminal.Game cannot handle interactive programs. But if you are just displaying a replay, this can come handy - find example/*.hs | entr -cr cabal run -f examples hot reload + find example/*.hs | entr -cr cabal run -f examples hot-reload This is very useful to incrementally build NPCs’ behaviour, iron out mechanics bugs etc. diff --git a/src/Terminal/Game.hs b/src/Terminal/Game.hs @@ -33,7 +33,7 @@ module Terminal.Game ( -- * Running ATGException(..), -- ** Helpers - playGameS, + playGame_, Terminal.Game.displaySize, assertTermDims, errorPress, diff --git a/src/Terminal/Game/Layer/Imperative.hs b/src/Terminal/Game/Layer/Imperative.hs @@ -15,29 +15,43 @@ import qualified Control.Concurrent as CC import qualified Control.Exception as E import qualified Control.Monad as CM import qualified Data.Bool as B +import qualified Data.Either as ET import qualified Data.List as D import qualified System.IO as SI import Terminal.Game.Plane --- | Game definition datatype, parametrised on your gamestate. The two most --- important elements are the function dealing with logic and the drawing --- one. Check @alone@ demo (@cabal run -f examples alone@) to see a simple --- game in action. -data Game s = - Game { gTPS :: TPS, - -- ^ Game speed in ticks per second. You do not - -- need high values, since the 2D canvas is coarse - -- (e.g. 13 TPS is enough for action games). - gInitState :: s, -- ^ Initial state of the game. - gLogicFunction :: GEnv -> s -> Event -> s, - -- ^ Logic function. - gDrawFunction :: GEnv -> s -> Plane, - -- ^ Draw function. Just want to blit your game - -- in the middle? Check 'centerFull'. - gQuitFunction :: s -> Bool - -- ^ /Should I quit?/ function. - } +-- | Game definition datatype, parametrised on: +-- +-- * your gamestate @s@; and +-- * a result when the game is finished @r@. Simple games do not need this, +-- just fill @r@ with @()@. +-- +-- The two most important elements are the function dealing with logic and +-- the drawing one. Check @alone@ demo (@cabal run -f examples alone@) to +-- see a basic game in action. +data Game s r = Game { + gTPS :: TPS, + -- ^ Game speed in ticks per second. You do not + -- need high values, since the 2D canvas is coarse + -- (e.g. 13 TPS is enough for action games). + gInitState :: s, -- ^ Initial state of the game. + gLogicFunction :: GEnv -> s -> Event -> Either r s, + -- ^ Logic function. @s@ is the game state while @r@ + -- is some sort of result you want to return when the + -- game is finished. + -- + -- If `gLogicFunction` returns 'Right' the game will + -- continue; if it returns 'Left' the game is over + -- (quit condition). + -- + -- Curious to see how @r@ can be useful? Check + -- @cabal run -f examples balls@ and + -- @example/MainBalls.hs@. + gDrawFunction :: GEnv -> s -> Plane + -- ^ Draw function. Just want to blit your game + -- in the middle? Check 'centerFull'. + } -- | A blank plane as big as the terminal. blankPlaneFull :: GEnv -> Plane @@ -64,18 +78,22 @@ centerFull e p = blankPlaneFull e *** p -- @ -- -- in your @.cabal@ file and you will be fine! --- --- Need to inspect state on exit? Check 'playGameS'. -playGame :: Game s -> IO () -playGame g = () <$ runGIO (runGameGeneral g) +playGame :: Game s r -> IO r +playGame g = either id (error "`Right` in playGame") <$> + runGIO (runGameGeneral g) --- | As 'playGame', but do not discard state. -playGameS :: Game s -> IO s -playGameS g = runGIO (runGameGeneral g) +-- | As 'playGame', but ignore the result @r@. +playGame_ :: Game s r -> IO () +playGame_ g = () <$ playGame g -- | Tests a game in a /pure/ environment. Aims to accurately emulate 'GEnv' --- changes (screen size, FPS) too. -testGame :: Game s -> GRec -> s +-- changes (screen size, FPS) too. Returns a result @r@ or a state @s@ in +-- case the Event stream is exhausted before the game exits. +-- +-- A useful trick is to call 'recordGame' and press /Ctrl-C/ while playing +-- (instead of quitting properly). This way @testGame@ will return +-- @Left s@, a state that you can then inspect. +testGame :: Game s r -> GRec -> Either r s testGame g ts = case runTest (runGameGeneral g) ts of (Nothing, l) -> error $ "testGame, exception called: " ++ @@ -85,12 +103,15 @@ testGame g ts = -- more infos on a failed test (Just s, _) -> s --- | As 'testGame', but returns 'Game' instead of a bare state. +-- | As 'testGame', but returns 'Game' instead of result/state. -- Useful to fast-forward (e.g.: skip menus) before invoking 'playGame'. -setupGame :: Game s -> GRec -> Game s +setupGame :: Game s r -> GRec -> Game s r setupGame g ts = let s' = testGame g ts - in g { gInitState = s' } - -- xx qua messi solo [Event]? + in case s' of + -- If the game is already over, return a mock logic + -- function which simply ends the game. + Left r -> g { gLogicFunction = \_ _ _ -> Left r } + Right s -> g { gInitState = s } -- | Similar to 'testGame', runs the game given a 'GRec'. Unlike -- 'testGame', the playthrough will be displayed on screen. Useful when a @@ -102,13 +123,13 @@ setupGame g ts = let s' = testGame g ts -- record-time; this can make emulation slightly inaccurate if — e.g. — -- you replay the game on a smaller terminal than the one you recorded -- the session on. -narrateGame :: Game s -> GRec -> IO s -narrateGame g e = runReplay (runGameGeneral g) e +narrateGame :: Game s r -> GRec -> IO () +narrateGame g e = () <$ runReplay (runGameGeneral g) e --- | Play as in 'playGame' and write the session to @file@. Useful to --- produce input for 'testGame' and 'narrateGame'. Session will be --- recorded even if an exception happens while playing. -recordGame :: Game s -> FilePath -> IO () +-- | Play as in 'playGame' and write the session (input stream, etc.) to +-- @file@. Then you can use this with 'testGame' and 'narrateGame'. Session +-- will be recorded even if an exception happens while playing. +recordGame :: Game s r -> FilePath -> IO () recordGame g fp = E.bracket (CC.newMVar igrec) @@ -118,9 +139,9 @@ recordGame g fp = data Config = Config { cMEvents :: CC.MVar [Event], cTPS :: TPS } -runGameGeneral :: forall s m. MonadGameIO m => - Game s -> m s -runGameGeneral (Game tps s lf df qf) = +runGameGeneral :: forall s r m. MonadGameIO m => + Game s r -> m (Either r s) +runGameGeneral (Game tps s lf df) = -- init setupDisplay >> startEvents tps >>= \(InputHandle ve ts) -> @@ -133,8 +154,8 @@ runGameGeneral (Game tps s lf df qf) = (stopEvents ts >> shutdownDisplay ) where - game :: MonadGameIO m => Config -> Dimensions -> m s - game c wds = gameLoop c s lf df qf + game :: MonadGameIO m => Config -> Dimensions -> m (Either r s) + game c wds = gameLoop c (Right s) lf df Nothing wds (creaFPSCalc tps) @@ -172,35 +193,38 @@ errorPress m = E.catches m [E.Handler errorDisplay, -- from http://www.loomsoft.net/resources/alltut/alltut_lesson6.htm gameLoop :: MonadGameIO m => - Config -> -- event source - s -> -- state + Config -> -- event source + Either r s -> -- state (GEnv -> - s -> Event -> s) -> -- logic function + s -> Event -> + Either r s) -> -- logic function (GEnv -> - s -> Plane) -> -- draw function - (s -> Bool) -> -- quit? function - Maybe Plane -> -- last blitted screen - Dimensions -> -- Term dimensions - FPSCalc -> -- calculate fps - m s -gameLoop c s lf df qf opln td fps = - - -- quit? - checkQuit qf s >>= \qb -> - if qb + s -> Plane) -> -- draw function + Maybe Plane -> -- last blitted screen + Dimensions -> -- Term dimensions + FPSCalc -> -- calculate fps + m (Either r s) +gameLoop c s lf df opln td fps = + + -- Quit? + areEventsOver >>= \qb -> + -- We will quit in case input stream (events) is exhausted. + -- This might happen during test/narrate. + if ET.isLeft s || qb then return s else - -- fetch events (if any) + -- Fetch events (if any). + -- This is safe as we checked for `areEventsOver` above. pollEvents (cMEvents c) >>= \es -> -- no events? skip everything if null es then sleepABit (cTPS c) >> - gameLoop c s lf df qf opln td fps + gameLoop c s lf df opln td fps else - displaySizeErr >>= \td' -> + displaySizeErr >>= \td' -> -- logic let ge = GEnv td' (calcFPS fps) @@ -208,7 +232,7 @@ gameLoop c s lf df qf opln td fps = -- no `Tick` events? You do not need to blit, just update state if i == 0 - then gameLoop c s' lf df qf opln td fps + then gameLoop c s' lf df opln td fps else -- FPS calc @@ -219,22 +243,35 @@ gameLoop c s lf df qf opln td fps = CM.when resc clearDisplay >> -- draw - let opln' | resc = Nothing -- res changed? restart double buffering + let + opln' | resc = Nothing -- res changed? restart double buffering | otherwise = opln - npln = df ge s' in + npln = case s' of + (Right rs) -> df ge rs + (Left _) -> uncurry blankPlane td' + -- In case the logic function came to an end + -- (Left), just print a blank plane. + in blitPlane opln' npln >> - gameLoop c s' lf df qf (Just npln) td' fps' + gameLoop c s' lf df (Just npln) td' fps' -- Int = number of `Tick` events -stepsLogic :: s -> (s -> Event -> s) -> [Event] -> (Integer, s) +stepsLogic :: Either r s -> (s -> Event -> Either r s) -> [Event] -> + (Integer, Either r s) stepsLogic s lf es = let ies = D.genericLength . filter isTick $ es - in (ies, foldl lf s es) + in (ies, logicFold lf s es) where isTick Tick = True isTick _ = False + logicFold :: (s -> Event -> Either r s) -> + Either r s -> [Event] -> Either r s + logicFold _ (Left r) _ = Left r + logicFold wlf (Right ws) wes = CM.foldM wlf ws wes + + ------------------------------------------------------------------------------- -- Frame per Seconds diff --git a/src/Terminal/Game/Layer/Object/IO.hs b/src/Terminal/Game/Layer/Object/IO.hs @@ -37,6 +37,10 @@ instance {-# OVERLAPS #-} (Monad m, T.MonadIO m) => MonadInput m where startEvents tps = T.liftIO $ startIOInput tps pollEvents ve = T.liftIO $ CC.swapMVar ve [] stopEvents ts = T.liftIO $ stopEventsIO ts + areEventsOver = return False + -- IO monad is the actual game, we never quit bar if + -- the logic function returns `Right`. + -- filepath = logging startIOInput :: TPS -> IO InputHandle @@ -117,14 +121,6 @@ instance {-# OVERLAPS #-} cleanUpErr m c = MC.finally m c throwExc t = MC.throwM t ------------ --- Logic -- ------------ - -instance {-# OVERLAPS #-} (Monad m, T.MonadIO m) => - MonadLogic m where - checkQuit fb s = return (fb s) - ------------- -- Display -- ------------- diff --git a/src/Terminal/Game/Layer/Object/Interface.hs b/src/Terminal/Game/Layer/Object/Interface.hs @@ -18,8 +18,7 @@ import qualified Control.Concurrent as CC -- mtl interface for game type MonadGameIO m = (MonadInput m, MonadTimer m, - MonadException m, MonadLogic m, - MonadDisplay m) + MonadException m, MonadDisplay m) data InputHandle = InputHandle { ihKeyMVar :: CC.MVar [Event], @@ -29,6 +28,13 @@ class Monad m => MonadInput m where startEvents :: TPS -> m InputHandle pollEvents :: CC.MVar [Event] -> m [Event] stopEvents :: [CC.ThreadId] -> m () + areEventsOver :: m Bool + -- Why do we need this? For test/narrate purposes. When + -- we play a game events are never over, but when we + -- test/narrate, it might be than the stream of [Event] + -- is exhausted before the state function returns Right. + -- We do not want to be stuck in an endless loop in that + -- case. class Monad m => MonadTimer m where getTime :: m Integer -- to nanoseconds @@ -40,10 +46,6 @@ class Monad m => MonadException m where cleanUpErr :: m a -> m b -> m a throwExc :: ATGException -> m a -class Monad m => MonadLogic m where - -- decide whether it's time to quit - checkQuit :: (s -> Bool) -> s -> m Bool - class Monad m => MonadDisplay m where setupDisplay :: m () clearDisplay :: m () @@ -55,3 +57,4 @@ displaySizeErr :: (MonadDisplay m, MonadException m) => m Dimensions displaySizeErr = displaySize >>= \case Nothing -> throwExc CannotGetDisplaySize Just d -> return d + diff --git a/src/Terminal/Game/Layer/Object/Narrate.hs b/src/Terminal/Game/Layer/Object/Narrate.hs @@ -22,9 +22,7 @@ instance MonadInput Narrate where startEvents fps = T.liftIO $ startEvents fps pollEvents _ = S.state getPolled stopEvents ts = T.liftIO $ stopEvents ts - -instance MonadLogic Narrate where - checkQuit _ _ = S.gets isOver + areEventsOver = S.gets isOver runReplay :: Narrate a -> GRec -> IO a runReplay (Narrate s) k = S.evalStateT s k diff --git a/src/Terminal/Game/Layer/Object/Primitive.hs b/src/Terminal/Game/Layer/Object/Primitive.hs @@ -85,7 +85,7 @@ addPolled es (GRec p s) = GRec (es S.<| p) s getPolled :: GRec -> ([Event], GRec) getPolled (GRec (ps S.:|> p) d) = (p, GRec ps d) -getPolled _ = error "getEvents: empty Seq" +getPolled _ = error "getPolled: empty Seq" isOver :: GRec -> Bool isOver (GRec S.Empty _) = True diff --git a/src/Terminal/Game/Layer/Object/Record.hs b/src/Terminal/Game/Layer/Object/Record.hs @@ -29,6 +29,7 @@ instance MonadInput Record where pollEvents ve = T.liftIO (pollEvents ve) >>= \es -> modMRec addPolled es stopEvents ts = T.liftIO (stopEvents ts) + areEventsOver = T.liftIO areEventsOver instance MonadDisplay Record where setupDisplay = T.liftIO setupDisplay diff --git a/src/Terminal/Game/Layer/Object/Test.hs b/src/Terminal/Game/Layer/Object/Test.hs @@ -58,6 +58,7 @@ instance MonadInput Test where return mockHandle pollEvents _ = S.state getPolled stopEvents _ = S.tell [TStopEvents] + areEventsOver = S.gets isOver instance MonadTimer Test where getTime = return 1 @@ -68,9 +69,6 @@ instance MonadException Test where throwExc e = S.tell [TException e] >> E.throwError () -instance MonadLogic Test where - checkQuit _ _ = S.gets isOver - instance MonadDisplay Test where setupDisplay = () <$ S.tell [TSetupDisplay] clearDisplay = return () diff --git a/test/Terminal/Game/Layer/ImperativeSpec.hs b/test/Terminal/Game/Layer/ImperativeSpec.hs @@ -13,6 +13,12 @@ import qualified Control.Exception as E import qualified Test.QuickCheck as Q import qualified GHC.Exts as X +-- Test for state. +stateTest :: Show r => Game s r -> GRec -> s +stateTest g r = either em id (testGame g r) + where + em wr = error $ "stateTest: " ++ show wr + spec :: Spec spec = do @@ -20,33 +26,35 @@ spec = do let nd = error "<not-defined>" s :: (Integer, Bool, Integer) s = (0, False, 0) - lf (t, True, i) Tick = (t+1, True, i+1) - lf (t, b, i) Tick = (t+1, b, i ) - lf (t, _, i) (KeyPress _) = (t, True, i ) - qf (3, _, _) = True - qf _ = False + lf (t, True, i) Tick = Right (t+1, True, i+1) + lf (t, b, i) Tick = Right (t+1, b, i ) + lf (t, _, i) (KeyPress _) = Right (t, True, i ) es = [Tick, KeyPress 'c', KeyPress 'c', Tick, Tick] - g = Game nd s (const lf) nd qf + g :: Game (Integer, Bool, Integer) () + g = Game nd s (const lf) nd it "does not confuse input and logic" $ - testGame g (createGRec (80, 24) es) `shouldBe` (3, True, 2) + stateTest g (createGRec (80, 24) es) `shouldBe` (3, True, 2) describe "testGame" $ do it "tests a game" $ do r <- readRecord "test/records/alone-record-test.gr" - testGame aloneInARoom r `shouldBe` MyState (20, 66) Stop True + stateTest aloneInARoom r `shouldBe` MyState (20, 66) Stop + it "tests a game exiting correctly" $ do + r <- readRecord "test/records/alone-record-left.gr" + testGame aloneInARoom r `shouldBe` Left () it "picks up screen resize events" $ do r <- readRecord "test/records/balls-dims.gr" let g = fireworks (mkStdGen 1) - t = testGame g r + t = stateTest g r length (balls t) `shouldBe` 1 - it "picks up screen resize events" $ do + it "picks FPS too" $ do r <- readRecord "test/records/balls-slow.gr" let g = fireworks (mkStdGen 1) - t = testGame g r + t = stateTest g r bslow t `shouldBe` True it "does not hang on empty/unclosed input" $ let w = createGRec (80, 24) [Tick] in - testGame aloneInARoom w `shouldBe` MyState (10, 10) Stop False + stateTest aloneInARoom w `shouldBe` MyState (10, 10) Stop modifyMaxSize (const 1000) $ it "does not crash/hang on random input" $ Q.property $ let genEvs = Q.listOf1 Q.arbitrary @@ -61,3 +69,4 @@ spec = do e = "testGame, exception called: [TSetupDisplay,TStartEvents,\ \TException CannotGetDisplaySize]" E.evaluate t `shouldThrow` errorCall e + diff --git a/test/records/alone-record-left.gr b/test/records/alone-record-left.gr Binary files differ. diff --git a/test/records/alone-record-test.gr b/test/records/alone-record-test.gr Binary files differ. diff --git a/test/records/balls-dims.gr b/test/records/balls-dims.gr Binary files differ. diff --git a/test/records/balls-slow.gr b/test/records/balls-slow.gr Binary files differ.