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 +12/−0
- ansi-terminal-game.cabal +7/−5
- example/Alone.hs +1/−1
- example/MainHotReload.hs +69/−13
- platform-dep/windows/Terminal/Game/Utils.hs +1/−0
- src/Terminal/Game.hs +8/−1
- src/Terminal/Game/Draw.hs +4/−4
- src/Terminal/Game/Layer/Imperative.hs +8/−1
- src/Terminal/Game/Layer/Object/IO.hs +3/−0
- src/Terminal/Game/Layer/Object/Interface.hs +0/−32
- src/Terminal/Game/Layer/Object/Primitive.hs +40/−3
- src/Terminal/Game/Layer/Object/Test.hs +14/−12
- src/Terminal/Game/Plane.hs +4/−6
- test/Terminal/Game/Layer/ImperativeSpec.hs +9/−0
- test/Terminal/Game/Layer/Object/TestSpec.hs +19/−0
- test/Terminal/Game/PlaneSpec.hs +3/−5
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'