-------------------------------------------------------------------------------
-- |
-- Module : Terminal.Game
-- Copyright : © 2017-2023 Francesco Ariis et al.
-- License : GPLv3 (see COPYING)
--
-- Maintainer : Francesco Ariis <fa-ml@ariis.it>
-- Stability : provisional
-- Portability : portable
--
-- Machinery and utilities for 2D terminal games.
--
-- New? Start from 'Game'.
--
-------------------------------------------------------------------------------
-- 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
-- todo rewrite test w/ an internal library method
module Terminal.Game ( -- * Running
TPS,
FPS,
Event(..),
GEnv(..),
Game(..),
playGame,
ATGException(..),
-- ** Helpers
playGame_,
Terminal.Game.displaySize,
assertTermDims,
errorPress,
blankPlaneFull,
centerFull,
-- * 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
Timed,
creaTimer, creaBoolTimer,
creaTimerLoop, creaBoolTimerLoop,
-- *** Animations
Animation,
creaAnimation,
creaLoopAnimation,
-- *** T/A interface
tick, ticks, reset, lapse,
fetchFrame, isExpired,
-- ** Random numbers
StdGen,
getStdGen, mkStdGen,
getRandom, pickRandom,
UniformRange,
-- * 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
Plane,
Dimensions,
Coords,
Row, Column,
Width, Height,
blankPlane,
stringPlane,
stringPlaneTrans,
makeTransparent,
makeOpaque,
planePaper,
planeSize,
-- ** Draw
Draw,
(%), (&), (#),
subPlane,
mergePlanes,
cell, word, box,
Color(..), ColorIntensity(..),
color, bold, invert,
-- *** Alternative origins
-- $origins
(%^>), (%.<), (%.>),
-- *** Text boxes
textBox, textBoxLiquid,
textBoxHyphen, textBoxHyphenLiquid,
Hyphenator,
-- | Eurocentric convenience reexports. Check
-- "Text.Hyphenation.Language" for more languages.
english_GB, english_US, esperanto,
french, german_1996, italian, spanish,
-- *** Declarative drawing
(|||), (===), (***), hcat, vcat,
-- *** 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.
S.Colour, rgbColor, paletteColor,
S.sRGB24, S.sRGBBounded, S.sRGB, S.sRGB24read,
xterm6LevelRGB, xterm24LevelGray, xtermSystem,
-- * Testing
GRec,
recordGame,
readRecord,
testGame,
setupGame,
narrateGame,
-- | A quick and dirty way to have /hot reload/
-- (autorestarting your game when source files change)
-- is illustrated in @example/MainHotReload.hs@.
-- * Embedding files
-- | Embedding files is convenient when working on
-- assets separately and still wanting to ship a
-- single binary. Remember to add this pragma to
-- the top of your module:
--
-- > {-# LANGUAGE TemplateHaskell #-}
embedFile,
embedDir,
BC.ByteString,
BC.unpack,
-- * Cross platform
-- $xcompat
)
where
import Data.FileEmbed
import System.Console.ANSI
import Terminal.Game.Animation
import Terminal.Game.Draw
import Terminal.Game.Layer.Imperative
import Terminal.Game.Layer.Object as O
import Terminal.Game.Plane
import Terminal.Game.Random
import Text.LineBreak
import qualified Control.Monad as CM
import qualified Data.ByteString.Char8 as BC
import qualified Data.Colour.SRGB as S
-- $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
-- @
-- $xcompat
-- 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](https://gitlab.haskell.org/ghc/ghc/issues/7593) 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.
--
-- * If you use WASD for movement, you can readily gain compatibility with
-- AZERTY keyboard layout by mapping “up” to both @W@ and @Z@ and “left”
-- to @A@ and @Q@. Users from France, Belgium, and Québec will thank you.
-- | /Usable/ terminal display size (on Win32 console the last line is
-- set aside for input). Throws 'CannotGetDisplaySize' on error.
displaySize :: IO Dimensions
displaySize = O.displaySizeErr
-- | Check if terminal can accomodate 'Dimensions', otherwise throws
-- 'DisplayTooSmall' with a helpful message for the player.
assertTermDims :: Width -> Height -> IO ()
assertTermDims dw dh =
clearScreen >>
setCursorPosition 0 0 >>
displaySizeErr >>= \ads ->
CM.when (isSmaller ads)
(throwExc $ DisplayTooSmall (dw, dh) ads)
where
isSmaller :: Dimensions -> Bool
isSmaller (ww, wh) = ww < dw || wh < dh