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