Copyright | © 2017-2021 Francesco Ariis |
---|---|
License | GPLv3 (see COPYING file) |
Maintainer | Francesco Ariis <fa-ml@ariis.it> |
Stability | provisional |
Portability | portable |
Safe Haskell | Safe-Inferred |
Language | Haskell2010 |
Machinery and utilities for 2D terminal games.
New? Start from Game
.
Synopsis
- type TPS = Integer
- type FPS = Integer
- data Event
- data GEnv = GEnv {
- eTermDims :: Dimensions
- eFPS :: FPS
- data Game s r = Game {
- gTPS :: TPS
- gInitState :: s
- gLogicFunction :: GEnv -> s -> Event -> Either r s
- gDrawFunction :: GEnv -> s -> Plane
- playGame :: Game s r -> IO r
- data ATGException
- playGame_ :: Game s r -> IO ()
- displaySize :: IO Dimensions
- assertTermDims :: Width -> Height -> IO ()
- errorPress :: IO a -> IO a
- blankPlaneFull :: GEnv -> Plane
- centerFull :: GEnv -> Plane -> Plane
- data Timed a
- creaTimer :: a -> a -> Integer -> Timed a
- creaBoolTimer :: Integer -> Timed Bool
- creaTimerLoop :: a -> a -> Integer -> Timed a
- creaBoolTimerLoop :: Integer -> Timed Bool
- type Animation = Timed Plane
- creaAnimation :: [(Integer, Plane)] -> Animation
- creaLoopAnimation :: [(Integer, Plane)] -> Animation
- tick :: Timed a -> Timed a
- ticks :: Integer -> Timed a -> Timed a
- reset :: Timed a -> Timed a
- lapse :: Timed a -> Timed a
- fetchFrame :: Timed a -> a
- isExpired :: Timed a -> Bool
- data StdGen
- getStdGen :: MonadIO m => m StdGen
- mkStdGen :: Int -> StdGen
- getRandom :: UniformRange a => (a, a) -> StdGen -> (a, StdGen)
- pickRandom :: [a] -> StdGen -> (a, StdGen)
- class UniformRange a
- data Plane
- type Dimensions = (Width, Height)
- type Coords = (Row, Column)
- type Row = Int
- type Column = Int
- type Width = Int
- type Height = Int
- blankPlane :: Width -> Height -> Plane
- stringPlane :: String -> Plane
- stringPlaneTrans :: Char -> String -> Plane
- makeTransparent :: Char -> Plane -> Plane
- makeOpaque :: Plane -> Plane
- planePaper :: Plane -> String
- planeSize :: Plane -> Dimensions
- type Draw = Plane -> Plane
- (%) :: Coords -> Plane -> Draw
- (&) :: a -> (a -> b) -> b
- (#) :: Plane -> Draw -> Plane
- subPlane :: Plane -> Coords -> Coords -> Plane
- mergePlanes :: Plane -> [(Coords, Plane)] -> Plane
- cell :: Char -> Plane
- word :: String -> Plane
- box :: Width -> Height -> Char -> Plane
- data Color
- data ColorIntensity
- color :: Color -> ColorIntensity -> Plane -> Plane
- bold :: Plane -> Plane
- invert :: Plane -> Plane
- (%^>) :: Coords -> Plane -> Draw
- (%.<) :: Coords -> Plane -> Draw
- (%.>) :: Coords -> Plane -> Draw
- textBox :: Width -> Height -> String -> Plane
- textBoxLiquid :: Width -> String -> Plane
- textBoxHyphen :: Hyphenator -> Width -> Height -> String -> Plane
- textBoxHyphenLiquid :: Hyphenator -> Width -> String -> Plane
- data Hyphenator
- english_GB :: Hyphenator
- english_US :: Hyphenator
- esperanto :: Hyphenator
- french :: Hyphenator
- german_1996 :: Hyphenator
- italian :: Hyphenator
- spanish :: Hyphenator
- (|||) :: Plane -> Plane -> Plane
- (===) :: Plane -> Plane -> Plane
- (***) :: Plane -> Plane -> Plane
- hcat :: [Plane] -> Plane
- vcat :: [Plane] -> Plane
- data Colour a
- rgbColor :: Colour Float -> Plane -> Plane
- paletteColor :: Word8 -> Plane -> Plane
- sRGB24 :: (Ord b, Floating b) => Word8 -> Word8 -> Word8 -> Colour b
- sRGBBounded :: (Ord b, Floating b, Integral a, Bounded a) => a -> a -> a -> Colour b
- sRGB :: (Ord b, Floating b) => b -> b -> b -> Colour b
- sRGB24read :: (Ord b, Floating b) => String -> Colour b
- xterm6LevelRGB :: Int -> Int -> Int -> Word8
- xterm24LevelGray :: Int -> Word8
- xtermSystem :: ColorIntensity -> Color -> Word8
- data GRec
- recordGame :: Game s r -> FilePath -> IO ()
- readRecord :: FilePath -> IO GRec
- testGame :: Game s r -> GRec -> Either r s
- setupGame :: Game s r -> GRec -> Game s r
- narrateGame :: Game s r -> GRec -> IO ()
Running
The number of Tick
s fed each second to the logic function;
constant on every machine. Frames per second might be lower
(depending on drawing function onerousness, terminal refresh rate,
etc.).
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.
For obvious reasons (blits would be wasted) max FPS = TPS
.
An Event
is a Tick
(time passes) or a KeyPress
.
Note that all Keypress
es are recorded and fed to your game-logic
function. This means you will not lose a single character, no matter
how fast your player is at typing or how low you set FPS
to be.
Example: in a game where you are controlling a hot-air baloon and have
direction
and position
variables, you most likely want direction
to change at every KeyPress
, while having position
only change at
Tick
s.
Instances
Arbitrary Event Source # | |
Generic Event Source # | |
Show Event Source # | |
Serialize Event Source # | |
Eq Event Source # | |
type Rep Event Source # | |
Defined in Terminal.Game.Layer.Object.Primitive type Rep Event = D1 ('MetaData "Event" "Terminal.Game.Layer.Object.Primitive" "ansi-terminal-game-1.9.0.0-7gG02U9AFxZCXjl8izQTU7" 'False) (C1 ('MetaCons "Tick" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "KeyPress" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Char))) |
Game environment with current terminal dimensions and current display rate.
GEnv | |
|
Game definition datatype, parametrised on:
- your gamestate
s
; and - a result when the game is finished
r
. Simple games do not need this, just fillr
with()
.
The two most important elements are the function dealing with logic and
the drawing one. Check alone
demo (cabal run -f examples alone
) to
see a basic game in action.
Game | |
|
playGame :: Game s r -> IO r Source #
Entry point for the game execution, should be called in main
.
You must compile your programs with -threaded
; if you do not do
this the game will crash at start-up. Just add:
ghc-options: -threaded
in your .cabal
file and you will be fine!
data ATGException Source #
ATGException
s are thrown synchronously for easier catching.
CannotGetDisplaySize | |
DisplayTooSmall Dimensions Dimensions | Required and actual dimensions. |
MalformedGRec String |
Instances
Exception ATGException Source # | |
Defined in Terminal.Game.Layer.Object.Primitive | |
Show ATGException Source # | |
Defined in Terminal.Game.Layer.Object.Primitive showsPrec :: Int -> ATGException -> ShowS # show :: ATGException -> String # showList :: [ATGException] -> ShowS # | |
Eq ATGException Source # | |
Defined in Terminal.Game.Layer.Object.Primitive (==) :: ATGException -> ATGException -> Bool # (/=) :: ATGException -> ATGException -> Bool # |
Helpers
displaySize :: IO Dimensions Source #
Usable terminal display size (on Win32 console the last line is
set aside for input). Throws CannotGetDisplaySize
on error.
assertTermDims :: Width -> Height -> IO () Source #
Check if terminal can accomodate Dimensions
, otherwise throws
DisplayTooSmall
with a helpful message for the player.
errorPress :: IO a -> IO a Source #
Wraps an IO
computation so that any ATGException
or error
gets
displayed along with a <press any key to quit>
prompt.
Some terminals shut-down immediately upon program end; adding
errorPress
to playGame
makes it easier to beta-test games on those
terminals.
blankPlaneFull :: GEnv -> Plane Source #
A blank plane as big as the terminal.
centerFull :: GEnv -> Plane -> Plane Source #
Blits plane in the middle of terminal.
draw :: GEnv -> MyState -> Plane draw ev s = centerFull ev $ ⁝
Game logic
Some convenient function dealing with
Timers (Timed
) and Animation
s.
Usage of these is not mandatory: Game
is
parametrised over any state s
, you are free
to implement game logic as you prefer.
Timers/Animations
Timers
A timed resource is a timer which, at any given moment, points to a specific item (like an animation).
Example:
timer = creaTimedRes (Times 1 Elapse) [(2, "a "), (1, "b "), (2, "c ")] test t | isExpired t = putStrLn "Fine." | otherwise = do putStr (fetchFrame t) test (tick t) -- λ> test timer -- a a b c c Fine.
Instances
creaTimer :: a -> a -> Integer -> Timed a #
A simple off/on timer expiring in fixed number of ticks.
Example:
timer = creaTimer Nothing (Just "Over!") 4 test t | isExpired t = print (fetchFrame t) | otherwise = do print (fetchFrame t) test (tick t) -- λ> test timer -- Nothing -- Nothing -- Nothing -- Nothing -- Just "Over"!
creaTimerLoop :: a -> a -> Integer -> Timed a #
A looped version of creaTimer
.
creaBoolTimerLoop :: Integer -> Timed Bool #
Shorthand for:
.creaTimerLoop
False True i
Animations
T/A interface
fetchFrame :: Timed a -> a #
Fetches the current resource of the timer.
isExpired :: Timed a -> Bool #
Checks wheter the timer is expired (an expired timer will not
respond to tick
).
Random numbers
The standard pseudo-random number generator.
Instances
Show StdGen | |
NFData StdGen | |
Defined in System.Random.Internal | |
Eq StdGen | |
RandomGen StdGen | |
Defined in System.Random.Internal next :: StdGen -> (Int, StdGen) # genWord8 :: StdGen -> (Word8, StdGen) # genWord16 :: StdGen -> (Word16, StdGen) # genWord32 :: StdGen -> (Word32, StdGen) # genWord64 :: StdGen -> (Word64, StdGen) # genWord32R :: Word32 -> StdGen -> (Word32, StdGen) # genWord64R :: Word64 -> StdGen -> (Word64, StdGen) # genShortByteString :: Int -> StdGen -> (ShortByteString, StdGen) # |
getStdGen :: MonadIO m => m StdGen #
Gets the global pseudo-random number generator. Extracts the contents of
globalStdGen
Since: random-1.0.0
getRandom :: UniformRange a => (a, a) -> StdGen -> (a, StdGen) Source #
Simple, pure pseudo-random generator.
pickRandom :: [a] -> StdGen -> (a, StdGen) Source #
Picks at random from list.
class UniformRange a #
The class of types for which a uniformly distributed value can be drawn from a range.
Since: random-1.2.0
Instances
Drawing
To get to the gist of drawing, check the
documentation for %
.
Blitting on screen is double-buffered and diff'd (at each frame, only cells with changed character will be redrawn).
Plane
A two-dimensional surface (Row, Column) where to blit stuff.
stringPlane :: String -> Plane Source #
stringPlaneTrans :: Char -> String -> Plane Source #
Same as stringPlane
, but with transparent Char
.
Returns a 1×1 transparent plane on empty string.
makeTransparent :: Char -> Plane -> Plane Source #
Adds transparency to a plane, matching a given character
makeOpaque :: Plane -> Plane Source #
Changes every transparent cell in the Plane
to an opaque ' '
character.
planePaper :: Plane -> String Source #
A String (n
divided and ended) representing the Plane
. Useful
for debugging/testing purposes.
planeSize :: Plane -> Dimensions Source #
Dimensions or a plane.
Draw
subPlane :: Plane -> Coords -> Coords -> Plane Source #
Cut out a plane by top-left and bottom-right coordinates.
Returns a 1×1 transparent plane when r1>r2
or c1>c2
.
word :: String -> Plane Source #
1xn
Plane
with a word in it. If you need to import multiline
ASCII art, check stringPlane
and stringPlaneTrans
.
ANSI's eight standard colors. They come in two intensities, which are
controlled by ColorIntensity
. Many terminals allow the colors of the
standard palette to be customised, so that, for example,
setSGR [ SetColor Foreground Vivid Green ]
may not result in bright green
characters.
data ColorIntensity #
ANSI's standard colors come in two intensities
Instances
Alternative origins
Placing a plane is sometimes more convenient if the coordinates origin
is a corner other than top-left (e.g. “Paste this plane one row from
bottom-left corner”). These combinators — meant to be used instead of %
— allow you to do so. Example:
prova :: Plane prova = let rect = box 6 3 '.' letters = word "ab" in rect & (1, 1) %.> letters -- start from bottom-right -- λ> putStr (planePaper prova) -- ...... -- ...... -- ....ab
(%.<) :: Coords -> Plane -> Draw infixl 4 Source #
Pastes a plane onto another (origin: bottom-left).
(%.>) :: Coords -> Plane -> Draw infixl 4 Source #
Pastes a plane onto another (origin: bottom-right).
Text boxes
textBoxHyphen :: Hyphenator -> Width -> Height -> String -> Plane Source #
As textBox
, but hypenated. Example:
(normal textbox) (hyphenated textbox) Rimasi un po’ a meditare nel buio Rimasi un po’ a meditare nel buio velato appena dal barlume azzurrino velato appena dal barlume azzurrino del fornello a gas, su cui del fornello a gas, su cui sobbol- sobbolliva quieta la pentola. liva quieta la pentola.
Notice how in the right box sobbolliva is broken in two. This can be useful and aesthetically pleasing when textboxes are narrow.
textBoxHyphenLiquid :: Hyphenator -> Width -> String -> Plane Source #
As textBoxLiquid
, but hypenated.
data Hyphenator #
A Hyphenator
is combination of an alphabet normalization scheme, a set of Patterns
, a set of Exceptions
to those patterns
and a number of characters at each end to skip hyphenating.
Eurocentric convenience reexports. Check Text.Hyphenation.Language for more languages.
>>>
hyphenate english_GB "supercalifragilisticexpialadocious"
["su","per","cal","i","fra","gil","istic","ex","pi","alado","cious"]
favors UK hyphenation
>>>
hyphenate english_US "supercalifragilisticexpialadocious"
["su","per","cal","ifrag","ilis","tic","ex","pi","al","ado","cious"]
favors US hyphenation
esperanto :: Hyphenator #
Hyphenators for a wide array of languages.
french :: Hyphenator #
>>>
hyphenate french "anticonstitutionnellement"
["an","ti","cons","ti","tu","tion","nel","le","ment"]
Hyphenators for a wide array of languages.
italian :: Hyphenator #
Hyphenators for a wide array of languages.
spanish :: Hyphenator #
Hyphenators for a wide array of languages.
Declarative drawing
hcat :: [Plane] -> Plane Source #
Place a list of Plane
s side-by-side, horizontally. Returns a 1×1
transparent plane on empty list.
vcat :: [Plane] -> Plane Source #
Place a list of Plane
s side-by-side, vertically. Returns a 1×1
transparent plane on empty list.
Non-standard colors
Non-standard RGB and xterm colors. These are prettier, but work on a minority of terminal emulators/multiplexers. Use them only on your machine or when you are sure of the terminal you are targetting.
This type represents the human preception of colour.
The a
parameter is a numeric type used internally for the
representation.
The Monoid
instance allows one to add colours, but beware that adding
colours can take you out of gamut. Consider using blend
whenever
possible.
sRGB24 :: (Ord b, Floating b) => Word8 -> Word8 -> Word8 -> Colour b #
Construct a colour from a 24-bit (three 8-bit words) sRGB specification.
sRGBBounded :: (Ord b, Floating b, Integral a, Bounded a) => a -> a -> a -> Colour b #
Construct a colour from an sRGB specification.
Input components are expected to be in the range [0..maxBound
].
sRGB :: (Ord b, Floating b) => b -> b -> b -> Colour b #
Construct a colour from an sRGB specification. Input components are expected to be in the range [0..1].
sRGB24read :: (Ord b, Floating b) => String -> Colour b #
Read a colour in hexadecimal form, e.g. "#00aaff" or "00aaff"
xterm6LevelRGB :: Int -> Int -> Int -> Word8 #
Given xterm's standard protocol for a 256-color palette, returns the index to that part of the palette which is a 6 level (6x6x6) color cube of 216 RGB colors. Throws an error if any of the red, green or blue channels is outside the range 0 to 5. An example of use is:
>>>
setSGR [ SetPaletteColor $ xterm6LevelRGB 5 2 0 ] -- Dark Orange
Since: ansi-terminal-0.9
xterm24LevelGray :: Int -> Word8 #
Given xterm's standard protocol for a 256-color palette, returns the index to that part of the palette which is a spectrum of 24 grays, from dark gray (0) to near white (23) (black and white are themselves excluded). Throws an error if the gray is outside of the range 0 to 23. An example of use is:
>>>
setSGR [ SetPaletteColor $ xterm24LevelGray 12 ] -- Gray50
Since: ansi-terminal-0.9
xtermSystem :: ColorIntensity -> Color -> Word8 #
Given xterm's standard protocol for a 256-color palette, returns the index to that part of the palette which corresponds to the 'ANSI' standards' 16 standard, or 'system', colors (eight colors in two intensities). An example of use is:
>>>
setSGR [ SetPaletteColor $ xtermSystem Vivid Green ]
Since: ansi-terminal-0.9
Testing
Opaque data type with recorded game input, for testing purposes.
Instances
Generic GRec Source # | |
Show GRec Source # | |
Serialize GRec Source # | |
Eq GRec Source # | |
type Rep GRec Source # | |
Defined in Terminal.Game.Layer.Object.Primitive type Rep GRec = D1 ('MetaData "GRec" "Terminal.Game.Layer.Object.Primitive" "ansi-terminal-game-1.9.0.0-7gG02U9AFxZCXjl8izQTU7" 'False) (C1 ('MetaCons "GRec" 'PrefixI 'True) (S1 ('MetaSel ('Just "aPolled") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (Seq [Event])) :*: S1 ('MetaSel ('Just "aTermSize") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (Seq (Maybe Dimensions))))) |
recordGame :: Game s r -> FilePath -> IO () Source #
Play as in playGame
and write the session (input stream, etc.) to
file
. Then you can use this with testGame
and narrateGame
. Session
will be recorded even if an exception happens while playing.
readRecord :: FilePath -> IO GRec Source #
Reads a file containing a recorded session. Throws
MalformedGRec
on failure.
testGame :: Game s r -> GRec -> Either r s Source #
Tests a game in a pure environment. Aims to accurately emulate GEnv
changes (screen size, FPS) too. Returns a result r
or a state s
in
case the Event stream is exhausted before the game exits.
A useful trick is to call recordGame
and press Ctrl-C while playing
(instead of quitting properly). This way testGame
will return
Left s
, a state that you can then inspect.
narrateGame :: Game s r -> GRec -> IO () Source #
Similar to testGame
, runs the game given a GRec
. Unlike
testGame
, the playthrough will be displayed on screen. Useful when a
test fails and you want to see how.
See this in action with cabal run -f examples alone-playback
.
Notice that GEnv
will be provided at run-time, and not
record-time; this can make emulation slightly inaccurate if — e.g. —
you replay the game on a smaller terminal than the one you recorded
the session on.
A quick and dirty way to have hot reload
(autorestarting your game when source files change)
is illustrated in example/MainHotReload.hs
.
Cross platform
Good practices for cross-compatibility:
- choose game dimensions of no more than 24 rows and 80 columns. This ensures compatibility with the trickiest terminals (i.e. Win32 console);
- use ASCII characters only. Again this is for Win32 console compatibility, until this GHC bug gets fixed;
- employ colour sparingly: as some users will play your game in a light-background terminal and some in a dark one, choose only colours that go well with either (blue, red, etc.);
- some terminals/multiplexers (i.e. tmux) do not make a distinction between vivid/dull, others do not display bold; do not base your game mechanics on that difference.