tetris-cli/src/UI/Game.hs
sherlock 725b7bdf94
Some checks are pending
build / Build (push) Waiting to run
move to forgejo
2025-03-13 12:21:05 +05:30

344 lines
10 KiB
Haskell

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module UI.Game
( playGame
) where
import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.STM (TVar, newTVarIO, readTVarIO, writeTVar, atomically)
import Control.Monad (void, forever)
import Prelude hiding (Left, Right)
import Brick hiding (Down)
import Brick.BChan
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.Border.Style as BS
import qualified Brick.Widgets.Center as C
import Control.Lens hiding (preview, op, zoom)
import Control.Monad.Extra (orM, unlessM)
import Control.Monad.IO.Class (liftIO)
import qualified Graphics.Vty as V
import qualified Graphics.Vty.CrossPlatform
import qualified Graphics.Vty.Config
import Data.Map (Map)
import qualified Data.Map as M
import Linear.V2 (V2(..))
import Tetris
data UI = UI
{ _game :: Game
, _initLevel :: Int
, _currLevel :: TVar Int
, _preview :: Maybe String
, _locked :: Bool
, _paused :: Bool
}
makeLenses ''UI
-- | Ticks mark passing of time
data Tick = Tick
-- | Named resources
type Name = ()
data VisualBlock
= NormalBlock
| HardDropBlock String
-- App definition and execution
app :: App UI Tick Name
app = App
{ appDraw = drawUI
, appChooseCursor = neverShowCursor
, appHandleEvent = handleEvent
, appStartEvent = pure ()
, appAttrMap = const theMap
}
playGame :: Int -- ^ Starting level
-> Maybe String -- ^ Preview cell (Nothing == no preview)
-> Bool -- ^ Enable level progression
-> IO Game
playGame lvl mp prog = do
chan <- newBChan 10
tv <- newTVarIO lvl
void . forkIO $ forever $ do
writeBChan chan Tick
lvl <- readTVarIO tv
threadDelay $ levelToDelay lvl
initialGame <- initGame lvl prog -- Pass the progression parameter
let buildVty = Graphics.Vty.CrossPlatform.mkVty Graphics.Vty.Config.defaultConfig
initialVty <- buildVty
ui <- customMain initialVty buildVty (Just chan) app $ UI
{ _game = initialGame
, _initLevel = lvl
, _currLevel = tv
, _preview = mp
, _locked = False
, _paused = False
}
return $ ui ^. game
levelToDelay :: Int -> Int
levelToDelay n = floor $ 400000 * (0.85 :: Double) ^ (2 * n)
-- Handling events
handleEvent :: BrickEvent Name Tick -> EventM Name UI ()
handleEvent (VtyEvent (V.EvKey (V.KChar 'r') [])) = restart
handleEvent (VtyEvent (V.EvKey (V.KChar 'q') [])) = halt
handleEvent (VtyEvent (V.EvKey V.KEsc [])) = halt
handleEvent (VtyEvent (V.EvKey V.KRight [])) = exec (shift Right)
handleEvent (VtyEvent (V.EvKey V.KLeft [])) = exec (shift Left)
handleEvent (VtyEvent (V.EvKey V.KDown [])) = exec (shift Down)
handleEvent (VtyEvent (V.EvKey (V.KChar 'l') [])) = exec (shift Right)
handleEvent (VtyEvent (V.EvKey (V.KChar 'h') [])) = exec (shift Left)
handleEvent (VtyEvent (V.EvKey (V.KChar 'j') [])) = exec (shift Down)
handleEvent (VtyEvent (V.EvKey V.KUp [])) = exec rotate
handleEvent (VtyEvent (V.EvKey (V.KChar 'k') [])) = exec rotate
handleEvent (VtyEvent (V.EvKey (V.KChar ' ') [])) =
unlessM (orM [use paused, use (game . to isGameOver)]) $ do
zoom game hardDrop
assign locked True
handleEvent (VtyEvent (V.EvKey (V.KChar 'p') [])) =
unlessM (orM [use locked, use (game . to isGameOver)]) $ do
modifying paused not
handleEvent (AppEvent Tick ) =
unlessM (orM [use paused, use (game . to isGameOver)]) $ do
zoom game timeStep
-- Keep level in sync with ticker (gross)
lvl <- use $ game . level
tv <- use $ currLevel
liftIO . atomically $ writeTVar tv lvl
assign locked False
handleEvent _ = pure ()
-- | This common execution function is used for all game user input except hard
-- drop and pause. If paused or locked (from hard drop) do nothing, else
-- execute the state computation.
exec :: Tetris () -> EventM Name UI ()
exec = unlessM (orM [use paused, use locked, use (game . to isGameOver)]) . zoom game
-- | Restart game at the initially chosen level
restart :: EventM Name UI ()
restart = do
lvl <- use initLevel
prog <- use (game . progression) -- Get current progression setting
g <- liftIO $ initGame lvl prog -- Use it when restarting
assign game g
assign locked False
-- Drawing
drawUI :: UI -> [Widget Name]
drawUI ui =
[ C.vCenter $ vLimit 22 $ hBox
[ padLeft Max $ padRight (Pad 2) $ drawStats (ui ^. game)
, drawGrid ui
, padRight Max $ padLeft (Pad 2) $ drawInfo (ui ^. game)
]
]
drawGrid :: UI -> Widget Name
drawGrid ui =
hLimit 22
$ withBorderStyle BS.unicodeBold
$ B.borderWithLabel (str "Tetris")
$ case ui ^. paused of
True -> C.center $ str "Paused"
False -> vBox $ [boardHeight, boardHeight - 1 .. 1] <&> \r ->
foldr (<+>) emptyWidget
. M.filterWithKey (\(V2 _ y) _ -> r == y)
$ mconcat
[ drawBlockCell NormalBlock <$> ui ^. (game . board)
, blockMap NormalBlock (ui ^. (game . block))
, case ui ^. preview of
Nothing -> M.empty
Just s -> blockMap (HardDropBlock s) (evalTetris hardDroppedBlock (ui ^. game))
, emptyCellMap
]
where
blockMap v b =
M.fromList $ [ (c, drawBlockCell v (b ^. shape)) | c <- coords b ]
emptyCellMap :: Map Coord (Widget Name)
emptyCellMap = M.fromList
[ (V2 x y, emptyGridCellW) | x <- [1 .. boardWidth], y <- [1 .. boardHeight] ]
emptyGridCellW :: Widget Name
emptyGridCellW = withAttr emptyAttr cw
emptyNextShapeCellW :: Widget Name
emptyNextShapeCellW = withAttr emptyAttr ecw
drawBlockCell :: VisualBlock -> Tetrimino -> Widget Name
drawBlockCell NormalBlock t = withAttr (tToAttr t) cw
drawBlockCell (HardDropBlock s) t = withAttr (tToAttrH t) (str s)
tToAttr :: Tetrimino -> AttrName
tToAttr I = iAttr
tToAttr O = oAttr
tToAttr T = tAttr
tToAttr S = sAttr
tToAttr Z = zAttr
tToAttr J = jAttr
tToAttr L = lAttr
tToAttrH :: Tetrimino -> AttrName
tToAttrH I = ihAttr
tToAttrH O = ohAttr
tToAttrH T = thAttr
tToAttrH S = shAttr
tToAttrH Z = zhAttr
tToAttrH J = jhAttr
tToAttrH L = lhAttr
cw :: Widget Name
cw = str " ."
ecw :: Widget Name
ecw = str " "
drawStats :: Game -> Widget Name
drawStats g =
hLimit 22
$ withBorderStyle BS.unicodeBold
$ B.borderWithLabel (str "Stats")
$ vBox
[ drawStat "Score" $ g ^. score
, padTop (Pad 1) $ drawStat "Lines" $ g ^. linesCleared
, padTop (Pad 1) $ drawStat "Level" $ g ^. level
, padTop (Pad 1) $ drawProgression (g ^. progression)
, drawLeaderBoard g
]
drawProgression :: Bool -> Widget Name
drawProgression True =
padLeftRight 1 $ str "Level Mode: " <+>
withAttr progressionAttr (padLeft Max $ str "ON")
drawProgression False =
padLeftRight 1 $ str "Level Mode: " <+>
withAttr fixedAttr (padLeft Max $ str "Fixed")
drawStat :: String -> Int -> Widget Name
drawStat s n = padLeftRight 1 $ str s <+> padLeft Max (str $ show n)
drawLeaderBoard :: Game -> Widget Name
drawLeaderBoard _ = emptyWidget
drawInfo :: Game -> Widget Name
drawInfo g = hLimit 18 -- size of next piece box
$ vBox
[ drawNextShape (g ^. nextShape)
, padTop (Pad 1) drawHelp
, padTop (Pad 1) (drawGameOver g)
]
drawNextShape :: Tetrimino -> Widget Name
drawNextShape t =
withBorderStyle BS.unicodeBold
$ B.borderWithLabel (str "Next")
$ padTopBottom 1
$ padLeftRight 4
$ vLimit 4
$ vBox
$ [0, -1] <&> \y ->
hBox [ if V2 x y `elem` coords blk
then drawBlockCell NormalBlock t
else emptyNextShapeCellW
| x <- [-2 .. 1]
]
where blk = Block t (V2 0 0) (relCells t)
drawHelp :: Widget Name
drawHelp =
withBorderStyle BS.unicodeBold
$ B.borderWithLabel (str "Help")
$ padTopBottom 1
$ vBox
$ map (uncurry drawKeyInfo)
[ ("Left" , "h, ←")
, ("Right" , "l, →")
, ("Down" , "j, ↓")
, ("Rotate" , "k, ↑")
, ("Drop" , "space")
, ("Restart", "r")
, ("Pause" , "p")
, ("Quit" , "q")
]
drawKeyInfo :: String -> String -> Widget Name
drawKeyInfo action keys =
padRight Max (padLeft (Pad 1) $ str action)
<+> padLeft Max (padRight (Pad 1) $ str keys)
drawGameOver :: Game -> Widget Name
drawGameOver g =
if isGameOver g
then padLeftRight 4 $ withAttr gameOverAttr $ str "GAME OVER"
else emptyWidget
theMap :: AttrMap
theMap = attrMap
V.defAttr
[ (iAttr , tToColor I `on` tToColor I)
, (oAttr , tToColor O `on` tToColor O)
, (tAttr , tToColor T `on` tToColor T)
, (sAttr , tToColor S `on` tToColor S)
, (zAttr , tToColor Z `on` tToColor Z)
, (jAttr , tToColor J `on` tToColor J)
, (lAttr , tToColor L `on` tToColor L)
, (ihAttr , fg $ tToColor I)
, (ohAttr , fg $ tToColor O)
, (thAttr , fg $ tToColor T)
, (shAttr , fg $ tToColor S)
, (zhAttr , fg $ tToColor Z)
, (jhAttr , fg $ tToColor J)
, (lhAttr , fg $ tToColor L)
, (gameOverAttr , fg V.red `V.withStyle` V.bold)
, (progressionAttr, fg V.green `V.withStyle` V.bold)
, (fixedAttr , fg V.blue `V.withStyle` V.bold)
]
tToColor :: Tetrimino -> V.Color
tToColor I = V.cyan
tToColor O = V.yellow
tToColor T = V.magenta
tToColor S = V.green
tToColor Z = V.red
tToColor J = V.blue
tToColor L = V.white
iAttr, oAttr, tAttr, sAttr, zAttr, jAttr, lAttr :: AttrName
iAttr = attrName "I"
oAttr = attrName "O"
tAttr = attrName "T"
sAttr = attrName "S"
zAttr = attrName "Z"
jAttr = attrName "J"
lAttr = attrName "L"
ihAttr, ohAttr, thAttr, shAttr, zhAttr, jhAttr, lhAttr :: AttrName
ihAttr = attrName "Ih"
ohAttr = attrName "Oh"
thAttr = attrName "Th"
shAttr = attrName "Sh"
zhAttr = attrName "Zh"
jhAttr = attrName "Jh"
lhAttr = attrName "Lh"
emptyAttr :: AttrName
emptyAttr = attrName "empty"
gameOverAttr :: AttrName
gameOverAttr = attrName "gameOver"
progressionAttr, fixedAttr :: AttrName
progressionAttr = attrName "progression"
fixedAttr = attrName "fixed"