packages feed

ansi-terminal-game 1.8.0.0 → 1.8.0.1

raw patch · 16 files changed

+202/−83 lines, 16 filesdep ~mtlPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependency ranges changed: mtl

API changes (from Hackage documentation)

+ Terminal.Game: MalformedGRec :: String -> ATGException

Files

NEWS view
@@ -1,3 +1,15 @@+Next+----++- Fixed hcat, vcat, stringPlane, stringPlane documentation to match+  behaviour (i.e. they do not error on empty strong/list).+- Now `subPlane` too does not throw an exception when called with+  inconsistent coordinates, but returns a transparent 1×1 plane.+- Introduced `MalformedGRec` exception for when `readRecord` fails.+- Provided instructions for hot-reloading a game running in normal+  (interactive) mode with various means (tmux, urxvt, etc.).+  Check `example/MainHotReload.hs`.+ 1.8.0.0 ------- 
ansi-terminal-game.cabal view
@@ -1,5 +1,5 @@ name:                ansi-terminal-game-version:             1.8.0.0+version:             1.8.0.1 synopsis:            sdl-like functions for terminal applications, based on                      ansi-terminal description:         Library which aims to replicate standard 2d game@@ -30,8 +30,8 @@   default:           False  source-repository head-    type:     darcs-    location: http://www.ariis.it/link/repos/ansi-terminal-game/+    type:     git+    location: https://gitlab.com/fffaaa/ansi-terminal-game  library   exposed-modules:     Terminal.Game@@ -61,7 +61,7 @@                        exceptions == 0.10.*,                        linebreak == 1.1.*,                        mintty == 0.1.*,-                       mtl == 2.2.*,+                       mtl >=2.2 && <2.4,                        QuickCheck >= 2.13 && < 2.15,                        random >= 1.2 && < 1.3,                        split == 0.2.*,@@ -98,6 +98,7 @@                        Terminal.Game.Layer.Object.Primitive,                        Terminal.Game.Layer.Object.Record,                        Terminal.Game.Layer.Object.Test,+                       Terminal.Game.Layer.Object.TestSpec,                        Terminal.Game.Utils,                        Terminal.Game.Plane,                        Terminal.Game.PlaneSpec@@ -113,7 +114,7 @@                        exceptions == 0.10.*,                        linebreak == 1.1.*,                        mintty == 0.1.*,-                       mtl == 2.2.*,+                       mtl >=2.2 && <2.4,                        QuickCheck >= 2.13 && < 2.15,                        random >= 1.2 && < 1.3,                        split == 0.2.*,@@ -122,6 +123,7 @@                        timers-tick > 0.5 && < 0.6                        -- the above plus hspec                        , hspec+  build-tool-depends:  hspec-discover:hspec-discover   type:                exitcode-stdio-1.0   ghc-options:         -Wall 
example/Alone.hs view
@@ -77,7 +77,7 @@ drawFun :: MyState -> Plane drawFun (MyState (r, c) _ _) =                            blankPlane mw     mh            &-                (1, 1)   % box mw mh '-'                   &+                (1, 1)   % box mw mh '.'                   &                 (2, 2)   % box (mw-2) (mh-2) ' '           &                 (15, 20) % textBox 10 4                                    "Tap WASD to move, tap again to stop." &
example/MainHotReload.hs view
@@ -5,20 +5,76 @@ import Terminal.Game  -- Hot reloading is a handy feature while writing a game. Here I will--- show you how to do that with ansi-terminal-game.------ 1. install `entr` from your repositories;--- 2. run `find example/*.hs | entr -cr cabal run -f examples hot-reload`;--- 3. now modify example/Alone.hs and see your changes live!------ Caveat: entr and similar applications do *not* work with interactive--- programs, so you need — as shown below — to load a record and play--- it as a demo.--- This is still useful to iteratively build NPCs’ behaviour, GUIs, etc.+-- show you various ways to do that with ansi-terminal-game. ----- Remember that you can use `recordGame` to record a session. If you--- need something fancier for your game (e.g. hot-reload with input),--- `venzone` [1] (module Watcher) has a builtin /watch mode/ you can+-- Hot reloading makes use of `entr` (install it from your repos) and+-- some additional scaffolding, provided by `tmux` or your plain terminal.+-- Read below to see two ideas in action.+++{- === HOT RELOAD WITH ENTR AND TMUX ===++	1. Install `entr` and `tmux`.++    2. open a tmux window, in the bottom-right plane launch the game+       in an infinite loop, e.g.++         while true; do cabal run -f examples alone; done++       Remember, the pane has to be the bottom-right one, like this:++            +----------------------------------------------++            |                        |                     |+            |                        |                     |+            |                        |                     |+            |                        |                     |+            |                        |                     |+            |                        |                     |+            |                        |---------------------|+            |                        |                     |+            |                        |                     |+            |                        |                     |+            |                        |       G A M E       |+            |                        |                     |+            |                        |                     |+            |                        |                     |+            +----------------------------------------------+++     3. in another pane launch `entr` in this fashion:++          find src example/ -name *.hs | entr tmux send-keys -t {bottom-right} q++     4. Now whenever you modify a source file in example/ , the game+        will be reloaded!  -}+++{- === HOT RELOAD WITH ENTR AND PLAIN TERMINAL ===++    1. If your terminal has a `command` option, it is even easier.+       We will use `urxvt` for this example++          find src example/ -name *.hs | entr -r urxvt -e cabal run -f examples alone++    2. Every time you save a source file, a terminal will be spawn with+       your game running in it.  -}+++{-  === HOT RELOAD REPLAYS WITH ENTR ===++    `entr` by itself *cannot* autoreload a game in the same window, as it+    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++    This is very useful to incrementally build NPCs’ behaviour,+    iron out mechanics bugs etc.++    Remember that you can use `recordGame` to record a session.  -}+++-- If you you need something fancier for your game (e.g. hot reloading user+-- maps), `venzone` [1] (module Watcher) has a builtin /watch mode/ you can -- take inspiration from. -- -- [1] https://hackage.haskell.org/package/venzone
platform-dep/windows/Terminal/Game/Utils.hs view
@@ -24,6 +24,7 @@ foreign import ccall safe "conio.h getch"   c_getch :: IO FT.CInt +-- TODO elimina isWin32Console [win] -- not perfect, but it is what it is (on win, non minTTY) isWin32Console :: IO Bool isWin32Console = not <$> M.isMinTTY
src/Terminal/Game.hs view
@@ -17,6 +17,12 @@ -- Basic col-on-black ASCII terminal, operations. -- Only module to be imported. +-- todo you can eliminate unidecode!+-- todo you probably can eliminate part of platform dep too (ffi)+--      (anche check what is linked -- see lib.so.8 error)+-- todo you should check again on win the «press exit to continue» test+-- todo write that ascii/latin-1/greek letters should be ok on win+ module Terminal.Game ( -- * Running                        TPS,                        FPS,@@ -175,7 +181,8 @@ --   that go well with either (blue, red, etc.); -- -- * some terminals/multiplexers (i.e. tmux) do not make a distinction---   between vivid/dull; do not base your game mechanics on that+--   between vivid/dull, others do not display bold; do not base your game+--   mechanics on that --   difference.  -- | /Usable/ terminal display size (on Win32 console the last line is
src/Terminal/Game/Draw.hs view
@@ -97,14 +97,14 @@                 (r, c) % b  --- | Place a list of 'Plane's side-by-side, horizontally. @error@s on--- empty list.+-- | Place a list of 'Plane's side-by-side, horizontally. Returns a 1×1+-- transparent plane on empty list. hcat :: [Plane] -> Plane hcat [] = blankPlane 1 1 # makeTransparent ' ' hcat ps = L.foldl1' (|||) ps --- | Place a list of 'Plane's side-by-side, vertically. @error@s on--- empty list.+-- | Place a list of 'Plane's side-by-side, vertically. Returns a 1×1+-- transparent plane on empty list. vcat :: [Plane] -> Plane vcat [] = blankPlane 1 1 # makeTransparent ' ' vcat ps = L.foldl1' (===) ps
src/Terminal/Game/Layer/Imperative.hs view
@@ -76,7 +76,14 @@ -- | Tests a game in a /pure/ environment. Aims to accurately emulate 'GEnv' -- changes (screen size, FPS) too. testGame :: Game s -> GRec -> s-testGame g ts = fst $ runTest (runGameGeneral g) ts+testGame g ts =+        case runTest (runGameGeneral g) ts of+            (Nothing, l) -> error $ "testGame, exception called: " +++                                    show l+                -- it is fine to use error here since in the end+                -- hspec can deal with it gracefully and we give+                -- more infos on a failed test+            (Just s, _) -> s  -- | As 'testGame', but returns 'Game' instead of a bare state. -- Useful to fast-forward (e.g.: skip menus) before invoking 'playGame'.
src/Terminal/Game/Layer/Object/IO.hs view
@@ -145,6 +145,9 @@             -- inefficient as it gets (attempts to scroll past             -- bottom right)         isWin32Console >>= \bw ->+            -- cmd.exe is present on Win10 `C:\Windows\system32\cmd.exe`+            -- — and default — too. So this is needed for the foreseeable+            -- future.          return (fmap (f bw) ts)     where
src/Terminal/Game/Layer/Object/Interface.hs view
@@ -13,7 +13,6 @@ import Terminal.Game.Layer.Object.Primitive  import qualified Control.Concurrent as CC-import qualified Control.Monad.Catch as MC  ------------------------------------------------------------------------------- -- mtl interface for game@@ -56,34 +55,3 @@ displaySizeErr = displaySize >>= \case                    Nothing -> throwExc CannotGetDisplaySize                    Just d -> return d------------------------------------------------------------------------------------ Error handling---- | @ATGException@s are thrown synchronously for easier catching.-data ATGException = CannotGetDisplaySize-                  | DisplayTooSmall Dimensions Dimensions-                        -- ^ Required and actual dimensions.--instance Show ATGException where-    show CannotGetDisplaySize = "CannotGetDisplaySize"-    show (DisplayTooSmall (sw, sh) tds) =-      let colS ww = ww < sw-          rowS wh = wh < sh--          smallMsg :: Dimensions -> String-          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-                  "This games requires a display of " ++ show sw ++-                  " columns and " ++ show sh ++ " rows.\n" ++-                  "Yours only has " ++ em ++ "!\n\n" ++-                  "Please resize your terminal and restart the game.\n"-      in "DisplayTooSmall.\n" ++ smallMsg tds--instance MC.Exception ATGException where
src/Terminal/Game/Layer/Object/Primitive.hs view
@@ -5,6 +5,7 @@  import Terminal.Game.Plane +import qualified Control.Monad.Catch as MC import qualified GHC.Generics as G import qualified Data.ByteString as BS import qualified Data.Serialize as Z@@ -29,6 +30,9 @@ -- | An @Event@ is a 'Tick' (time passes) or a 'KeyPress'. data Event = Tick            | KeyPress Char+              -- ↑↓→← do not work on Windows (are handled by the app,+              -- not passed to the program) both on cmd.exe and+              -- PowerShell.            deriving (Show, Eq, G.Generic) instance Z.Serialize Event where @@ -78,11 +82,11 @@ isOver (GRec S.Empty _) = True isOver _ = False --- | Reads a file containing a recorded session.+-- | Reads a file containing a recorded session. Throws+-- 'MalformedGRec' on failure. readRecord :: FilePath -> IO GRec readRecord fp = Z.decode <$> BS.readFile fp >>= \case-                  Left e  -> error $ "readRecord could not decode: " ++-                                     show e+                  Left e  -> MC.throwM (MalformedGRec e)                   Right r -> return r  -- | Convenience function to create a 'GRec' from screen size (constant) plus a list of events. Useful with 'setupGame'.@@ -91,3 +95,36 @@                    GRec (S.fromList [es])                         (S.fromList . replicate l $ Just ds) +-------------------------------------------------------------------------------+-- Exceptions++-- | @ATGException@s are thrown synchronously for easier catching.+data ATGException = CannotGetDisplaySize+                  | DisplayTooSmall Dimensions Dimensions+                        -- ^ Required and actual dimensions.+                  | MalformedGRec String+        deriving (Eq)++instance Show ATGException where+    show CannotGetDisplaySize = "CannotGetDisplaySize"+    show (DisplayTooSmall (sw, sh) tds) =+      let colS ww = ww < sw+          rowS wh = wh < sh++          smallMsg :: Dimensions -> String+          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+                  "This games requires a display of " ++ show sw +++                  " columns and " ++ show sh ++ " rows.\n" +++                  "Yours only has " ++ em ++ "!\n\n" +++                  "Please resize your terminal and restart the game.\n"+      in "DisplayTooSmall.\n" ++ smallMsg tds+    show (MalformedGRec e) = "MalformedGRec: " ++ e++instance MC.Exception ATGException where
src/Terminal/Game/Layer/Object/Test.hs view
@@ -13,7 +13,9 @@ import Terminal.Game.Layer.Object.Interface import Terminal.Game.Layer.Object.Primitive +import qualified Control.Monad.Except as E import qualified Control.Monad.RWS as S+import qualified Data.Bifunctor as B   -----------@@ -21,6 +23,7 @@ -----------  data TestEvent = TCleanUpError+               | TException ATGException                | TQuitGame                | TSetupDisplay                | TShutdownDisplay@@ -29,24 +32,22 @@                | TStopEvents         deriving (Eq, Show) +-- e: () -- r: () -- w: [TestEvents] -- s: [GTest]-newtype Test a = Test (S.RWS () [TestEvent] GRec a)+newtype Test a = Test (E.ExceptT () (S.RWS () [TestEvent] GRec) a)                deriving (Functor, Applicative, Monad,-                         S.MonadState GRec,+                         E.MonadError (), S.MonadState GRec,                          S.MonadWriter [TestEvent]) -runTest :: Test a -> GRec -> (a, [TestEvent])-runTest (Test m) es = S.evalRWS m () es----------------- CLASS --------------+runTest :: Test a -> GRec -> (Maybe a, [TestEvent])+runTest (Test em) es = let m = E.runExceptT em+                           t = S.evalRWS m () es in+                       B.first (either (const Nothing) Just) t -tconst :: a -> Test a-tconst a = Test $ return a+-------------------------------------------------------------------------------+-- Class  mockHandle :: InputHandle mockHandle = InputHandle (error "mock handle keyMvar")@@ -64,7 +65,8 @@  instance MonadException Test where     cleanUpErr a _ = S.tell [TCleanUpError] >> a-    throwExc e = error . show $ e+    throwExc e = S.tell [TException e] >>+                 E.throwError ()  instance MonadLogic Test where     checkQuit _ _ = S.gets isOver
src/Terminal/Game/Plane.hs view
@@ -100,12 +100,12 @@ reverseCell Transparent        = Transparent
 
 -- | Creates 'Plane' from 'String', good way to import ASCII
--- art/diagrams. @error@s on empty string.
+-- art/diagrams. Returns a 1×1 transparent plane on empty string.
 stringPlane :: String -> Plane
 stringPlane t = stringPlaneGeneric Nothing t
 
 -- | Same as 'stringPlane', but with transparent 'Char'.
--- @error@s on empty string.
+-- Returns a 1×1 transparent plane on empty string.
 stringPlaneTrans :: Char -> String -> Plane
 stringPlaneTrans c t = stringPlaneGeneric (Just c) t
 
@@ -158,9 +158,10 @@           (w2, h2)  = planeSize p2
 
 -- | Cut out a plane by top-left and bottom-right coordinates.
+-- Returns a 1×1 transparent plane when @r1>r2@ or @c1>c2@.
 subPlane :: Plane -> Coords -> Coords -> Plane
 subPlane p (r1, c1) (r2, c2)
-        | r1 > r2 || c1 > c2 = err (r1, c1) (r2, c2)
+        | r1 > r2 || c1 > c2 = makeTransparent ' ' (blankPlane 1 1)
         | otherwise          =
             let cs       = assocsPlane p
                 fs       = filter f cs
@@ -170,9 +171,6 @@     where
           f ((rw, cw), _) = rw >= r1 && rw <= r2 &&
                             cw >= c1 && cw <= c2
-
-          err p1 p2 = error ("subPlane: top-left point " ++ show p1 ++
-                             " > bottom-right point " ++ show p2 ++ ".")
 
 -------------
 -- INQUIRE --
test/Terminal/Game/Layer/ImperativeSpec.hs view
@@ -9,7 +9,9 @@ import Test.Hspec import Test.Hspec.QuickCheck +import qualified Control.Exception as E import qualified Test.QuickCheck as Q+import qualified GHC.Exts as X  spec :: Spec spec = do@@ -52,3 +54,10 @@              \es -> let w = createGRec (80, 24) es                         a = testGame aloneInARoom w                     in a == a+    it "fails with an informative message" $ do+        r <- readRecord "test/records/alone-record-test.gr"+        let r' = r { aTermSize = X.fromList (replicate 1000 Nothing) }+            t = testGame aloneInARoom r'+            e = "testGame, exception called: [TSetupDisplay,TStartEvents,\+                \TException CannotGetDisplaySize]"+        E.evaluate t `shouldThrow` errorCall e
+ test/Terminal/Game/Layer/Object/TestSpec.hs view
@@ -0,0 +1,19 @@+module Terminal.Game.Layer.Object.TestSpec where++import Terminal.Game.Layer.Imperative+import Terminal.Game.Layer.Object+import Alone++import Test.Hspec++import qualified GHC.Exts as E++spec :: Spec+spec = do++  describe "runTest" $ do+    it "logs exceptions without failing" $ do+        r <- readRecord "test/records/alone-record-test.gr"+        let r' = r { aTermSize = E.fromList (replicate 1000 Nothing) }+            t = runTest (runGameGeneral aloneInARoom) r'+        last (snd t) `shouldBe` TException CannotGetDisplaySize
test/Terminal/Game/PlaneSpec.hs view
@@ -4,9 +4,7 @@ import Terminal.Game.Plane import Terminal.Game.Draw -import qualified Control.Exception as E - spec :: Spec spec = do @@ -44,9 +42,9 @@       planePaper (subPlane pa (1, 1) (2, 1)) `shouldBe` "p\nf\n"     it "does not crash on OOB" $       planeSize (subPlane pa (1, 1) (10, 10)) `shouldBe` (5, 2)-    it "errs on emptycell" $-      E.evaluate (subPlane pa (2, 3) (1, 1)) `shouldThrow`-        errorCall "subPlane: top-left point (2,3) > bottom-right point (1,1)."+    it "does not err on inconsistent coords" $+      subPlane pa (2, 3) (1, 1) `shouldBe`+        (blankPlane 1 1 # makeTransparent ' ')     it "but not on a single cell" $       subPlane pa (2, 3) (2, 3) `shouldBe` cell 'l'