packages feed

ansi-terminal-game 1.5.0.0 → 1.6.0.0

raw patch · 14 files changed

+463/−135 lines, 14 filesnew-component:exe:ballsPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Terminal.Game: DisplayTooSmall :: Coords -> Coords -> ATGException
- Terminal.Game: [gScreenHeight] :: Game s -> Height
- Terminal.Game: [gScreenWidth] :: Game s -> Width
+ Terminal.Game: GEnv :: Dimensions -> FPS -> GEnv
+ Terminal.Game: [eFPS] :: GEnv -> FPS
+ Terminal.Game: [eTermDims] :: GEnv -> Dimensions
+ Terminal.Game: data GEnv
+ Terminal.Game: simpleGame :: forall s. (Width, Height) -> TPS -> s -> (s -> Event -> s) -> (s -> Plane) -> (s -> Bool) -> Game s
+ Terminal.Game: type Dimensions = (Width, Height)
+ Terminal.Game: type FPS = Integer
- Terminal.Game: Game :: Width -> Height -> TPS -> s -> (s -> Event -> s) -> (s -> Plane) -> (s -> Bool) -> Game s
+ Terminal.Game: Game :: TPS -> s -> (GEnv -> s -> Event -> s) -> (GEnv -> s -> Plane) -> (s -> Bool) -> Game s
- Terminal.Game: [gDrawFunction] :: Game s -> s -> Plane
+ Terminal.Game: [gDrawFunction] :: Game s -> GEnv -> s -> Plane
- Terminal.Game: [gLogicFunction] :: Game s -> s -> Event -> s
+ Terminal.Game: [gLogicFunction] :: Game s -> GEnv -> s -> Event -> s
- Terminal.Game: displaySize :: IO (Width, Height)
+ Terminal.Game: displaySize :: IO Dimensions
- Terminal.Game: planeSize :: Plane -> (Width, Height)
+ Terminal.Game: planeSize :: Plane -> Dimensions

Files

CHANGES view
@@ -1,3 +1,49 @@+1.6.0.0+-------++Summary and tl;dr migration guide:+- This version introduces a breaking changes in the main way to make+  a `Game`. I will detail the changes below, but first a two-lines+  migration guide:+    the only thing you should have to do is to replace your `Game`+    data constructor with `simpleGame` smart constructor.+  And of course, if you are interested in displaying FPS and adapt to+  screen size modifications at game-time (“liquid” layout), read along!++Changes:+- This version introduces GEnv, a structure that exposes current frame+  rate (in FPS) and current terminal size (in Width, Height).+- `GEnv` is added as a parameter to logic and draw functions, which+  now have these signatures:+    gLogicFunction :: GEnv -> s -> Event -> slightly+    gDrawFunction :: GEnv -> s -> plane+- If you do not want to dabble with GEnv, you can still use `simpleGame`+  smart constructor, which mimicks the old `Game`. `simpleGame` has some+  nice defaults:+    - if the terminal is too small it will ask the player to resize it+      (even in the middle of the game), blocking any input;+    - if the terminal is bigger, it will paste `Plane` in the middle+      of the screen.+- For this reason, `DisplayTooSmall` exception exists no more.+- the new `Game` does not have those defaults, but allows you to get+  creative with screen resizes, e.g. accomodating as much gameworld+  as possible etc. Check `cabal run -f examples balls` and resize the+  screen to see it in action.+- Minor change: I have introduced a `Dimensions` alias for+  `(Width, Height)`.++Future work:+- these changes lay the path for an even more general `Game` type,+  adding effects like reading form a game configuration, writing to it+  etc.+  I would like to have these wrapped in a pure interface (maybe à la+  Response/Request? Maybe callbacks?) and for sure want them to be+  composable with current test scaffolding (testGame,+  narrateGame, etc.). It will not be easy to design; if are reading+  this and have any suggestion, please write to me.++Released dom 14 nov 2021, 20:25:19+ 1.5.0.0 ------- @@ -10,10 +56,13 @@ - Removed `getFrames` from Animation interface. - Updated `Random` interface to fit the new `random`. This is a breaking   change but it should be easy to fix by updating your `Random` constraints-  to `UniformRange`+  to `UniformRange`.+  Be mindful that `recordGame` could play slightly differently, as the+  update function for the StdGen in `random` has changed. - Removed `getRandomList` from Random interface. - Added `pickRandom` to Random interface. - Removed unuseful `creaStaticAnimation` from Animation interface.+- Released mar 9 nov 2021, 15:56:14.  1.4.0.0 -------
README view
@@ -27,6 +27,16 @@      http://www.ariis.it/static/articles/venzone/page.html +Other games made with a-t-g+---------------------------++- caverunner:+    https://github.com/simonmichael/games/blob/main/caverunner/caverunner.hs+- pigafetta: http://www.ariis.it/link/repos/pigafetta/+- avoidance: https://sabadev.xyz/avoidance_game++If you want yours to be added to this list, write to me.+ Contact ------- 
ansi-terminal-game.cabal view
@@ -1,5 +1,5 @@ name:                ansi-terminal-game-version:             1.5.0.0+version:             1.6.0.0 synopsis:            sdl-like functions for terminal applications, based on                      ansi-terminal description:         Library which aims to replicate standard 2d game@@ -134,6 +134,7 @@     other-modules:    Alone     default-language: Haskell2010     ghc-options:      -threaded+                      -Wall  executable alone-playback     if flag(examples)@@ -148,3 +149,17 @@     other-modules:    Alone     default-language: Haskell2010     ghc-options:      -threaded+                      -Wall++executable balls+    if flag(examples)+      build-depends:  base == 4.*,+                      ansi-terminal-game+    else+      buildable:      False++    hs-source-dirs:   example+    main-is:          Balls.hs+    default-language: Haskell2010+    ghc-options:      -threaded+                      -Wall
example/Alone.hs view
@@ -5,21 +5,19 @@  import Terminal.Game +import qualified Data.Tuple as T --- this is the game specification-aloneInARoom :: Game MyState-aloneInARoom =-    Game { gScreenWidth  = gw,-           gScreenHeight = gh,-           gTPS          = 13, -           gInitState     = MyState (10, 10) Stop False,-           gLogicFunction = logicFun,-           gDrawFunction  = drawFun,-           gQuitFunction  = gsQuit }-    where-          (gh, gw) = snd boundaries-+-- game specification+aloneInARoom :: Game MyState+aloneInARoom = let ds = T.swap . snd $ boundaries in+               simpleGame ds                   -- size+                          13                   -- ticks per second+                          (MyState (10, 10)+                                   Stop False) -- init state+                          logicFun             -- logic function+                          drawFun              -- draw function+                          gsQuit               -- quit function  ----------- -- TYPES --
+ example/Balls.hs view
@@ -0,0 +1,185 @@+module Main where++import Terminal.Game++import qualified Data.Bool as B+import qualified Data.Ix as I+import qualified Data.Maybe as M+import qualified Data.Tuple as T++{-+   There are three things I will showcase in this example:++   1. ** How you can display current FPS. **+      This is done using `generalGame` to create your game rather than+      `simpleGame`. `generalGame` is a bit more complex but you gain+      additional infos to manipulate/blit, like FPS.++   2. ** How your game can gracefully handle screen resize. **+      Notice how if you resize the terminal, balls will still+      fill the entire screen. This is again possible using `generalGame`+      and the information passed via GameEnv (in this case, terminal+      dimensions).++   3. ** That — while FPS can change  — game speed does not. **+      Check the timer: even when screen is crowded and frames are+      dropped, it is not slowed down.+++   This game runs at 60 FPS, you will almost surely never need such+   a high TPS! 15-20 is more than enough in most cases.+-}++main :: IO ()+main = getStdGen >>= \g ->+       playGame (fireworks g)++-------------------------------------------------------------------------------+-- Ball++data Ball = Ball { pChar :: Plane,+                   pSpeed :: Timed Bool,+                   pDir :: Coords,+                   pPos :: Coords }++-- change direction is necessary, then and move+modPar :: Dimensions -> Ball -> Maybe Ball+modPar ds b@(Ball _ _ d _) =+            -- tick the ball and check it is time to move+            let b' = tickBall b in+            if not (fetchFrame . pSpeed $ b')+              then Just b' -- no time to move for you+            else++            -- check all popssible directions+            let pd = [d, togR d, togC d, togB d]+                bs  = map (\ld -> b' { pDir = ld })  pd+                bs' = filter (isIn ds) $ map modPos bs in++            -- returns a moved ball nor nothing to mark it “to eliminate”+            case bs' of+              [] -> Nothing+              (cp:_) -> Just cp+    where+          togR (wr, wc) = (-wr,  wc)+          togC (wr, wc) = ( wr, -wc)+          togB (wr, wc) = (-wr, -wc)++tickBall :: Ball -> Ball+tickBall b = b { pSpeed = tick (pSpeed b) }++modPos :: Ball -> Ball+modPos (Ball p t d@(dr, dc) (r, c)) = Ball p t d (r+dr, c+dc)++isIn :: Dimensions -> Ball -> Bool+isIn (w, h) (Ball p _ _ (pr, pc)) =+            let (pw, ph) = planeSize p+            in pr >= 1 &&+               pr+ph-1 <= h &&+               pc >= 1 &&+               pc+pw-1 <= w++dpart :: Ball -> (Coords, Plane)+dpart (Ball p _ _ cs) = (cs, p)++genBall :: StdGen -> Dimensions -> (Ball, StdGen)+genBall g ds =+                let (c, g1) = pickRandom [minBound..] g+                    (s, g2) = getRandom (1, 3) g1+                    (v, g3) = pickRandom dirs g2+                    (p, g4) = ranIx ((1,1), T.swap ds) g3+                    b = Ball (cell 'o' # color c Vivid)+                             (creaBoolTimerLoop s) v p+                in (b, g4)+        where+              dirs = [(1, 1), (1, -1), (-1, 1), (-1, -1)]++              -- tuples instances are yet to be added to `random`+              -- as nov 21; this will do meanwhile.+              ranIx :: I.Ix a => (a, a) -> StdGen -> (a, StdGen)+              ranIx r wg = pickRandom (I.range r) wg++-------------------------------------------------------------------------------+-- Timer++type Timer = (Timed Bool, Integer)++ctimer :: TPS -> Timer+ctimer tps = (creaBoolTimerLoop tps, 0)++ltimer :: Timer -> Timer+ltimer (t, i) = let t' = tick t+                    k = B.bool 0 1 (fetchFrame t')+                in (t', i+k)++dtimer :: Timer -> Plane+dtimer (_, i) = word . show $ i++-------------------------------------------------------------------------------+-- Game++data GState = GState { gen :: StdGen,+                       quit :: Bool,+                       timer :: Timer,+                       balls :: [Ball] }++fireworks :: StdGen -> Game GState+fireworks g = Game tps istate lfun dfun qfun+    where+          tps = 60++          istate :: GState+          istate = GState g False (ctimer tps) []++-------------------------------------------------------------------------------+-- Logic++lfun :: GEnv -> GState -> Event -> 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+lfun r s Tick           =+            let ds = eTermDims r++                ps = balls s+                ps' = M.mapMaybe (modPar ds) ps+            in s { timer = ltimer (timer s),+                   balls = filter (isIn ds)  ps' }++qfun :: GState -> Bool+qfun s = quit s++-------------------------------------------------------------------------------+-- Draw++dfun :: GEnv -> GState -> Plane+dfun r s =             mergePlanes+                         (uncurry blankPlane ds)+                         (map dpart $ balls s) &+            (1, 2) %^> tui # trans &+            (1, 2) %.< inst # trans # bold+    where+          ds = eTermDims r+          tm = timer s++          tui :: Plane+          tui = let fps = eFPS r+                    np = length $ balls s++                    l1 = word "FPS: " ||| word (show fps)+                    l2 = word "Timer: " ||| dtimer tm+                    l3 = word ("Balls: " ++ show np)+                    l4 = word ("Term. dims.: " ++ show ds)+                in vcat [l1, l2, l3, l4]++          inst :: Plane+          inst = word "Press (s) to spawn" ===+                 word "Press (q) to quit"++          trans :: Draw+          trans = makeTransparent ' '
example/Playback.hs view
@@ -21,7 +21,7 @@         recordGame aloneInARoom f         prompt "Press <Enter> to watch playback."         es <- readRecord f-        narrateGame aloneInARoom es+        _ <- narrateGame aloneInARoom es         prompt "Playback over! Press <Enter> to quit."     where           prompt :: String -> IO ()
src/Terminal/Game.hs view
@@ -10,25 +10,15 @@ -- -- Machinery and utilities for 2D terminal games. ----- New? Start from 'Game'.+-- New? Start from 'simpleGame'. -- --------------------------------------------------------------------------------  -- Basic col-on-black ASCII terminal, operations. -- Only module to be imported. --- todo a-t-g senza timer -- todo test that handlers are closed in case of errr [test] -- todo invert # invert (doppio) si elimina? [test] [design]--- todo schermo piccolo, si centra verticalmente? [test]--- todo metti in a-t-g la formula «menu, poi fai questo, poi---      ritorni, astrai questo pattern [menu]---- todo timer in contrib o altro modulo?--- todo controlla caratteri strani su windows, prova con morganw e ibispi--- todo boxratio, blit top left etc in ansi-terminal-game? [u:2]--- todo autoresize terminal (or get-term-size?)--- todo ranSelect :: [a] -> m a in a-t-g [design] -- todo random shuffle in atg? -- todo (da sm) sm any plans to expose more of these bits in a future --      release, making ansi-terminal-game more of a full game engine ?@@ -38,14 +28,22 @@ --      play, to speed things up -- todo (da sm) hot reload for game (see IHP web app) [suggestion] -- todo (da sm) sound with sox--- todo (da sm) add tick counter -- todo eccezioni con maybe o lanciando l’eccezione?+-- todo (da sm) not having access to IO forces me to restart a-t-g at+--      each level (if I want to write to disk).+--      If they C-c that will not be saved too.+--      <icrbow> an engine can provide a separate post-frame callback that+--      has read-only access to world and stuff, but can do `IO ()`.+-- todo space (stack/heap) analysis   module Terminal.Game ( -- * Running                        TPS,+                       FPS,                        Event(..),+                       GEnv(..),                        Game(..),+                       simpleGame,                        playGame,                         -- * Game logic@@ -88,6 +86,7 @@                         -- ** Plane                        Plane,+                       Dimensions,                        Coords,                        Row, Column,                        Width, Height,@@ -192,5 +191,5 @@  -- | /Usable/ terminal display size (on Win32 console the last line is -- set aside for input). Throws 'CannotGetDisplaySize' on error.-displaySize :: IO (Width, Height)+displaySize :: IO Dimensions displaySize = O.displaySizeErr
src/Terminal/Game/Layer/Imperative.hs view
@@ -8,36 +8,92 @@  module Terminal.Game.Layer.Imperative where +import Terminal.Game.Draw import Terminal.Game.Layer.Object -import qualified Control.Concurrent   as CC-import qualified Control.Exception    as E-import qualified Control.Monad        as CM-import qualified System.IO            as SI+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.List as D+import qualified System.IO as SI  import Terminal.Game.Plane --- xxx also when it goes to crash screen, it says press any key to---     continue, yet only enter works+-- | Game environment with current terminal dimensions and current display+-- rate.+data GEnv = GEnv { eTermDims :: Dimensions,+                   eFPS :: FPS } --- | Game definition datatype, parametrised on your gamestate. The two--- most important elements are the function dealing with logic and the--- drawing one. Check @alone@ (you can compile it with @cabal run -f--- examples alone@) to see a simple game in action.-data Game s = Game {-        gScreenWidth   :: Width,           -- ^Gamescreen width, in columns.-        gScreenHeight  :: Height,          -- ^Gamescreen height, in rows.-        gTPS           :: TPS,             -- ^Ticks per second. Since the-                                           -- 2D «char canvas» is coarse, you-                                           -- do not need a high value (e.g.-                                           -- 13 TPS is enough for action-                                           -- games).-        gInitState     :: s,               -- ^Initial state of the game.-        gLogicFunction :: s -> Event -> s, -- ^Logic function.-        gDrawFunction  :: s -> Plane,      -- ^Draw function.-        gQuitFunction  :: s -> Bool        -- ^«Should I quit?» function.-      }+-- | General way to create a 'Game'. This allows you more control by+-- exposing 'GEnv' (allows you to e.g. adapt to screen resizes, blit+-- FPS). If you fancy simple, sensible defaults, check 'simpleGame'.+data Game s =+        Game { gTPS           :: TPS, -- ^ Ticks per second. Since the 2D+                                      -- “char canvas” is coarse, you do not+                                      -- need high values (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.+               gQuitFunction  :: s -> Bool+                                      -- ^ “Should I quit?” function.+                                      } +-- | Simplest way to create a game. The two most important parameters+-- are the function dealing with logic and the drawing one. Check @alone@+-- (you can compile it with @cabal run -f examples alone@) to see a basic+-- game in action. If you want more control, look at 'Game'.+simpleGame :: forall s.+              (Width, Height)      -- ^ Gamescreen dimensions, in columns and+                                   --   rows. Asks the player to resize their+                                   --   terminal if it is too small.+              -> TPS               -- ^ Ticks per second. Since the 2D “char+                                   --   canvas” is coarse, you do not need+                                   --   high values (e.g. 13 TPS is enough+                                   --   for action games).+              -> s                 -- ^ Initial state of the game.+              -> (s -> Event -> s) -- ^ Simple logic function.+              -> (s -> Plane)      -- ^ Simple draw function.+              -> (s -> Bool)       -- ^ “Should I quit?” function.+              -> Game s+simpleGame (sw, sh) tps s lf df qf = Game tps s lf' df' qf+    where+          lf' :: GEnv -> s -> Event -> s+          lf' wen ws we+                | isSmaller (eTermDims wen) = ws+                | otherwise = lf ws we++          df' :: GEnv -> s -> Plane+          df' wen ws =+                let ds = eTermDims wen in+                if isSmaller ds+                  then smallMsg ds+                  else uncurry blankPlane ds *** df ws++          colS ww = ww < sw+          rowS wh = wh < sh+          isSmaller :: Dimensions -> Bool+          isSmaller (ww, wh) = colS ww || rowS wh++          smallMsg :: Dimensions -> Plane+          smallMsg (ww, wh) =+                let cm = show ww ++ " columns"+                    rm = show wh ++ " rows"+                    em | colS ww && rowS wh = cm ++ " and " ++ rm+                       | colS ww = cm+                       | rowS wh = rm+                       | otherwise = "smallMsg: passed correct term size!"+                in+                textBoxLiquid ww $+                  "This games requires a screen of " ++ show sw +++                  " columns and " ++ show sh ++ " rows.\n" +++                  "Yours only has " ++ em ++ "!\n\n" +++                  "Please resize your terminal now!"++ -- | Entry point for the game execution, should be called in @main@. -- -- You __must__ compile your programs with @-threaded@; if you do not do@@ -93,23 +149,23 @@  runGameGeneral :: forall s m. MonadGameIO m =>                   Game s -> m s-runGameGeneral (Game gw gh tps s lf df qf) =+runGameGeneral (Game tps s lf df qf) =             -- init-            sizeException gw gh >>-            setupDisplay     >>-            startEvents tps  >>= \(InputHandle ve ts) ->+            setupDisplay    >>+            startEvents tps >>= \(InputHandle ve ts) ->+            displaySizeErr  >>= \ds ->              -- do it!             let c = Config ve tps in-            cleanUpErr (game c)+            cleanUpErr (game c ds)                             -- this under will be run regardless                        (stopEvents ts >>                         shutdownDisplay  )     where-          game :: MonadGameIO m => Config -> m s-          game c = gameLoop gw gh c-                            s lf df qf-                            Nothing (0,0)+          game :: MonadGameIO m => Config -> Dimensions -> m s+          game c wds = gameLoop c s lf df qf+                                Nothing wds+                                (creaFPSCalc tps)   -- | Wraps an @IO@ computation so that any 'ATGException' or 'error' gets@@ -128,8 +184,7 @@               putStrLn l            atgDisplay :: ATGException -> IO a-          atgDisplay e = report $-              putStrLn (show e)+          atgDisplay e = report $ print e            report :: IO () -> IO a           report wm =@@ -147,17 +202,18 @@  -- from http://www.loomsoft.net/resources/alltut/alltut_lesson6.htm gameLoop :: MonadGameIO m     =>-            Width             -> -- gamewidth-            Height            -> -- gameheight             Config            -> -- event source             s                 -> -- state-            (s -> Event -> s) -> -- logic function-            (s -> Plane)      -> -- draw function+            (GEnv ->+             s -> Event -> s) -> -- logic function+            (GEnv ->+             s -> Plane)      -> -- draw function             (s -> Bool)       -> -- quit? function             Maybe Plane       -> -- last blitted screen-            (Width, Height)   -> -- Term Dimensions+            Dimensions        -> -- Term dimensions+            FPSCalc           -> -- calculate fps             m s-gameLoop gw gh c s lf df qf opln td =+gameLoop c s lf df qf opln td fps =          -- quit?         checkQuit qf s >>= \qb ->@@ -167,45 +223,78 @@          -- fetch events (if any)         pollEvents (cMEvents c) >>= \es ->-            -- xxx test poll events si rompe se lo sposto su          -- no events? skip everything         if null es           then sleepABit (cTPS c)               >>-               gameLoop gw gh c s lf df qf opln td-               -- xxx reader monad qui+               gameLoop c s lf df qf opln td fps         else +        displaySizeErr            >>= \td' ->+         -- logic-        let s' = stepsLogic s lf es in+        let ge = GEnv td' (calcFPS fps)+            (i, s') = stepsLogic s (lf ge) es in -        -- clear screen if resolution change-        displaySizeErr            >>= \td'@(tw, th) ->+        -- 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+        else++        -- FPS calc+        let fps' = addFPS i fps in++        -- clear screen if resolution changed         let resc = td /= td' in         CM.when resc clearDisplay >>          -- draw-        -- xxx solo se è tick e non kpress? [loop]         let opln' | resc = Nothing -- res changed? restart double buffering                   | otherwise = opln-            gpl   = blankPlane gw gh-            npln  = pastePlane (df s') gpl (1, 1) in-        blitPlane tw th opln' npln >>+            npln = df ge s' in -        gameLoop gw gh c s' lf df qf (Just npln) td'+        blitPlane opln' npln >> +        gameLoop c s' lf df qf (Just npln) td' fps' -stepsLogic :: s -> (s -> Event -> s) -> [Event] -> s-stepsLogic s lf es = foldl lf s es+-- Int = number of `Tick` events+stepsLogic :: s -> (s -> Event -> s) -> [Event] -> (Integer, s)+stepsLogic s lf es = let ies = D.genericLength . filter isTick $ es+                     in (ies, foldl lf s es)+    where+          isTick Tick = True+          isTick _    = False +-------------------------------------------------------------------------------+-- Frame per Seconds --------------------- ANCILLARIES --------------------+-- | The number of frames blit to terminal per second. Frames might be+-- dropped, but game speed will remain constant. Check @balls@+-- (@cabal run -f examples balls@) to see how to display FPS.+type FPS = Integer -sizeException :: (MonadDisplay m, MonadException m) => Width -> Height -> m ()-sizeException gw gh =-        displaySizeErr >>= \(sw, sh) ->-        CM.when (gw > sw || gh > sh)-                (throwExc $ DisplayTooSmall (gw, gh) (sw, sh))+data FPSCalc = FPSCalc [Integer] TPS+    -- list with number of `Ticks` processed at each blit and expected+    -- FPS (i.e. TPS) +-- the size of moving average will be TPS (that simplifies calculations)+creaFPSCalc :: TPS -> FPSCalc+creaFPSCalc tps = FPSCalc (D.genericReplicate (tps*1) 1) tps+    -- tps*1: size of thw window in **blit actions** (not tick actions!)+    --        so keeping it small should be responsive and non flickery+    --        at the same time!++-- add ticks+addFPS :: Integer -> FPSCalc -> FPSCalc+addFPS nt (FPSCalc (_:fps) tps) = FPSCalc (fps ++ [nt]) tps+addFPS _ (FPSCalc [] _) = error "addFPS: empty list."++calcFPS :: FPSCalc -> Integer+calcFPS (FPSCalc fps tps) =+        let ts = sum fps+            ds = D.genericLength fps+        in roundQuot (tps * ds) ts+    where+          roundQuot :: Integer -> Integer -> Integer+          roundQuot a b = let (q, r) = quotRem a b+                          in q + B.bool 0 1 (r > div b 2)
src/Terminal/Game/Layer/Object.hs view
@@ -27,8 +27,3 @@ -- they'll just touch the classes from interface which behaviour they -- will modify; being more specific, they will be chosen instead of plain -- IO.---- xxx cambia questa descrizione---- xxx resize term to be smaller and see characters broken [ibispi]-
src/Terminal/Game/Layer/Object/IO.hs view
@@ -76,9 +76,9 @@ addTick mr ve tps el =                 -- precise timing. With `treadDelay`, on finer TPS,                 -- ticks take too much (check threadDelay doc).-                getTimeTick tps                     >>= \t ->-                CM.replicateM (fromIntegral $ t-el)-                              (addEvent mr ve Tick) >>+                getTimeTick tps                      >>= \t ->+                CM.replicateM_ (fromIntegral $ t-el)+                               (addEvent mr ve Tick) >>                  -- sleep some                 sleepABit tps >>@@ -139,10 +139,10 @@     setupDisplay = T.liftIO initPart     clearDisplay = T.liftIO clearScreen     displaySize = T.liftIO displaySizeIO-    blitPlane w h mp p = T.liftIO (blitPlaneIO w h mp p)+    blitPlane mp p = T.liftIO (blitPlaneIO mp p)     shutdownDisplay = T.liftIO cleanAndExit -displaySizeIO :: IO (Maybe (Width, Height))+displaySizeIO :: IO (Maybe Dimensions) displaySizeIO =         TS.size        >>= \ts ->             -- cannot use ansi-terminal, on Windows you get@@ -151,18 +151,22 @@          return (fmap (f bw) ts)     where-          f :: Bool -> TS.Window Int -> (Width, Height)+          f :: Bool -> TS.Window Int -> Dimensions           f wbw (TS.Window h w) =                 let h' | wbw       = h - 1                        | otherwise = h                 in (w, h') --- th tw: terminal width and height -- pn: new plane, po: old plane -- wo, ho: dimensions of the terminal. If they change, reinit double buffering-blitPlaneIO :: Width -> Height -> Maybe Plane -> Plane -> IO ()-blitPlaneIO tw th mpo pn =+blitPlaneIO :: Maybe Plane -> Plane -> IO ()+blitPlaneIO mpo pn = +        -- remember that Nothing will be passed:+        -- - at the beginning of the game (first blit)+        -- - when resolution changes (see gameLoop)+        -- so do not duplicate hasResChanged checks here!+         -- old plane         let             (pw, ph) = planeSize pn@@ -179,7 +183,7 @@             -- (error 87 on Win32 console).          CA.setSGR [CA.Reset] >>-        blitMap po pn' tw th+        blitMap po pn'   -----------------@@ -223,21 +227,14 @@                CA.setCursorPosition 0 0 >>                CA.showCursor --- plane + term w/h-blitMap :: Plane -> Plane -> Width -> Height -> IO ()-blitMap po pn tw th =+-- plane+blitMap :: Plane -> Plane -> IO ()+blitMap po pn =             CM.when (planeSize po /= planeSize pn)                     (error "blitMap: different plane sizes")      >>-            CA.setCursorPosition (fi cr) (fi cc)                  >>+            CA.setCursorPosition 0 0                              >>                 -- setCursorPosition is *zero* based!-            blitToTerminal (cr, cc) (orderedCells po) (orderedCells pn)-    where-          (pw, ph) = planeSize pn--          cr = div (th - ph) 2-          cc = div (tw - pw) 2--          fi = fromIntegral+            blitToTerminal (0, 0) (orderedCells po) (orderedCells pn)  orderedCells :: Plane -> [[Cell]] orderedCells p = LS.chunksOf (fromIntegral w) cells
src/Terminal/Game/Layer/Object/Interface.hs view
@@ -77,17 +77,9 @@ -- | @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 @@ -108,15 +100,15 @@ class Monad m => MonadDisplay m where     setupDisplay :: m ()     clearDisplay :: m ()-    displaySize :: m (Maybe (Width, Height))-    blitPlane :: Width -> Height -> Maybe Plane -> Plane -> m ()+    displaySize :: m (Maybe Dimensions)+    blitPlane :: Maybe Plane -> Plane -> m ()     shutdownDisplay :: m ()  ----------- -- Utils -- ----------- -displaySizeErr :: (MonadDisplay m, MonadException m) => m (Width, Height)+displaySizeErr :: (MonadDisplay m, MonadException m) => m Dimensions displaySizeErr = displaySize >>= \case                    Nothing -> throwExc CannotGetDisplaySize                    Just d -> return d
src/Terminal/Game/Layer/Object/Test.hs view
@@ -80,6 +80,6 @@     clearDisplay = return ()     displaySize = return $ Just (8000, 2400)         -- xxx no display size but check display size-    blitPlane _ _ _ _ = return ()+    blitPlane _ _ = return ()     shutdownDisplay = () <$ S.tell [TShutdownDisplay] 
src/Terminal/Game/Plane.hs view
@@ -27,6 +27,9 @@ type Row    = Int
 type Column = Int
 
+-- | Size of a surface.
+type Dimensions = (Width, Height)
+
 -- | Expressed in 'Column's.
 type Width  = Int
 -- | Expressed in 'Row's.
@@ -133,16 +136,12 @@           err p1 p2 = error ("subPlane: top-left point " ++ show p1 ++
                              " > bottom-right point " ++ show p2 ++ ".")
 
--- xxx crea modulo primitive in cui fai pastrocci ed esponi solo poche
---     funzioni di base da quello [refactor]
-
-
 -------------
 -- INQUIRE --
 -------------
 
 -- | Dimensions or a plane.
-planeSize :: Plane -> (Width, Height)
+planeSize :: Plane -> Dimensions
 planeSize p = T.swap . snd $ A.bounds (fromPlane p)
 
 cellChar :: Cell -> Char
test/Terminal/Game/Layer/ImperativeSpec.hs view
@@ -22,7 +22,7 @@       qf (3, _,    _) = True       qf _            = False       es = [Tick, KeyPress 'c', KeyPress 'c', Tick, Tick]-      g = Game 10 10 nd s lf nd qf+      g = simpleGame (10, 10) nd s lf nd qf    describe "runGame" $ do     it "does not confuse input and logic" $