Compare commits

..

No commits in common. "725b7bdf9456707f7ff2da4afb60a7909151caa5" and "3fd342c69f8a8f305dad12ed150d92f27de60ea5" have entirely different histories.

8 changed files with 155 additions and 450 deletions

View file

@ -1,138 +0,0 @@
# This is copied from the stan project
#
# Note [environment variables]
#
# It seems absurd, but the syntax for creating environment variables
# differs between Windows and Linux/MacOS. See
#
# https://docs.github.com/en/actions/learn-github-actions/variables
#
# In Linux/MacOS we have to use
#
# run: echo "VARNAME=content" >> "$GITHUB_ENV"
#
# whereas in Windows we have to use
#
# run: echo "VARNAME=content" >> $env:GITHUB_ENV
name: Release
on:
# Trigger the workflow on the new 'v*' tag created
push:
tags:
- "v*"
jobs:
create_release:
name: Create Github Release
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Create Release
id: create_release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: true
prerelease: false
- name: Output Release URL File
run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt
- name: Save Release URL File for publish
uses: actions/upload-artifact@v3
with:
name: release_url
path: release_url.txt
build_artifact:
needs: [create_release]
name: ${{ matrix.os }}/${{ github.ref }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set tag name
uses: olegtarasov/get-tag@v2.1.2
id: tag
with:
tagRegex: "v(.*)"
tagRegexGroup: 1
- name: Setup Haskell
uses: haskell-actions/setup@v2
id: setup-haskell-cabal
with:
ghc-version: "latest"
cabal-version: "latest"
- name: Freeze
run: |
cabal freeze
- name: Cache ~/.cabal/store
uses: actions/cache@v4
with:
path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
key: ${{ runner.os }}-${{ hashFiles('cabal.project.freeze') }}
- name: Build binary
run: |
mkdir dist
cabal install exe:tetris --install-method=copy --overwrite-policy=always --installdir=dist
# See Note [environment variables]
- if: matrix.os == 'windows-latest'
name: Set binary path name on Windows
run: echo "BINARY_PATH=./dist/tetris.exe" >> $env:GITHUB_ENV
# See Note [environment variables]
- if: matrix.os != 'windows-latest'
name: Set binary path name not on Windows
run: echo "BINARY_PATH=./dist/tetris" >> "$GITHUB_ENV"
- if: matrix.os != 'macOS-latest'
name: Compress binary
uses: svenstaro/upx-action@2.3.0
with:
file: ${{ env.BINARY_PATH }}
- name: Load Release URL File from release job
uses: actions/download-artifact@v3
with:
name: release_url
path: release_url
# See Note [environment variables]
- if: matrix.os == 'windows-latest'
name: Get Release File Name & Upload URL on Widows
run: |
echo "upload_url=$(cat release_url/release_url.txt)" >> $env:GITHUB_ENV
# See Note [environment variables]
- if: matrix.os != 'windows-latest'
name: Get Release File Name & Upload URL not on Widows
run: |
echo "upload_url=$(cat release_url/release_url.txt)" >> $GITHUB_ENV
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1.0.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.upload_url }}
asset_path: ${{ env.BINARY_PATH }}
asset_name: tetris-${{ steps.tag.outputs.tag }}-${{ runner.os }}
asset_content_type: application/octet-stream

View file

@ -9,7 +9,7 @@ import qualified System.Directory as D
import System.FilePath ((</>)) import System.FilePath ((</>))
import Tetris (Game(..)) import Tetris (Game(..))
import UI.PickLevel (pickLevel, LevelConfig(..)) import UI.PickLevel (pickLevel)
import UI.Game (playGame) import UI.Game (playGame)
data Opts = Opts data Opts = Opts
@ -70,11 +70,11 @@ hdOptStr (CustomChars s) = Just s
main :: IO () main :: IO ()
main = do main = do
(Opts hd ml hs) <- execParser fullopts (Opts hd ml hs) <- execParser fullopts -- get CLI opts/args
when hs (getHighScore >>= printM >> exitSuccess) when hs (getHighScore >>= printM >> exitSuccess) -- show high score and exit
levelConfig <- maybe pickLevel (\l -> return $ LevelConfig l False) ml l <- maybe pickLevel return ml -- pick level prompt if necessary
g <- playGame (levelNumber levelConfig) (hdOptStr hd) (progression levelConfig) g <- playGame l (hdOptStr hd) -- play game
handleEndGame (_score g) handleEndGame (_score g) -- save & print score
handleEndGame :: Int -> IO () handleEndGame :: Int -> IO ()
handleEndGame s = do handleEndGame s = do

View file

@ -1,4 +1,3 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
@ -12,6 +11,7 @@ module Tetris
, rotate , rotate
, hardDrop , hardDrop
-- Game state handlers -- Game state handlers
, execTetris
, evalTetris , evalTetris
-- Game state queries -- Game state queries
, isGameOver , isGameOver
@ -25,7 +25,7 @@ module Tetris
, Tetrimino(..) , Tetrimino(..)
, Tetris , Tetris
-- Lenses -- Lenses
, block, board, level, nextShape, score, shape, linesCleared, progression , block, board, level, nextShape, score, shape
-- Constants -- Constants
, boardHeight, boardWidth, relCells , boardHeight, boardWidth, relCells
) where ) where
@ -33,10 +33,9 @@ module Tetris
import Prelude hiding (Left, Right) import Prelude hiding (Left, Right)
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Monad (forM_, mfilter, when, (<=<)) import Control.Monad (forM_, mfilter, when, (<=<))
import Control.Monad.IO.Class (MonadIO(..), liftIO) import Control.Monad.IO.Class (MonadIO(..), liftIO)
import Control.Monad.State.Class (MonadState, gets, put)
import Control.Monad.Trans.State (evalStateT) import Control.Monad.Trans.State (StateT(..), gets, evalStateT, execStateT)
import Data.Map (Map) import Data.Map (Map)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Sequence (Seq(..), (><)) import Data.Sequence (Seq(..), (><))
@ -45,7 +44,6 @@ import Control.Lens hiding (Empty)
import Linear.V2 (V2(..), _y) import Linear.V2 (V2(..), _y)
import qualified Linear.V2 as LV import qualified Linear.V2 as LV
import System.Random (getStdRandom, randomR) import System.Random (getStdRandom, randomR)
-- Types and instances -- Types and instances
-- | Tetris shape types -- | Tetris shape types
@ -79,17 +77,20 @@ data Game = Game
, _block :: Block , _block :: Block
, _nextShape :: Tetrimino , _nextShape :: Tetrimino
, _nextShapeBag :: Seq.Seq Tetrimino , _nextShapeBag :: Seq.Seq Tetrimino
, _linesCleared :: Int , _rowClears :: Seq.Seq Int
, _score :: Int , _score :: Int
, _board :: Board , _board :: Board
, _progression :: Bool } deriving (Eq, Show)
} deriving (Eq)
makeLenses ''Game makeLenses ''Game
type TetrisT = StateT Game
type Tetris a = forall m. (Monad m) => TetrisT m a
evalTetris :: Tetris a -> Game -> a evalTetris :: Tetris a -> Game -> a
evalTetris m = runIdentity . evalStateT m evalTetris m = runIdentity . evalStateT m
type Tetris a = forall m. MonadState Game m => m a execTetris :: Tetris a -> Game -> Game
execTetris m = runIdentity . execStateT m
-- Translate class for direct translations, without concern for boundaries -- Translate class for direct translations, without concern for boundaries
-- 'shift' concerns safe translations with boundaries -- 'shift' concerns safe translations with boundaries
@ -162,8 +163,8 @@ bagFourTetriminoEach Empty =
bagFourTetriminoEach <=< shuffle . Seq.fromList . take 28 $ cycle [I ..] bagFourTetriminoEach <=< shuffle . Seq.fromList . take 28 $ cycle [I ..]
-- | Initialize a game with a given level -- | Initialize a game with a given level
initGame :: Int -> Bool -> IO Game -- Updated signature initGame :: Int -> IO Game
initGame lvl prog = do initGame lvl = do
(s1, bag1) <- bagFourTetriminoEach mempty (s1, bag1) <- bagFourTetriminoEach mempty
(s2, bag2) <- bagFourTetriminoEach bag1 (s2, bag2) <- bagFourTetriminoEach bag1
pure $ Game pure $ Game
@ -172,40 +173,31 @@ initGame lvl prog = do
, _nextShape = s2 , _nextShape = s2
, _nextShapeBag = bag2 , _nextShapeBag = bag2
, _score = 0 , _score = 0
, _linesCleared = 0 , _rowClears = mempty
, _board = mempty , _board = mempty
, _progression = prog -- Added prog parameter
} }
-- | Increment level
nextLevel :: (MonadIO m, MonadState Game m) => m ()
nextLevel = do
level %= (+ 1)
isGameOver :: Game -> Bool isGameOver :: Game -> Bool
isGameOver g = blockStopped g && g ^. (block . origin) == startOrigin isGameOver g = blockStopped g && g ^. (block . origin) == startOrigin
-- | The main game execution, this is executed at each discrete time step -- | The main game execution, this is executed at each discrete time step
timeStep :: (MonadIO m, MonadState Game m) => m () timeStep :: MonadIO m => TetrisT m ()
timeStep = do timeStep = do
gets blockStopped >>= \case gets blockStopped >>= \case
False -> gravitate False -> gravitate
True -> do True -> do
freezeBlock freezeBlock
clearFullRows >>= updateScore n <- clearFullRows
prog <- use progression addToRowClears n
when prog $ do updateScore
levelFinished >>= \case
True -> nextLevel
False -> pure ()
nextBlock nextBlock
-- | Gravitate current block, i.e. shift down -- | Gravitate current block, i.e. shift down
gravitate :: MonadState Game m => m () gravitate :: Tetris ()
gravitate = shift Down gravitate = shift Down
-- | If necessary: clear full rows and return the count -- | If necessary: clear full rows and return the count
clearFullRows :: MonadState Game m => m Int clearFullRows :: Tetris Int
clearFullRows = do clearFullRows = do
brd <- use board brd <- use board
let rowSize r = length $ M.filterWithKey (\(V2 _ y) _ -> r == y) brd let rowSize r = length $ M.filterWithKey (\(V2 _ y) _ -> r == y) brd
@ -215,42 +207,40 @@ clearFullRows = do
-- Shift cells above full rows -- Shift cells above full rows
modifying board $ M.mapKeysMonotonic $ over _y $ \y -> modifying board $ M.mapKeysMonotonic $ over _y $ \y ->
y - length (filter (< y) fullRows) y - length (filter (< y) fullRows)
let clearedLines = length fullRows return $ length fullRows
linesCleared %= (+ clearedLines)
pure clearedLines
-- | This updates game points with respect to the provided number of cleared -- | Empties row on 0, otherwise appends value (just keeps consecutive information)
-- lines. addToRowClears :: Int -> Tetris ()
addToRowClears 0 = rowClears .= mempty
addToRowClears n = rowClears %= (|> n)
-- | This updates game points with respect to the current
-- _rowClears value (thus should only be used ONCE per step)
-- --
-- See https://tetris.fandom.com/wiki/Scoring -- Note I'm keeping rowClears as a sequence in case I want to award
updateScore :: (MonadState Game m, MonadIO m) => Int -> m () -- more points for back to back clears, right now the scoring is more simple,
updateScore 0 = pure () -- but you do get more points for more rows cleared at once.
updateScore lines = do updateScore :: Tetris ()
lvl <- use level updateScore = do
let newPoints = (lvl + 1) * points lines multiplier <- (1 +) <$> use level
clears <- latestOrZero <$> use rowClears
let newPoints = multiplier * points clears
score %= (+ newPoints) score %= (+ newPoints)
where where
-- Translate row line clears to points -- Translate row clears to points
points 0 = 0 points 0 = 0
points 1 = 40 points 1 = 40
points 2 = 100 points 2 = 100
points 3 = 300 points 3 = 300
points _ = 1200 points _ = 800
-- | Get last value of sequence or 0 if empty
-- | Using the fixed-goal system described here: https://tetris.wiki/Marathon latestOrZero :: Seq.Seq Int -> Int
levelFinished :: (MonadState Game m, MonadIO m) => m Bool latestOrZero Empty = 0
levelFinished = do latestOrZero (_ :|> n) = n
prog <- use progression
if not prog
then pure False
else do
lvl <- use level
lc <- use linesCleared
pure $ lvl < 15 && lc >= 10 * (lvl + 1)
-- | Handle counterclockwise block rotation (if possible) -- | Handle counterclockwise block rotation (if possible)
-- Allows wallkicks: http://tetris.wikia.com/wiki/TGM_rotation -- Allows wallkicks: http://tetris.wikia.com/wiki/TGM_rotation
rotate :: MonadState Game m => m () rotate :: Tetris ()
rotate = do rotate = do
blk <- use block blk <- use block
brd <- use board brd <- use board
@ -274,10 +264,10 @@ isStopped brd = any stopped . coords
stopped = (||) <$> atBottom <*> (`M.member` brd) . translate Down stopped = (||) <$> atBottom <*> (`M.member` brd) . translate Down
atBottom = (== 1) . view _y atBottom = (== 1) . view _y
hardDrop :: MonadState Game m => m () hardDrop :: Tetris ()
hardDrop = hardDroppedBlock >>= assign block hardDrop = hardDroppedBlock >>= assign block
hardDroppedBlock :: MonadState Game m => m Block hardDroppedBlock :: Tetris Block
hardDroppedBlock = do hardDroppedBlock = do
boardCoords <- M.keys <$> use board boardCoords <- M.keys <$> use board
blockCoords <- coords <$> use block blockCoords <- coords <$> use block
@ -293,13 +283,13 @@ hardDroppedBlock = do
translateBy dist Down <$> use block translateBy dist Down <$> use block
-- | Freeze current block -- | Freeze current block
freezeBlock :: MonadState Game m => m () freezeBlock :: Tetris ()
freezeBlock = do freezeBlock = do
blk <- use block blk <- use block
modifying board $ M.union $ M.fromList [ (c, _shape blk) | c <- coords blk ] modifying board $ M.union $ M.fromList [ (c, _shape blk) | c <- coords blk ]
-- | Replace block with next block -- | Replace block with next block
nextBlock :: (MonadIO m, MonadState Game m) => m () nextBlock :: MonadIO m => TetrisT m ()
nextBlock = do nextBlock = do
bag <- use nextShapeBag bag <- use nextShapeBag
(t, ts) <- liftIO $ bagFourTetriminoEach bag (t, ts) <- liftIO $ bagFourTetriminoEach bag
@ -308,7 +298,7 @@ nextBlock = do
nextShapeBag .= ts nextShapeBag .= ts
-- | Try to shift current block; if shifting not possible, leave block where it is -- | Try to shift current block; if shifting not possible, leave block where it is
shift :: MonadState Game m => Direction -> m () shift :: Direction -> Tetris ()
shift dir = do shift dir = do
brd <- use board brd <- use board
blk <- use block blk <- use block

View file

@ -1,4 +1,3 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
@ -8,8 +7,9 @@ module UI.Game
) where ) where
import Control.Concurrent (threadDelay, forkIO) import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.STM (TVar, newTVarIO, readTVarIO, writeTVar, atomically) import Control.Monad (void, forever, when, unless)
import Control.Monad (void, forever) import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.State (execStateT)
import Prelude hiding (Left, Right) import Prelude hiding (Left, Right)
import Brick hiding (Down) import Brick hiding (Down)
@ -18,11 +18,7 @@ import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.Border.Style as BS import qualified Brick.Widgets.Border.Style as BS
import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Center as C
import Control.Lens hiding (preview, op, zoom) 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 as V
import qualified Graphics.Vty.CrossPlatform
import qualified Graphics.Vty.Config
import Data.Map (Map) import Data.Map (Map)
import qualified Data.Map as M import qualified Data.Map as M
import Linear.V2 (V2(..)) import Linear.V2 (V2(..))
@ -30,12 +26,10 @@ import Linear.V2 (V2(..))
import Tetris import Tetris
data UI = UI data UI = UI
{ _game :: Game { _game :: Game -- ^ tetris game
, _initLevel :: Int , _preview :: Maybe String -- ^ hard drop preview cell
, _currLevel :: TVar Int , _locked :: Bool -- ^ lock after hard drop before time step
, _preview :: Maybe String , _paused :: Bool -- ^ game paused
, _locked :: Bool
, _paused :: Bool
} }
makeLenses ''UI makeLenses ''UI
@ -61,27 +55,24 @@ app = App
, appAttrMap = const theMap , appAttrMap = const theMap
} }
playGame :: Int -- ^ Starting level playGame
-> Maybe String -- ^ Preview cell (Nothing == no preview) :: Int -- ^ Starting level
-> Bool -- ^ Enable level progression -> Maybe String -- ^ Preview cell (Nothing == no preview)
-> IO Game -> IO Game
playGame lvl mp prog = do playGame lvl mp = do
let delay = levelToDelay lvl
chan <- newBChan 10 chan <- newBChan 10
tv <- newTVarIO lvl
void . forkIO $ forever $ do void . forkIO $ forever $ do
writeBChan chan Tick writeBChan chan Tick
lvl <- readTVarIO tv threadDelay delay
threadDelay $ levelToDelay lvl initialGame <- initGame lvl
initialGame <- initGame lvl prog -- Pass the progression parameter let builder = V.mkVty V.defaultConfig
let buildVty = Graphics.Vty.CrossPlatform.mkVty Graphics.Vty.Config.defaultConfig initialVty <- builder
initialVty <- buildVty ui <- customMain initialVty builder (Just chan) app $ UI
ui <- customMain initialVty buildVty (Just chan) app $ UI { _game = initialGame
{ _game = initialGame , _preview = mp
, _initLevel = lvl , _locked = False
, _currLevel = tv , _paused = False
, _preview = mp
, _locked = False
, _paused = False
} }
return $ ui ^. game return $ ui ^. game
@ -91,9 +82,7 @@ levelToDelay n = floor $ 400000 * (0.85 :: Double) ^ (2 * n)
-- Handling events -- Handling events
handleEvent :: BrickEvent Name Tick -> EventM Name UI () handleEvent :: BrickEvent Name Tick -> EventM Name UI ()
handleEvent (VtyEvent (V.EvKey (V.KChar 'r') [])) = restart handleEvent (AppEvent Tick ) = handleTick
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.KRight [])) = exec (shift Right)
handleEvent (VtyEvent (V.EvKey V.KLeft [])) = exec (shift Left) handleEvent (VtyEvent (V.EvKey V.KLeft [])) = exec (shift Left)
handleEvent (VtyEvent (V.EvKey V.KDown [])) = exec (shift Down) handleEvent (VtyEvent (V.EvKey V.KDown [])) = exec (shift Down)
@ -103,36 +92,53 @@ handleEvent (VtyEvent (V.EvKey (V.KChar 'j') [])) = exec (shift Down)
handleEvent (VtyEvent (V.EvKey V.KUp [])) = exec rotate handleEvent (VtyEvent (V.EvKey V.KUp [])) = exec rotate
handleEvent (VtyEvent (V.EvKey (V.KChar 'k') [])) = exec rotate handleEvent (VtyEvent (V.EvKey (V.KChar 'k') [])) = exec rotate
handleEvent (VtyEvent (V.EvKey (V.KChar ' ') [])) = handleEvent (VtyEvent (V.EvKey (V.KChar ' ') [])) =
unlessM (orM [use paused, use (game . to isGameOver)]) $ do guarded
zoom game hardDrop (not . view paused)
assign locked True (over game (execTetris hardDrop) . set locked True)
handleEvent (VtyEvent (V.EvKey (V.KChar 'p') [])) = handleEvent (VtyEvent (V.EvKey (V.KChar 'p') [])) =
unlessM (orM [use locked, use (game . to isGameOver)]) $ do guarded
modifying paused not (not . view locked)
handleEvent (AppEvent Tick ) = (over paused not)
unlessM (orM [use paused, use (game . to isGameOver)]) $ do handleEvent (VtyEvent (V.EvKey (V.KChar 'r') [])) = restart
zoom game timeStep handleEvent (VtyEvent (V.EvKey (V.KChar 'q') [])) = halt
-- Keep level in sync with ticker (gross) handleEvent (VtyEvent (V.EvKey V.KEsc [])) = halt
lvl <- use $ game . level
tv <- use $ currLevel
liftIO . atomically $ writeTVar tv lvl
assign locked False
handleEvent _ = pure () handleEvent _ = pure ()
-- | This common execution function is used for all game user input except hard -- | 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 -- drop and pause. If paused or locked (from hard drop) do nothing, else
-- execute the state computation. -- execute the state computation.
exec :: Tetris () -> EventM Name UI () exec :: Tetris () -> EventM Name UI ()
exec = unlessM (orM [use paused, use locked, use (game . to isGameOver)]) . zoom game exec op =
guarded
(not . \ui -> ui ^. paused || ui ^. locked)
(game %~ execTetris op)
-- | Restart game at the initially chosen level -- | This base execution function takes a predicate and only issues UI
-- modification when predicate passes and game is not over.
guarded :: (UI -> Bool) -> (UI -> UI) -> EventM Name UI ()
guarded p f = do
ui <- get
when (p ui && not (ui ^. game . to isGameOver)) $
modify f
-- | Handles time steps, does nothing if game is over or paused
handleTick :: EventM Name UI ()
handleTick = do
ui <- get
unless (ui ^. paused || ui ^. game . to isGameOver) $ do
-- awkward, should just mutate the inner state
--zoom game timeStep
g' <- execStateT timeStep $ ui ^. game
game .= g'
locked .= False
-- | Restart game at the same level
restart :: EventM Name UI () restart :: EventM Name UI ()
restart = do restart = do
lvl <- use initLevel lvl <- use $ game . level
prog <- use (game . progression) -- Get current progression setting g <- liftIO $ initGame lvl
g <- liftIO $ initGame lvl prog -- Use it when restarting game .= g
assign game g locked .= False
assign locked False
-- Drawing -- Drawing
@ -212,20 +218,10 @@ drawStats g =
$ B.borderWithLabel (str "Stats") $ B.borderWithLabel (str "Stats")
$ vBox $ vBox
[ drawStat "Score" $ g ^. score [ drawStat "Score" $ g ^. score
, padTop (Pad 1) $ drawStat "Lines" $ g ^. linesCleared
, padTop (Pad 1) $ drawStat "Level" $ g ^. level , padTop (Pad 1) $ drawStat "Level" $ g ^. level
, padTop (Pad 1) $ drawProgression (g ^. progression)
, drawLeaderBoard g , 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 :: String -> Int -> Widget Name
drawStat s n = padLeftRight 1 $ str s <+> padLeft Max (str $ show n) drawStat s n = padLeftRight 1 $ str s <+> padLeft Max (str $ show n)
@ -287,23 +283,21 @@ drawGameOver g =
theMap :: AttrMap theMap :: AttrMap
theMap = attrMap theMap = attrMap
V.defAttr V.defAttr
[ (iAttr , tToColor I `on` tToColor I) [ (iAttr , tToColor I `on` tToColor I)
, (oAttr , tToColor O `on` tToColor O) , (oAttr , tToColor O `on` tToColor O)
, (tAttr , tToColor T `on` tToColor T) , (tAttr , tToColor T `on` tToColor T)
, (sAttr , tToColor S `on` tToColor S) , (sAttr , tToColor S `on` tToColor S)
, (zAttr , tToColor Z `on` tToColor Z) , (zAttr , tToColor Z `on` tToColor Z)
, (jAttr , tToColor J `on` tToColor J) , (jAttr , tToColor J `on` tToColor J)
, (lAttr , tToColor L `on` tToColor L) , (lAttr , tToColor L `on` tToColor L)
, (ihAttr , fg $ tToColor I) , (ihAttr , fg $ tToColor I)
, (ohAttr , fg $ tToColor O) , (ohAttr , fg $ tToColor O)
, (thAttr , fg $ tToColor T) , (thAttr , fg $ tToColor T)
, (shAttr , fg $ tToColor S) , (shAttr , fg $ tToColor S)
, (zhAttr , fg $ tToColor Z) , (zhAttr , fg $ tToColor Z)
, (jhAttr , fg $ tToColor J) , (jhAttr , fg $ tToColor J)
, (lhAttr , fg $ tToColor L) , (lhAttr , fg $ tToColor L)
, (gameOverAttr , fg V.red `V.withStyle` V.bold) , (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 :: Tetrimino -> V.Color
@ -338,7 +332,3 @@ emptyAttr = attrName "empty"
gameOverAttr :: AttrName gameOverAttr :: AttrName
gameOverAttr = attrName "gameOver" gameOverAttr = attrName "gameOver"
progressionAttr, fixedAttr :: AttrName
progressionAttr = attrName "progression"
fixedAttr = attrName "fixed"

View file

@ -1,6 +1,5 @@
module UI.PickLevel module UI.PickLevel
( pickLevel ( pickLevel
, LevelConfig(..)
) where ) where
import System.Exit (exitSuccess) import System.Exit (exitSuccess)
@ -12,39 +11,17 @@ import qualified Brick.Widgets.Border.Style as BS
import qualified Brick.Widgets.Center as C import qualified Brick.Widgets.Center as C
import qualified Graphics.Vty as V import qualified Graphics.Vty as V
data LevelConfig = LevelConfig app :: App (Maybe Int) e ()
{ levelNumber :: Int
, progression :: Bool
} deriving (Show, Eq)
data MenuOption = YesOption | NoOption deriving (Eq)
data PickState = PickState
{ currentLevel :: Maybe Int
, showProgression :: Bool
, pickingLevel :: Bool
, selectedOption :: MenuOption
}
app :: App PickState e ()
app = App app = App
{ appDraw = drawUI { appDraw = const [ui]
, appHandleEvent = handleEvent , appHandleEvent = handleEvent
, appStartEvent = pure () , appStartEvent = pure ()
, appAttrMap = const $ attrMap V.defAttr , appAttrMap = const $ attrMap V.defAttr []
[ (selectedAttr, V.black `on` V.white)
]
, appChooseCursor = neverShowCursor , appChooseCursor = neverShowCursor
} }
selectedAttr :: AttrName ui :: Widget ()
selectedAttr = attrName "selected" ui =
drawUI :: PickState -> [Widget ()]
drawUI ps = [ui ps]
ui :: PickState -> Widget ()
ui ps =
padLeft (Pad 19) padLeft (Pad 19)
$ padRight (Pad 21) $ padRight (Pad 21)
$ C.center $ C.center
@ -53,69 +30,17 @@ ui ps =
$ withBorderStyle BS.unicodeBold $ withBorderStyle BS.unicodeBold
$ B.borderWithLabel (str "Tetris") $ B.borderWithLabel (str "Tetris")
$ C.center $ C.center
$ vBox $ str " Choose Level (0-9)"
[ if pickingLevel ps
then str "Choose Level (0-9)"
else vBox
[ str "Level Progression?"
, str ""
, C.hCenter $ drawOption "YES" YesOption (selectedOption ps)
, C.hCenter $ drawOption "NO" NoOption (selectedOption ps)
, str ""
, C.hCenter $ str "Use ↑↓ or j/k"
, C.hCenter $ str "to Select."
, str ""
, C.hCenter $ str "Press Enter"
, C.hCenter $ str "to Continue."
]
]
drawOption :: String -> MenuOption -> MenuOption -> Widget () handleEvent :: BrickEvent () e -> EventM () (Maybe Int) ()
drawOption label opt current =
withAttr (if opt == current then selectedAttr else attrName "")
$ str $ " " ++ label ++ " "
handleEvent :: BrickEvent () e -> EventM () PickState ()
handleEvent (VtyEvent (V.EvKey V.KEsc _)) = halt handleEvent (VtyEvent (V.EvKey V.KEsc _)) = halt
handleEvent (VtyEvent (V.EvKey (V.KChar 'q') _)) = halt handleEvent (VtyEvent (V.EvKey (V.KChar 'q') _)) = halt
handleEvent (VtyEvent (V.EvKey (V.KChar 'Q') _)) = halt handleEvent (VtyEvent (V.EvKey (V.KChar 'Q') _)) = halt
handleEvent (VtyEvent (V.EvKey (V.KChar d) [])) = handleEvent (VtyEvent (V.EvKey (V.KChar d) [])) =
whenPickingLevel $ when (d `elem` ['0' .. '9']) $ do when (d `elem` ['0' .. '9']) $ do
modify $ \s -> s { currentLevel = Just $ read [d], pickingLevel = False } put $ Just $ read [d]
handleEvent (VtyEvent (V.EvKey V.KEnter [])) = do halt
s <- get
when (not $ pickingLevel s) $ do
case currentLevel s of
Just l -> do
put $ PickState (Just l) (selectedOption s == YesOption) True YesOption
halt
Nothing -> pure ()
handleEvent (VtyEvent (V.EvKey V.KUp [])) =
whenNotPickingLevel $ modify $ \s -> s { selectedOption = YesOption }
handleEvent (VtyEvent (V.EvKey V.KDown [])) =
whenNotPickingLevel $ modify $ \s -> s { selectedOption = NoOption }
handleEvent (VtyEvent (V.EvKey (V.KChar 'j') [])) =
whenNotPickingLevel $ modify $ \s -> s { selectedOption = YesOption }
handleEvent (VtyEvent (V.EvKey (V.KChar 'k') [])) =
whenNotPickingLevel $ modify $ \s -> s { selectedOption = NoOption }
handleEvent _ = pure () handleEvent _ = pure ()
whenPickingLevel :: EventM () PickState () -> EventM () PickState () pickLevel :: IO Int
whenPickingLevel action = do pickLevel = defaultMain app Nothing >>= maybe exitSuccess return
picking <- gets pickingLevel
when picking action
whenNotPickingLevel :: EventM () PickState () -> EventM () PickState ()
whenNotPickingLevel action = do
picking <- gets pickingLevel
when (not picking) action
initialState :: PickState
initialState = PickState Nothing True True YesOption
pickLevel :: IO LevelConfig
pickLevel = do
result <- defaultMain app initialState
case currentLevel result of
Nothing -> exitSuccess
Just l -> return $ LevelConfig l (showProgression result)

View file

@ -1,67 +1,10 @@
# This file was automatically generated by 'stack init' flags: {}
# extra-package-dbs: []
# Some commonly used options have been documented as comments in this file.
# For advanced use and comprehensive documentation of the format, please see:
# https://docs.haskellstack.org/en/stable/yaml_configuration/
# A 'specific' Stackage snapshot or a compiler version.
# A snapshot resolver dictates the compiler version and the set of packages
# to be used for project dependencies. For example:
#
# snapshot: lts-22.28
# snapshot: nightly-2024-07-05
# snapshot: ghc-9.6.6
#
# The location of a snapshot can be provided as a file or url. Stack assumes
# a snapshot provided as a file might change, whereas a url resource does not.
#
# snapshot: ./custom-snapshot.yaml
# snapshot: https://example.com/snapshots/2024-01-01.yaml
snapshot:
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/43.yaml
# User packages to be built.
# Various formats can be used as shown in the example below.
#
# packages:
# - some-directory
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
# subdirs:
# - auto-update
# - wai
packages: packages:
- . - '.'
# Dependency packages to be pulled from upstream that are not in the snapshot. extra-deps: []
# These entries can reference officially published versions as well as resolver: lts-20.1
# forks / in-progress versions pinned to a git hash. For example: nix:
# packages:
# extra-deps: - gcc
# - acme-missiles-0.3 - ncurses
# - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#
# extra-deps: []
# Override default flag values for project packages and extra-deps
# flags: {}
# Extra package databases containing global packages
# extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
#
# Require a specific version of Stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: ">=3.1"
#
# Override the architecture used by Stack, especially useful on Windows
# arch: i386
# arch: x86_64
#
# Extra directories used by Stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
#
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View file

@ -6,8 +6,7 @@
packages: [] packages: []
snapshots: snapshots:
- completed: - completed:
sha256: 08bd13ce621b41a8f5e51456b38d5b46d7783ce114a50ab604d6bbab0d002146 sha256: b73b2b116143aea728c70e65c3239188998bac5bc3be56465813dacd74215dc5
size: 720271 size: 648424
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/43.yaml url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/1.yaml
original: original: lts-20.1
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/43.yaml

View file

@ -1,5 +1,5 @@
name: tetris name: tetris
version: 0.1.6 version: 0.1.5
homepage: https://github.com/samtay/tetris#readme homepage: https://github.com/samtay/tetris#readme
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
@ -19,15 +19,11 @@ library
build-depends: base >= 4.7 && < 5 build-depends: base >= 4.7 && < 5
, brick , brick
, containers , containers
, extra
, lens , lens
, linear , linear
, mtl
, random , random
, stm
, transformers , transformers
, vty , vty
, vty-crossplatform
default-language: Haskell2010 default-language: Haskell2010
executable tetris executable tetris