Patch to ansi-terminal-game

This commit is contained in:
Julian Ospald 2022-12-06 20:21:21 +08:00
parent 44e8bec74c
commit 6a5043b68f
Signed by: hasufell
GPG Key ID: 3786C5262ECB4A3F
5 changed files with 59 additions and 36 deletions

View File

@ -22,8 +22,10 @@ module Terminal.Game ( -- * Running
FPS, FPS,
Event(..), Event(..),
GEnv(..), GEnv(..),
Game(..), Game,
GameT(..),
playGame, playGame,
playGameT,
ATGException(..), ATGException(..),
-- ** Helpers -- ** Helpers
@ -33,6 +35,7 @@ module Terminal.Game ( -- * Running
errorPress, errorPress,
blankPlaneFull, blankPlaneFull,
centerFull, centerFull,
cleanAndExit,
-- * Game logic -- * Game logic
-- | Some convenient function dealing with -- | Some convenient function dealing with
@ -119,6 +122,11 @@ module Terminal.Game ( -- * Running
setupGame, setupGame,
narrateGame, narrateGame,
-- * Transformers
GameIO(..),
Test(..),
Narrate(..)
-- | A quick and dirty way to have /hot reload/ -- | A quick and dirty way to have /hot reload/
-- (autorestarting your game when source files change) -- (autorestarting your game when source files change)
-- is illustrated in @example/MainHotReload.hs@. -- is illustrated in @example/MainHotReload.hs@.
@ -133,6 +141,7 @@ import Terminal.Game.Animation
import Terminal.Game.Draw import Terminal.Game.Draw
import Terminal.Game.Layer.Imperative import Terminal.Game.Layer.Imperative
import Terminal.Game.Layer.Object as O import Terminal.Game.Layer.Object as O
import Terminal.Game.Layer.Object.IO ( cleanAndExit )
import Terminal.Game.Plane import Terminal.Game.Plane
import Terminal.Game.Random import Terminal.Game.Random
import Text.LineBreak import Text.LineBreak

View File

@ -5,6 +5,7 @@
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
{-# Language ScopedTypeVariables #-} {-# Language ScopedTypeVariables #-}
{-# Language RankNTypes #-}
module Terminal.Game.Layer.Imperative where module Terminal.Game.Layer.Imperative where
@ -14,23 +15,26 @@ import Terminal.Game.Layer.Object
import qualified Control.Concurrent as CC import qualified Control.Concurrent as CC
import qualified Control.Exception as E import qualified Control.Exception as E
import qualified Control.Monad as CM import qualified Control.Monad as CM
import qualified Control.Monad.Trans as T
import qualified Data.Bool as B import qualified Data.Bool as B
import qualified Data.List as D import qualified Data.List as D
import qualified System.IO as SI import qualified System.IO as SI
import Terminal.Game.Plane import Terminal.Game.Plane
type Game s = GameT IO s
-- | Game definition datatype, parametrised on your gamestate. The two most -- | Game definition datatype, parametrised on your gamestate. The two most
-- important elements are the function dealing with logic and the drawing -- important elements are the function dealing with logic and the drawing
-- one. Check @alone@ demo (@cabal run -f examples alone@) to see a simple -- one. Check @alone@ demo (@cabal run -f examples alone@) to see a simple
-- game in action. -- game in action.
data Game s = data GameT m s =
Game { gTPS :: TPS, Game { gTPS :: TPS,
-- ^ Game speed in ticks per second. You do not -- ^ Game speed in ticks per second. You do not
-- need high values, since the 2D canvas is coarse -- need high values, since the 2D canvas is coarse
-- (e.g. 13 TPS is enough for action games). -- (e.g. 13 TPS is enough for action games).
gInitState :: s, -- ^ Initial state of the game. gInitState :: s, -- ^ Initial state of the game.
gLogicFunction :: GEnv -> s -> Event -> s, gLogicFunction :: GEnv -> s -> Event -> m s,
-- ^ Logic function. -- ^ Logic function.
gDrawFunction :: GEnv -> s -> Plane, gDrawFunction :: GEnv -> s -> Plane,
-- ^ Draw function. Just want to blit your game -- ^ Draw function. Just want to blit your game
@ -67,20 +71,23 @@ centerFull e p = blankPlaneFull e *** p
-- --
-- Need to inspect state on exit? Check 'playGameS'. -- Need to inspect state on exit? Check 'playGameS'.
playGame :: Game s -> IO () playGame :: Game s -> IO ()
playGame g = () <$ runGIO (runGameGeneral g) playGame g = () <$ playGameT T.liftIO g
playGameT :: Monad m => (forall m1 a. T.MonadIO m1 => m a -> m1 a) -> GameT m s -> IO s
playGameT trans g = runGameGeneral trans g
-- | As 'playGame', but do not discard state. -- | As 'playGame', but do not discard state.
playGameS :: Game s -> IO s playGameS :: Game s -> IO s
playGameS g = runGIO (runGameGeneral g) playGameS g = playGameT T.liftIO g
-- | Tests a game in a /pure/ environment. Aims to accurately emulate 'GEnv' -- | Tests a game in a /pure/ environment. Aims to accurately emulate 'GEnv'
-- changes (screen size, FPS) too. -- changes (screen size, FPS) too.
testGame :: Game s -> GRec -> s testGame :: GameT Test s -> GRec -> s
testGame g ts = fst $ runTest (runGameGeneral g) ts testGame g ts = fst $ runTest (runGameGeneral id g) ts
-- | As 'testGame', but returns 'Game' instead of a bare state. -- | As 'testGame', but returns 'Game' instead of a bare state.
-- Useful to fast-forward (e.g.: skip menus) before invoking 'playGame'. -- Useful to fast-forward (e.g.: skip menus) before invoking 'playGame'.
setupGame :: Game s -> GRec -> Game s setupGame :: GameT Test s -> GRec -> GameT Test s
setupGame g ts = let s' = testGame g ts setupGame g ts = let s' = testGame g ts
in g { gInitState = s' } in g { gInitState = s' }
-- xx qua messi solo [Event]? -- xx qua messi solo [Event]?
@ -95,39 +102,40 @@ setupGame g ts = let s' = testGame g ts
-- record-time; this can make emulation slightly inaccurate if — e.g. — -- record-time; this can make emulation slightly inaccurate if — e.g. —
-- you replay the game on a smaller terminal than the one you recorded -- you replay the game on a smaller terminal than the one you recorded
-- the session on. -- the session on.
narrateGame :: Game s -> GRec -> IO s narrateGame :: GameT Narrate s -> GRec -> IO s
narrateGame g e = runReplay (runGameGeneral g) e narrateGame g e = runReplay (runGameGeneral id g) e
-- | Play as in 'playGame' and write the session to @file@. Useful to -- | Play as in 'playGame' and write the session to @file@. Useful to
-- produce input for 'testGame' and 'narrateGame'. Session will be -- produce input for 'testGame' and 'narrateGame'. Session will be
-- recorded even if an exception happens while playing. -- recorded even if an exception happens while playing.
recordGame :: Game s -> FilePath -> IO () recordGame :: GameT Record s -> FilePath -> IO ()
recordGame g fp = recordGame g fp =
E.bracket E.bracket
(CC.newMVar igrec) (CC.newMVar igrec)
(\ve -> writeRec fp ve) (\ve -> writeRec fp ve)
(\ve -> () <$ runRecord (runGameGeneral g) ve) (\ve -> () <$ runRecord (runGameGeneral id g) ve)
data Config = Config { cMEvents :: CC.MVar [Event], data Config = Config { cMEvents :: CC.MVar [Event],
cTPS :: TPS } cTPS :: TPS }
runGameGeneral :: forall s m. MonadGameIO m => runGameGeneral :: forall s m1 m. (Monad m1, MonadGameIO m)
Game s -> m s => (forall a. m1 a -> m a)
runGameGeneral (Game tps s lf df qf) = -> GameT m1 s
-> m s
runGameGeneral trans (Game tps s lf df qf) =
-- init -- init
setupDisplay >> setupDisplay >>
startEvents tps >>= \(InputHandle ve ts) -> startEvents tps >>= \(InputHandle ve ts) ->
displaySizeErr >>= \ds -> displaySizeErr >>= \ds -> do
-- do it! -- do it!
let c = Config ve tps in let c = Config ve tps
cleanUpErr (game c ds) s' <- (game c ds) `onException` (stopEvents ts >> shutdownDisplay)
-- this under will be run regardless stopEvents ts
(stopEvents ts >> return s'
shutdownDisplay )
where where
game :: MonadGameIO m => Config -> Dimensions -> m s game :: MonadGameIO m => Config -> Dimensions -> m s
game c wds = gameLoop c s lf df qf game c wds = gameLoop trans c s lf df qf
Nothing wds Nothing wds
(creaFPSCalc tps) (creaFPSCalc tps)
@ -164,11 +172,12 @@ errorPress m = E.catches m [E.Handler errorDisplay,
----------- -----------
-- from http://www.loomsoft.net/resources/alltut/alltut_lesson6.htm -- from http://www.loomsoft.net/resources/alltut/alltut_lesson6.htm
gameLoop :: MonadGameIO m => gameLoop :: (Monad m1, MonadGameIO m) =>
(forall a. m1 a -> m a) ->
Config -> -- event source Config -> -- event source
s -> -- state s -> -- state
(GEnv -> (GEnv ->
s -> Event -> s) -> -- logic function s -> Event -> m1 s) -> -- logic function
(GEnv -> (GEnv ->
s -> Plane) -> -- draw function s -> Plane) -> -- draw function
(s -> Bool) -> -- quit? function (s -> Bool) -> -- quit? function
@ -176,7 +185,7 @@ gameLoop :: MonadGameIO m =>
Dimensions -> -- Term dimensions Dimensions -> -- Term dimensions
FPSCalc -> -- calculate fps FPSCalc -> -- calculate fps
m s m s
gameLoop c s lf df qf opln td fps = gameLoop trans c s lf df qf opln td fps =
-- quit? -- quit?
checkQuit qf s >>= \qb -> checkQuit qf s >>= \qb ->
@ -190,18 +199,18 @@ gameLoop c s lf df qf opln td fps =
-- no events? skip everything -- no events? skip everything
if null es if null es
then sleepABit (cTPS c) >> then sleepABit (cTPS c) >>
gameLoop c s lf df qf opln td fps gameLoop trans c s lf df qf opln td fps
else else
displaySizeErr >>= \td' -> displaySizeErr >>= \td' ->
-- logic -- logic
let ge = GEnv td' (calcFPS fps) let ge = GEnv td' (calcFPS fps) in
(i, s') = stepsLogic s (lf ge) es in trans (stepsLogic s (lf ge) es) >>= \(i, s') ->
-- no `Tick` events? You do not need to blit, just update state -- no `Tick` events? You do not need to blit, just update state
if i == 0 if i == 0
then gameLoop c s' lf df qf opln td fps then gameLoop trans c s' lf df qf opln td fps
else else
-- FPS calc -- FPS calc
@ -218,15 +227,17 @@ gameLoop c s lf df qf opln td fps =
blitPlane opln' npln >> blitPlane opln' npln >>
gameLoop c s' lf df qf (Just npln) td' fps' gameLoop trans c s' lf df qf (Just npln) td' fps'
-- Int = number of `Tick` events -- Int = number of `Tick` events
stepsLogic :: s -> (s -> Event -> s) -> [Event] -> (Integer, s) stepsLogic :: Monad m => s -> (s -> Event -> m s) -> [Event] -> m (Integer, s)
stepsLogic s lf es = let ies = D.genericLength . filter isTick $ es stepsLogic s lf es = do
in (ies, foldl lf s es) let ies = D.genericLength . filter isTick $ es
where res <- CM.foldM lf s es
isTick Tick = True return (ies, res)
isTick _ = False where
isTick Tick = True
isTick _ = False
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Frame per Seconds -- Frame per Seconds

View File

@ -115,6 +115,7 @@ instance {-# OVERLAPS #-}
(Monad m, T.MonadIO m, MC.MonadMask m, MC.MonadThrow m) => (Monad m, T.MonadIO m, MC.MonadMask m, MC.MonadThrow m) =>
MonadException m where MonadException m where
cleanUpErr m c = MC.finally m c cleanUpErr m c = MC.finally m c
onException m c = MC.onException m c
throwExc t = MC.throwM t throwExc t = MC.throwM t
----------- -----------

View File

@ -39,6 +39,7 @@ class Monad m => MonadTimer m where
-- if a fails, do b (useful for cleaning up) -- if a fails, do b (useful for cleaning up)
class Monad m => MonadException m where class Monad m => MonadException m where
cleanUpErr :: m a -> m b -> m a cleanUpErr :: m a -> m b -> m a
onException :: m a -> m b -> m a
throwExc :: ATGException -> m a throwExc :: ATGException -> m a
class Monad m => MonadLogic m where class Monad m => MonadLogic m where

View File

@ -64,6 +64,7 @@ instance MonadTimer Test where
instance MonadException Test where instance MonadException Test where
cleanUpErr a _ = S.tell [TCleanUpError] >> a cleanUpErr a _ = S.tell [TCleanUpError] >> a
onException a _ = S.tell [TCleanUpError] >> a
throwExc e = error . show $ e throwExc e = error . show $ e
instance MonadLogic Test where instance MonadLogic Test where