packages feed

ansi-terminal-game-1.3.0.0: src/Terminal/Game/Layer/Object/Interface.hs

-------------------------------------------------------------------------------
-- Layer 2 (mockable IO), as per
-- https://www.parsonsmatt.org/2018/03/22/three_layer_haskell_cake.html
-- 2019 Francesco Ariis GPLv3
-------------------------------------------------------------------------------

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-}

module Terminal.Game.Layer.Object.Interface where

import Terminal.Game.Plane

import qualified Control.Concurrent as CC
import qualified Control.Monad.Catch as MC
import qualified Data.Serialize     as S
import qualified GHC.Generics       as G
import qualified Test.QuickCheck    as Q


-- mtl inferface for game

type MonadGameIO m = (MonadInput m, MonadTimer m,
                      MonadException m, MonadLogic m,
                      MonadDisplay m)


----------------
-- Game input --
----------------

-- | Frames per second.
type FPS = Integer

-- | An @Event@ is a 'Tick' (time passes) or a 'KeyPress'.
data Event = Tick
           | KeyPress Char
           deriving (Show, Eq, G.Generic)

instance S.Serialize Event where

instance Q.Arbitrary Event where
  arbitrary = Q.oneof [ pure Tick,
                        KeyPress <$> Q.arbitrary ]

data InputHandle = InputHandle
            { ihKeyMVar    :: CC.MVar [Event],
              ihOpenThreds :: [CC.ThreadId] }

class Monad m => MonadInput m where
    startEvents :: FPS -> m InputHandle
    pollEvents  :: CC.MVar [Event] -> m [Event]
    stopEvents :: [CC.ThreadId] -> m ()

-----------------
-- Game timing --
-----------------

class Monad m => MonadTimer m where
    getTime :: m Integer     -- to nanoseconds
    sleepABit :: FPS -> m () -- useful not to hammer cpu while polling

--------------------
-- Error handling --
--------------------

-- if a fails, do b (useful for cleaning up)
class Monad m => MonadException m where
    cleanUpErr :: m a -> m b -> m a
    throwExc :: ATGException -> m a

-- | @ATGException@s are thrown synchronously for easier catching.
data ATGException =
      CannotGetDisplaySize
    | DisplayTooSmall Coords Coords -- ^ Required size and actual size.

instance Show ATGException where
    show CannotGetDisplaySize = "Cannot get display size!"
    show (DisplayTooSmall (gw, gh) (sw, sh)) =
            "This games requires a screen of " ++ show gw ++
            " columns and " ++ show gh ++ " rows.\n" ++
            "Yours only has " ++ show sw ++ " columns and " ++
            show sh ++ " rows!\n\n" ++
            "Please resize your terminal and relaunch " ++
            "the game!\n"

instance MC.Exception ATGException


-----------
-- Logic --
-----------

-- if a fails, do b (useful for cleaning up)
class Monad m => MonadLogic m where
    -- decide whether it's time to quit
    checkQuit :: (s -> Bool) -> s -> m Bool

-------------
-- Display --
-------------

class Monad m => MonadDisplay m where
    setupDisplay :: m ()
    clearDisplay :: m ()
    displaySize :: m (Maybe (Width, Height))
    blitPlane :: Width -> Height -> Maybe Plane -> Plane -> m ()
    shutdownDisplay :: m ()

-----------
-- Utils --
-----------

displaySizeErr :: (MonadDisplay m, MonadException m) => m (Width, Height)
displaySizeErr = displaySize >>= \case
                   Nothing -> throwExc CannotGetDisplaySize
                   Just d -> return d