battleships-1.0.0: Handler/Play.hs
----------------------------------------------------------------------------
-- |
-- Module : Handler.Play
-- Stability : experimental
-- Portability : non-portable
--
-- Handler for playing the battleships game.
--
-- Beside the GET handler for displaying the UI, two POST handlers for performing
-- the two actions of the game (moving and shooting) are provided. After a validation
-- step, these advance the simulation and redirect the user either to the game UI
-- again, if the game is still on, or to the game ended screen.
module Handler.Play
( getPlayR
, playView
, postMoveR
, postFireR
) where
import Import
import Control.Applicative
import Control.Monad.State
import Data.List (nub)
import Data.Map ((!))
import Data.Serialize (Serialize)
import Handler.Util
import Handler.GameEnded
import Logic.Game
import Logic.GameExt
import Logic.Render
import Logic.Types
import Text.Julius (rawJS)
-------------------------------------------------------------------------------
-- * Forms
-------------------------------------------------------------------------------
-- | A form with two required double fields named "X" and "Y".
fireForm :: FormInput Handler (Double, Double)
fireForm = (,) <$> ireq doubleField "X" <*> ireq doubleField "Y"
-- | A form with two optional double fields named "X" and "Y".
moveForm :: FormInput Handler (Maybe (Double, Double))
moveForm = liftA2 (liftA2 (,)) (iopt doubleField "X") (iopt doubleField "Y")
-------------------------------------------------------------------------------
-- * Handler
-------------------------------------------------------------------------------
-- | Displays the game UI to the user.
getPlayR :: Bool -> GameStateExt -> Handler Html
getPlayR firstContact gameE = withGame gameE $ \game -> playView firstContact game gameE
playView :: Bool -> GameState a -> GameStateExt -> Handler Html
playView firstContact game@GameState{..} gameE = defaultLayout $ do
setNormalTitle
addScript $ StaticR js_jquery_js
messageRender <- getMessageRender
let remTurns = remainingTurns game `div` 2
let showAlert = firstContact && isCountdownStart game
$(widgetFile "board")
$(widgetFile "play")
-- | Handles a request to move one of the player's ships.
postMoveR :: GameStateExt -> Handler Html
postMoveR gameE = withGame gameE $ \game -> case expectedAction game of
ActionFire -> invalidMove game gameE
ActionMove -> do
mpos <- runInputPost moveForm
case mpos of
Just (x, y) -> case fieldPos (x,y) of
-- invalid click
Nothing -> invalidMove game gameE
-- valid click
Just pos ->
let humanFleet = playerFleet $ currentPlayer game
in case desiredMove pos humanFleet of
Just (ship,movement)
| isMovable movement humanFleet (humanFleet!ship)
-> performMove game (Just pos)
_ -> invalidMove game gameE
-- player skips moving
_ -> performMove game Nothing
where
performMove game pos = do
game' <- execStateT (moveHuman pos >>= executeMove) game
performAI game'
-- | Handles a request to fire at an enemy position.
postFireR :: GameStateExt -> Handler Html
postFireR gameE = withGame gameE $ \game -> do
(x,y) <- runInputPost fireForm
case fieldPos (x,y) of
-- invalid click
Nothing -> invalidMove game gameE
-- valid click
Just pos -> case expectedAction game of
ActionMove -> invalidMove game gameE
ActionFire -> do
(result, game') <- runStateT (humanTurnFire pos) game
-- evaluate outcome
case result of
Over -> gameEnded game'
-- shoot again. expectedAction has not changed
Again -> continue False game'
-- either perform AI turn or let human move
Next
| expectedAction game' == ActionMove -> continue False game'
| otherwise -> performAI game'
-- | A widget that displays a table showing which ships of the opponent are remaining.
shipsOpponentWidget :: GameState a -> Orientation -> WidgetT App IO ()
shipsOpponentWidget game orientation =
let sizes = sizesOfShips $ unsunkShips $ playerFleet $ otherPlayer game
in $(widgetFile "shipsOpponent")
-- | A widget that renders the legend in the given orientation.
legendWidget :: Orientation -> Bool -> Widget
legendWidget orientation movesAllowed = $(widgetFile "legend")
-------------------------------------------------------------------------------
-- * Redirections
-------------------------------------------------------------------------------
-- | Redirects to the game UI in case of an invalid move.
invalidMove :: GameState a -> GameStateExt -> Handler Html
invalidMove = playView False -- \_ -> redirect . PlayR False
-- | Redirects to the game ended screen.
gameEnded :: (Serialize a, AI a) => GameState a -> Handler Html
gameEnded game = expGameH game >>= gameEndedView game -- redirect . GameEndedR
-- | Redirects to the game UI in case the game is still on.
continue :: (Serialize a, AI a) => Bool -> GameState a -> Handler Html
continue firstContact game = expGameH game >>= playView firstContact game -- redirect . PlayR firstContact
-------------------------------------------------------------------------------
-- * AI
-------------------------------------------------------------------------------
-- | Performs the AI actions.
performAI :: (Serialize a, AI a) => GameState a -> Handler Html
performAI game = do
(result, game') <- liftIO $ runStateT aiTurn game
case result of
Over -> gameEnded game'
Next -> continue True game'
Again -> error "impossible. `Again` is handled by aiTurn"
-- | Determines whether the countdown should already be shown.
-- Should be shown when at most countdownTurns turns remain.
showCountdown :: GameState a -> Bool
showCountdown game = remTurns <= cdTurns where
remTurns = remainingTurns game
cdTurns = rulesCountdownTurns . gameRules $ game