module Main where
import Control.Lens
import Game.HSnake.AI
import Game.HSnake.Basic
import Game.HSnake.Game
import Game.HSnake.Graphics
import Game.HSnake.Player
import Game.HSnake.Snake
import Control.Monad
import Control.Monad.State
import Control.Monad.Reader
import Data.List
import Foreign.C.Types (CInt(..))
import qualified SDL
import qualified SDL.Font as SDLF
data MessageDir = MessageDir {
quitMessage :: Bool
}
data AppConfig = AppConfig {
window :: SDL.Window,
renderer :: SDL.Renderer,
gameScreen :: SDL.Surface,
messageDir :: MessageDir
}
type AppState = StateT GameState IO
type AppEnv = ReaderT AppConfig AppState
initEnv :: IO (AppConfig, GameState)
initEnv = do
let windowSize = SDL.V2 (fromIntegral screenWidth :: CInt) (fromIntegral screenHeight :: CInt)
let p = SDL.defaultWindow {SDL.windowInitialSize = windowSize}
window <- SDL.createWindow "Haskell Snake" p
SDL.showWindow window
let screenSize = SDL.V2 (fromIntegral gameScreenWidth :: CInt) (fromIntegral gameScreenHeight :: CInt)
gameScreen <- SDL.createRGBSurface screenSize SDL.RGB24
-- Renderer renders to gameScreen, not to window
-- We have text additionally to render to other parts of the window
-- If we rendered directly to window and then rendered text, this
-- would result in flickering
renderer <- SDL.createSoftwareRenderer gameScreen
let msgDir = MessageDir False
applePos <- getRandomApple initialSnakePosition
tick <- SDL.ticks
return (AppConfig window renderer gameScreen msgDir,
applePosition .~ applePos $
lastTick .~ tick $ initialGameState) -- timerState
runLoop :: AppConfig -> GameState -> IO ()
runLoop = evalStateT . runReaderT loop
handleInputHuman :: GameState -> (Maybe SDL.Event) -> ReaderT AppConfig AppState ()
handleInputHuman gs (Just (SDL.Event _ (SDL.KeyboardEvent k))) =
handleInputKeyboard gs k
handleInputHuman _ _ = return ()
handleInputKeyboard :: MonadState GameState m => GameState -> SDL.KeyboardEventData -> m ()
handleInputKeyboard gs (SDL.KeyboardEventData _ SDL.Pressed False keysym) =
case SDL.keysymKeycode keysym of
SDL.KeycodeDown -> do
put $ players .~ moveHuman South $ gs
SDL.KeycodeUp -> do
put $ players .~ moveHuman North $ gs
SDL.KeycodeLeft -> do
put $ players .~ moveHuman West $ gs
SDL.KeycodeRight -> do
put $ players .~ moveHuman East $ gs
_ -> return ()
where
moveHuman dir = mapToIndices (\pl -> setNextPlayerDirection pl dir) (gs^.players) humanPlayerIndices
where
humanPlayerIndices = findIndices isHuman (gs^.players)
handleInputKeyboard _ _ = return ()
-- | poll for event until it is SDL_QUIT or NoEvent
whileEvents :: MonadIO m => (Maybe SDL.Event -> m ()) -> m Bool
whileEvents act = do
event <- liftIO SDL.pollEvent
case event of
Just (SDL.Event _ SDL.QuitEvent) -> return True
_ -> do
act event
return False
loop :: AppEnv ()
loop = do
gameState <- get
quit <- whileEvents $ handleInputHuman gameState
computeAINextMoves
gameState <- get
let apple = gameState^.applePosition
let ps = gameState^.players
if checkPlayersCollision ps
then error "Game over"
else return ()
tick <- liftIO SDL.ticks
let tickDifference = fromIntegral tick - fromIntegral (gameState^.lastTick)
if (tickDifference > speedFromLevel (fromIntegral $ gameState^.level))
then
do
-- Move the snake first, only then check for apple eating.
-- Otherwise we would eat the apple, then immediately move
-- the snake, which is wrong.
updatePlayerDirections
movePlayers
gameState <- get
put $ lastTick .~ tick $ gameState
drawGame
let appleEatersInd = findIndices (playerEatsApple apple) ps
if (length appleEatersInd > 0)
then do
newApplePosition <- liftIO $ getRandomApple (totalPlayersPosition ps)
-- increase snake's length
increaseAppleEatersLength appleEatersInd
gameState <- get
put $ applePosition .~ newApplePosition $ gameState
else return ()
else return ()
if (shouldIncreaseLevel ps)
then do
newApplePosition <- liftIO $ getRandomApple (totalPlayersPosition ps)
-- start new level after apple is eaten
put $ applePosition .~ newApplePosition $
level .~ (gameState^.level) + 1 $ initialGameState
else return ()
unless quit loop
loop
computeAINextMoves :: ReaderT AppConfig AppState ()
computeAINextMoves = do
gameState <- get
put $ players .~ movedAI gameState $ gameState
where
aiIndices gs = findIndices (not . isHuman) (gs^.players)
movedAI gs = mapToIndices (\pl -> setNextPlayerDirection pl (computeAIPlayerMove pl gs)) (gs^.players) (aiIndices gs)
updatePlayerDirections :: ReaderT AppConfig AppState ()
updatePlayerDirections = do
gameState <- get
put $ players .~ map updatePlayerDirection (gameState^.players) $ gameState
movePlayers :: ReaderT AppConfig AppState ()
movePlayers = do
gameState <- get
put $ players .~ map movePlayer (gameState^.players) $ gameState
increaseAppleEatersLength :: [Int] -> ReaderT AppConfig AppState ()
increaseAppleEatersLength appleEatersIndices = do
gameState <- get
put $ players .~ mapToIndices increasePlayerSnakeLength (gameState^.players) appleEatersIndices $ gameState
drawGame :: ReaderT AppConfig AppState ()
drawGame = do
window <- window `liftM` ask
renderer <- renderer `liftM` ask
gameScreen <- gameScreen `liftM` ask
gameState <- get
liftIO $ do
-- clear window surface first
screen <- SDL.getWindowSurface window
clearSurface screen
paintBoard renderer
paintApple renderer (gameState^.applePosition)
mapM_ (paintPlayer renderer) (gameState^.players)
SDL.present renderer
showGameMessages window gameState
_ <- SDL.surfaceBlit gameScreen Nothing screen Nothing
SDL.updateWindowSurface window
SDL.freeSurface screen
return ()
main :: IO ()
main = do
SDL.initializeAll
SDLF.initialize
(env, snakeState) <- initEnv
runLoop env snakeState