brillo-examples-2.0.0: picture/Tetris/src/World.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module World where
import Control.Monad.Reader
import Data.List
import Data.Map qualified as Map
import Data.Ord
import System.Random
import Config
import Figures
import Util
type Grid = Map.Map (GridPosition, GameColor) ()
data Hardness = Noob | Beginner | Average | Skilled | Masterful | Insane | Godlike
deriving (Enum, Bounded, Show)
noobBound :: Integer
noobBound = 0
beginnerBound :: Integer
beginnerBound = 300
averageBound :: Integer
averageBound = 800
skilledBound :: Integer
skilledBound = 1400
masterfulBound :: Integer
masterfulBound = 2200
insaneBound :: Integer
insaneBound = 3500
godlikeBound :: Integer
godlikeBound = 5000
-- COLORS
data GameColor = Red | Yellow | Green | Blue | Violet
deriving (Enum, Bounded, Ord, Eq)
-- Instance of class Random for Colors
instance Random GameColor where
random g = randomR (minBound, maxBound) g
randomR (a, b) g = case randomR (fromEnum a, fromEnum b) g of
(r, g') -> (toEnum r, g')
-- | Data represents the state of the Tetris game
data TetrisGame = Game
{ fallingFigure :: Figure
, fallingPosition :: GridPosition
, startFalling :: GridPosition
, fallingColor :: GameColor
, nextColors :: [GameColor]
, width :: Int
, height :: Int
, nextFigures :: [Figure]
, grid :: Grid
, hardness :: Hardness
, score :: Integer
, frapsCounter :: Integer
, isPause :: Bool
, gameOver :: Bool
, pressedDown :: Bool
}
-- | Initial state of the game
initialState :: ReaderT AppConfig IO TetrisGame
initialState = do
cfg <- ask
gen <- liftIO getStdGen
let (f, fs) = case randomFigures gen of
(x : xs) -> (x, xs)
[] -> error "initialState: randomFigures returned empty list"
let (c, cs) = case randoms gen of
(x : xs) -> (x, xs)
[] -> error "initialState: randoms returned empty list"
let startPos = startPosition cfg
return $
Game
f
startPos
startPos
c
cs
(fst $ gridSize $ cfg)
(snd $ gridSize $ cfg)
fs
Map.empty
minBound
0
0
False
False
False
-- | Real position in Grid
getRealCoords :: Figure -> GridPosition -> [Block]
getRealCoords (Figure _ _ bs) curPos = map (sumPair curPos) bs
-- | List of random figures
randomFigures :: (RandomGen g) => g -> [Figure]
randomFigures gen = zipWith getFigures (randoms gen) (randoms gen)
-- | Sets the currently falling figure from nextFigures
nextFigureGame :: TetrisGame -> TetrisGame
nextFigureGame g@Game{..}
| checkingGO = g{gameOver = True}
| (nf : nfs) <- nextFigures
, (nc : ncs) <- nextColors =
updateHardness $
updateScore $
Game
nf
startFalling
startFalling
nc
ncs
width
height
nfs
updateGrid
hardness
score
frapsCounter
isPause
checkingGO
pressedDown
| otherwise = g{gameOver = True}
where
updateScore gnew = gnew{score = score + (getScore $ countOfBurns g gnew)}
countOfBurns (length . getGridAsList -> countOld) (length . getGridAsList -> countNew) = div ((countOld + 4) - countNew) width
-- Returns score based on count of burned lines.
getScore :: Int -> Integer
getScore 0 = 0
getScore 1 = 100
getScore 2 = 300
getScore 3 = 700
getScore 4 = 1500
getScore _ = 0
updateHardness :: TetrisGame -> TetrisGame
updateHardness game@(Game _ _ _ _ _ _ _ _ _ _ scr _ _ _ _) = game{hardness = nextHardness scr}
nextHardness :: Integer -> Hardness
nextHardness scr
| scr < beginnerBound = Noob
| scr < averageBound = Beginner
| scr < skilledBound = Average
| scr < masterfulBound = Skilled
| scr < insaneBound = Masterful
| scr < godlikeBound = Insane
| otherwise = Godlike
updateGrid =
burnFullLines $
foldl
(\gr bl -> (Map.insert (bl, fallingColor) () gr))
grid
(getRealCoords fallingFigure fallingPosition)
checkingGO = any (\(_, y) -> y >= height) (getRealCoords fallingFigure fallingPosition)
burnFullLines =
listToGrid
. concat
. zipWith
(\num list -> map (\((x, _), color) -> ((x, num), color)) list)
[0, 1 ..]
. filter ((/= width) . length)
. groupBy (\((_, y1), _) ((_, y2), _) -> y1 == y2)
. sortBy (comparing (snd . fst))
. gridToList
-- | Shifts left a figure if able to
shiftLeftFigure :: TetrisGame -> TetrisGame
shiftLeftFigure curTetrisGame@(Game ff (shiftLeft -> fpos) spos fc nc w h fs grid hrd scr fcnt isPs go km)
| goodCoords grid w h (getRealCoords ff fpos) =
Game ff fpos spos fc nc w h fs grid hrd scr fcnt isPs go km
| otherwise = curTetrisGame
-- | Shifts right a figure if able to
shiftRightFigure :: TetrisGame -> TetrisGame
shiftRightFigure curTetrisGame@(Game ff (shiftRight -> fpos) spos fc nc w h fs grid hrd scr fcnt isPs go km)
| goodCoords grid w h (getRealCoords ff fpos) =
Game ff fpos spos fc nc w h fs grid hrd scr fcnt isPs go km
| otherwise = curTetrisGame
-- | Shifts down a figure if able to
shiftDownFigure :: TetrisGame -> TetrisGame
shiftDownFigure curTetrisGame@(Game ff (shiftDown -> fpos) spos fc nc w h fs grid hrd scr fcnt isPs go km)
| goodCoords grid w h (getRealCoords ff fpos) =
Game ff fpos spos fc nc w h fs grid hrd scr fcnt isPs go km
| otherwise = nextFigureGame curTetrisGame
-- | Rotates a figure if able to
rotateFigure :: TetrisGame -> TetrisGame
rotateFigure curTetrisGame@(Game (rotate -> ff) fpos spos fc nc w h fs grid hrd scr fcnt isPs go km)
| goodCoords grid w h (getRealCoords ff fpos) =
Game ff fpos spos fc nc w h fs grid hrd scr fcnt isPs go km
| otherwise = curTetrisGame
-- | Instantly drops a figure to the bottom
dropFigure :: TetrisGame -> TetrisGame
dropFigure game = dropUntilCollision game
where
dropUntilCollision currentGame@(Game ff (shiftDown -> fpos) spos fc nc w h fs grid hrd scr fcnt isPs go km) =
if goodCoords grid w h (getRealCoords ff fpos)
then
dropUntilCollision (Game ff fpos spos fc nc w h fs grid hrd scr fcnt isPs go km)
else nextFigureGame currentGame -- Can't move down anymore, lock the piece
resetGame :: TetrisGame -> TetrisGame
resetGame Game{..}
| (_ : nf : nfs) <- nextFigures
, (_ : nc : ncs) <- nextColors =
Game
nf
startFalling
startFalling
nc
ncs
width
height
nfs
Map.empty
minBound
0
0
False
False
pressedDown
| otherwise = error "resetGame: insufficient figures or colors"
pressedKeyDown :: TetrisGame -> Bool
pressedKeyDown Game{..} = pressedDown
-- | Checks that the point belongs to the Grid and that it is free
goodCoords :: Grid -> Int -> Int -> [Block] -> Bool
goodCoords grid w _h = all goodCoord
where
goodCoord pos@(x, y) =
x >= 0
&& x < w
&& y >= 0
&& ((== 0) . (length) . (filter (\(grPos, _) -> grPos == pos)) . gridToList) grid
-- | Returns next figure
getNextFigure :: TetrisGame -> Figure
getNextFigure (nextFigures -> (f : _)) = f
getNextFigure _ = error "getNextFigure: no next figure available"
getGridAsList :: TetrisGame -> [(GridPosition, GameColor)]
getGridAsList (grid -> gr) = gridToList gr
listToGrid :: [(GridPosition, GameColor)] -> Grid
listToGrid = Map.fromList . (`zip` repeat ())
gridToList :: Grid -> [(GridPosition, GameColor)]
gridToList = Map.keys
shiftRight :: GridPosition -> GridPosition
shiftRight = sumPair (1, 0)
shiftLeft :: GridPosition -> GridPosition
shiftLeft = sumPair (-1, 0)
shiftDown :: GridPosition -> GridPosition
shiftDown = sumPair (0, -1)