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 +50/−1
- README +10/−0
- ansi-terminal-game.cabal +16/−1
- example/Alone.hs +11/−13
- example/Balls.hs +185/−0
- example/Playback.hs +1/−1
- src/Terminal/Game.hs +12/−13
- src/Terminal/Game/Layer/Imperative.hs +150/−61
- src/Terminal/Game/Layer/Object.hs +0/−5
- src/Terminal/Game/Layer/Object/IO.hs +19/−22
- src/Terminal/Game/Layer/Object/Interface.hs +3/−11
- src/Terminal/Game/Layer/Object/Test.hs +1/−1
- src/Terminal/Game/Plane.hs +4/−5
- test/Terminal/Game/Layer/ImperativeSpec.hs +1/−1
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" $