packages feed

betris-0.2.3.0: src/Command/Betris.hs

{-# LANGUAGE DeriveFunctor    #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase       #-}
{-# LANGUAGE RecordWildCards  #-}
{-# LANGUAGE TupleSections    #-}
module Command.Betris (command, Options(..), betris) where

import           Control.Concurrent           (forkIO, threadDelay)
import           Control.Concurrent.STM.TChan
import           Control.Monad                (forever)
import           Control.Monad.STM            (atomically)
import           Data.Char                    (chr)
import           Data.IORef
import qualified Data.Map                     as Map
import           Data.Time.Units
import           Data.Version                 (showVersion)
import           Game.Tetris
import           Graphics.Vty.Config          (userConfig)
import           Graphics.Vty.Input           hiding (Event)
import qualified Graphics.Vty.Input           as Vty (Event)
import           Graphics.Vty.Input.Events    (InternalEvent (InputEvent))
import           Lens.Micro                   ((^.))
import           Linear.V2                    (V2 (..), _x)
import           Options.Applicative          hiding (command, (<|>))
import           Paths_betris                 (version)
import           Prelude                      hiding (Left, Right)
import           System.Console.ANSI
import           System.IO                    (hFlush, stdout)

command :: Parser (IO ())
command = betris <$> (versionOption <*> programOptions)

newtype Options = Options { initialDelay :: Millisecond } deriving (Eq, Show)

programOptions :: Parser Options
programOptions = Options <$> initialDelayOption

data Event e = Tick | Ev e deriving (Eq, Read, Show, Functor)

betris :: Options -> IO ()
betris Options{..} = do
  input <- inputForConfig =<< userConfig
  chan <- newTChanIO
  game <- initGame 0
  delay <- newIORef $ fromIntegral $ toMicroseconds initialDelay

  _ <- forkIO . forever . atomically $ readTChan (_eventChannel input) >>= \case
         InputEvent e -> writeTChan chan (Ev e)
         _            -> pure ()
  _ <- forkIO $ forever $ do
    readIORef delay >>= threadDelay
    modifyIORef delay (subtract 100)
    atomically $ writeTChan chan Tick

  _ <- play chan game
  shutdownInput input
  putStrLn ""

play :: TChan (Event Vty.Event) -> Game -> IO Game
play chan tetris
  | isGameOver tetris = pure tetris
  | otherwise = do
    putStr $ "\r"
          <> emboss tetris
          <> " [" <> show (tetris ^. score) <> "]"
          <> clearFromCursorToLineEndCode
    hFlush stdout
    atomically (readTChan chan) >>= \case
      Tick                      -> play chan =<< timeStep tetris
      Ev (EvKey (KChar ' ') []) -> play chan =<< timeStep tetris
      Ev (EvKey KLeft [])       -> play chan $ hardDrop tetris
      Ev (EvKey KUp [])         -> play chan $ Left `shift` tetris
      Ev (EvKey KDown [])       -> play chan $ Right `shift` tetris
      Ev (EvKey KEnter [])      -> play chan $ rotate tetris
      Ev (EvKey KEsc [])        -> pure tetris
      _                         -> play chan tetris

emboss :: Game -> String
emboss game = map go [1, 3 .. boardHeight + 6] where
  go y = chr $ foldr (f y) 0x2800 [((0,0),1), ((1,0), 2), ((2,0), 4)
                                  ,((0,1),8), ((1,1),16), ((2,1),32)
                                  ,((3,0),64),((3,1),128)]
  f y ((x',y'), v) a = case Map.lookup (V2 (x+x') (y+y')) fullBoard of
    Just _ -> a + v
    _      -> a
  minx b = minimum $ (boardWidth - 3) : map (^. _x) (coords b)
  x = minx (game ^. block)
  fullBoard = game ^. board <> blk (game ^. block) <> blk next
  next = let b = initBlock (game ^. nextShape) in
         translateBy (-4) Down $ translateBy (x - minx b) Right b
  blk b = Map.fromList $ map (, b ^. shape) $ coords b

initialDelayOption :: Parser Millisecond
initialDelayOption = option auto $
    long "initial-delay"
 <> short 'i'
 <> metavar "DURATION"
 <> value (fromMicroseconds 1500000)
 <> showDefault
 <> help "Initial delay"

versionOption :: Parser (a -> a)
versionOption = infoOption (showVersion version) $
    long "version"
 <> help "Show version"