hexmino-0.1.0.0: src/Game.hs
module Game where
import qualified Options as Opts
import qualified Selection as Sel
import qualified Table
import qualified Widgets as Wid
import qualified Score
import Data.Function (on)
import qualified System.Random as Rand
import qualified System.Exit as Exit
import qualified Graphics.Gloss as Gloss
import qualified Graphics.Gloss.Data.Color as Color
import qualified Graphics.Gloss.Data.Picture as Pict
import qualified Graphics.Gloss.Data.Point.Arithmetic as PArith
import Graphics.Gloss.Interface.IO.Game (playIO, Event(..), Key(..), SpecialKey(..), KeyState(..), MouseButton(..), Modifiers(..))
data State = State {
status :: GameStatus,
score :: Score.Score,
topTen :: Score.Leaderboard,
gameTable :: Table.Table,
selection :: Maybe Sel.Selection,
winScale :: Float
}
data GameStatus = SplashScreen | Running | Complete | Info deriving (Show, Eq)
-- scaling from this; NOTE: Game keeps track of window scaling, everything else will assume no scaling
standardSize :: (Int, Int)
standardSize = (700, 480)
scalePoint :: Pict.Point -> Float -> Pict.Point
scalePoint point factor = (1/factor) PArith.* point
-- entry point
run :: Opts.Options -> IO ()
run opts = do
gen <- Rand.getStdGen
state <- initialState gen
playIO window background (Opts.fps opts) state render handleEvent step
-- gloss-starting functions
window :: Gloss.Display
window = Gloss.InWindow "Hexmino" standardSize (50,50)
background :: Color.Color
background = Color.white
initialState :: Rand.StdGen -> IO State
initialState gen = do
sc <- Score.readPlayer
leaders <- Score.readTopTen
return $ State {
status = SplashScreen,
score = sc,
topTen = leaders,
gameTable = Table.empty gen,
selection = Nothing,
winScale = 1
}
-- rendering functions
render :: State -> IO Pict.Picture
render st = returnScaled st . (Table.render (gameTable st) :) $ case status st of
SplashScreen -> [Wid.renderBanner, Wid.renderGameSelector $ score st, Wid.renderTopTen $ topTen st, Wid.renderInfoButton]
Running -> [Wid.renderTime $ score st, renderSelection st, Wid.renderCloseGame]
Complete -> [Wid.renderCompleted $ score st, Wid.renderNameSelector $ score st, Wid.renderTopTen $ topTen st]
Info -> [Wid.renderBanner, Wid.renderInfo, Wid.renderTopTen $ topTen st]
returnScaled :: State -> [Pict.Picture] -> IO Pict.Picture
returnScaled st = return . Pict.scale (winScale st) (winScale st) . Pict.pictures
renderSelection :: State -> Pict.Picture
renderSelection st = case selection st of
Just sel -> Sel.render sel
_ -> Pict.Blank
-- event handling / state changing
handleEvent :: Event -> State -> IO State
handleEvent (EventKey (SpecialKey KeyEsc) Up _ _) _ = Exit.exitSuccess
handleEvent (EventResize newSize) state = return $ changeScale newSize state
handleEvent ev st = let se = scaledEvent ev (winScale st) in case status st of
SplashScreen -> handleSplash se st
Running -> handleRunning se st
Complete -> handleComplete se st
Info -> handleInfo se st
scaledEvent :: Event -> Float -> Event
scaledEvent ev factor = case ev of
EventKey k ks mods point -> EventKey k ks mods $ scalePoint point factor
EventMotion point -> EventMotion $ scalePoint point factor
_ -> ev
changeScale :: (Int, Int) -> State -> State
changeScale (w, h) state = state {winScale = newScale}
where
(dw, dh) = standardSize
newScale = min (floatDiv w dw) (floatDiv h dh)
handleSplash :: Event -> State -> IO State
handleSplash (EventKey k ks _ pos) st = case (k, ks) of
(SpecialKey KeyEnter, Up) -> return $ newGame st
(SpecialKey KeyRight, Up) -> return $ st {score = Score.toNextLevel $ score st}
(SpecialKey KeyLeft, Up) -> return $ st {score = Score.toPreviousLevel $ score st}
(Char 'i', Up) -> return $ st {status = Info}
(MouseButton LeftButton, Up) -> handleWidgetClick pos st
_ -> return st
handleSplash _ st = return st
handleInfo :: Event -> State -> IO State
handleInfo (EventKey k ks _ pos) st = case (k, ks) of
(SpecialKey KeyEnter, Up) -> return $ st {status = SplashScreen}
(MouseButton LeftButton, Up) -> handleWidgetClick pos st
_ -> return st
handleInfo _ st = return st
handleComplete :: Event -> State -> IO State
handleComplete (EventKey k ks m pos) st = case (k, ks) of
(SpecialKey KeyEnter, Up) -> submitScore st
(SpecialKey KeyDelete, Up) -> return $ st {score = Score.delFromName $ score st}
(Char '\x0008', Up) -> return $ st {score = Score.delFromName $ score st}
(Char c, Up) -> return $ st {score = Score.addToName c $ score st}
(MouseButton LeftButton, Up) -> handleWidgetClick pos st
_ -> return st
handleComplete _ st = return st
handleWidgetClick :: Pict.Point -> State -> IO State
handleWidgetClick pos st = case Wid.findClicked pos . buttonsInStatus $ status st of
Just name -> handleEvent (EventKey (keyFromWidget name) Up noMod (0,0)) st
_ -> return st
buttonsInStatus :: GameStatus -> [Wid.Name]
buttonsInStatus status = case status of
SplashScreen -> [Wid.NewGame, Wid.LeftArrow, Wid.RightArrow, Wid.Info]
Running -> [Wid.CloseGame]
Complete -> [Wid.Delete, Wid.Submit]
Info -> [Wid.CloseInfo]
keyFromWidget :: Wid.Name -> Key
keyFromWidget name = case name of
Wid.NewGame -> SpecialKey KeyEnter
Wid.LeftArrow -> SpecialKey KeyLeft
Wid.RightArrow -> SpecialKey KeyRight
Wid.Delete -> SpecialKey KeyDelete
Wid.Submit -> SpecialKey KeyEnter
Wid.CloseInfo -> SpecialKey KeyEnter
Wid.CloseGame -> Char '\x0008'
Wid.Info -> Char 'i'
_ -> SpecialKey KeyEsc -- something went wrong here
submitScore :: State -> IO State
submitScore st = do
leaders <- Score.submit $ score st
return $ st {status = SplashScreen, topTen = leaders}
handleRunning :: Event -> State -> IO State
handleRunning (EventKey k ks _ pos) = case (k, ks) of
(SpecialKey KeySpace, Up) -> return . rotateSelection
(Char '\x0008', Up) -> return . backToSpashScreen
(MouseButton LeftButton, Down) -> return . grabSelection pos
(MouseButton LeftButton, Up) -> handleRunningClick pos
_ -> return
handleRunning (EventMotion pos) = return . dragSelection pos
handleRunning _ = return
backToSpashScreen :: State -> State
backToSpashScreen st = st {status = SplashScreen, gameTable = Table.clear $ gameTable st}
handleRunningClick :: Pict.Point -> State -> IO State
handleRunningClick pos st = case selection st of
Just sel -> return $ dropSelection pos st
_ -> handleWidgetClick pos st
rotateSelection :: State -> State
rotateSelection st = st {selection = Sel.rotate <$> selection st}
dragSelection :: Pict.Point -> State -> State
dragSelection pos st = st {selection = Sel.moveTo pos <$> selection st}
grabSelection :: Pict.Point -> State -> State
grabSelection pos st = st {gameTable = newTable, selection = Sel.make <$> newTile}
where (newTable, newTile) = Table.grab pos $ gameTable st
dropSelection :: Pict.Point -> State -> State
dropSelection point st = case selection st of
Just sel -> checkCompleted $ st {gameTable = Table.putTile (Sel.tile sel) point $ gameTable st, selection = Nothing}
_ -> st
newGame :: State -> State
newGame state = state {
status = Running,
gameTable = Table.newGame (Score.levelNum $ score state) $ gameTable state,
score = Score.clearTime $ score state
}
checkCompleted :: State -> State
checkCompleted state
| Table.isCompleted table = state {status = Complete, gameTable = Table.clear table}
| otherwise = state
where table = gameTable state
-- stepping
step :: Float -> State -> IO State
step secs st = return $ st {
gameTable = Table.step secs $ gameTable st,
selection = Sel.step secs <$> selection st,
score = stepTime $ score st
}
where stepTime = if status st == Running then Score.step secs else id
-- utils
floatDiv :: Int -> Int -> Float
floatDiv = (/) `on` fromIntegral
noMod :: Modifiers
noMod = Modifiers {shift = Up, ctrl = Up, alt = Up}