{-# 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"