packages feed

battleships-1.0.0: Logic/Types.hs

----------------------------------------------------------------------------
-- |
-- Module      :  Logic.Types
-- Stability   :  experimental
-- Portability :  semi-portable
--
-- Data structures and type classes of the battleships implementation.
--
module Logic.Types
  (
  -- * Typeclasses
    AI (..)
  , HasShipShape (..)
  -- * Datatypes
  , Action (..)
  , GameState (..)
  , DifficultyLevel (..)
  , HitResponse (..)
  , Movement (..)
  , Orientation (..)
  , Time (..)
  , Player (..)
  , PlayerState (..)
  , Options (..)
  , Rules (..)
  , Ship (..)
  , ShipShape (..)
  , ShipMove (..)
  , Shot (..)
  , Turn (..)
  -- * Type Synonyms
  , Fleet
  , FleetPlacement
  , Grid
  , Pos
  , ShipID
  , TrackingList
  -- * Smart Constructors
  , newGrid
  -- * Serialization Helpers
  , getPos
  , getSmallGrid
  , putPos
  , putSmallGrid
  ) where


import           Prelude
import           Control.Applicative
import           Control.Monad
import           Control.Monad.Random
import           Control.Monad.State.Class (MonadState)
import           Data.Aeson hiding (Array, encode, decode)
import           Data.Array
import           Data.Bits
import           Data.Function (on)
import qualified Data.Map as Map
import           Data.Maybe (fromMaybe)
import           Data.Monoid
import           Data.Serialize (Serialize (..), encode, decode)
import           Data.Serialize.Get
import           Data.Serialize.Put
import           Data.Word
import           Logic.Binary
import           Yesod (PathPiece (..))

-------------------------------------------------------------------------------
-- * AI
-------------------------------------------------------------------------------

-- | Operations an artificial intelligence has to provide for playing this game.
class AI a where
  -- | Initial state and fleet position
  aiInit
    :: MonadRandom m 
    => Rules                 -- ^ game rules
    -> m (a, FleetPlacement) -- ^ initial state

  -- | Computes the next shot based on the current AI state
  aiFire
    :: (MonadRandom m, MonadState a m) 
    => m Pos

  -- | Feedback to `aiFire`
  aiResponse
    :: (MonadRandom m, MonadState a m) 
    => Pos         -- ^ target position
    -> HitResponse -- ^ result of the last shot
    -> m ()

  -- | Computes the ship movement
  aiMove
    :: (MonadRandom m, MonadState a m)
    => Fleet                        -- ^ AI's fleet including IDs
    -> TrackingList                 -- ^ enemy shots
    -> m (Maybe (ShipID, Movement)) -- ^ ship and movement, if any
  aiMove _ _ = return Nothing

-------------------------------------------------------------------------------
-- * Game State
-------------------------------------------------------------------------------

data Options = Options
  { againWhenHit   :: Bool
  , move           :: Bool
  , noviceMode     :: Bool
  , devMode        :: Bool
  , difficulty     :: DifficultyLevel
  } deriving (Show, Eq, Read)

data Rules = Rules
  { rulesAgainWhenHit   :: Bool
  , rulesMove           :: Bool
  , rulesDifficulty     :: DifficultyLevel
  , rulesMaximumTurns   :: Int
  , rulesCountdownTurns :: Int
  }

-- | Playing strength of the AI.
data DifficultyLevel
  = Easy
  | Medium
  | Hard
  deriving (Enum, Eq, Show, Read)

-- | Reponse sent to the AI after a shot.
data HitResponse
  = Water -- ^ the shot hit the water
  | Hit   -- ^ the shot hit a ship
  | Sunk  -- ^ the shot hit the last intact part of a ship
  deriving (Eq, Enum)

data Orientation 
  = Horizontal
  | Vertical
  deriving (Enum, Bounded)

-- | Represents the last time (turn number) that an event happened,
-- if any, especially a ship hit in the Ship data type.
newtype Time = Time { unwrapTime :: Maybe Int }

-- | Maximum monoid.
-- Useful because we often want to know when the last time
-- was that something happened. (last -> maximum)
instance Monoid Time where
  mempty = Time Nothing
  mappend (Time Nothing)   t = t
  mappend (Time (Just t1)) t = Time . Just $ case t of { Time Nothing -> t1 ; Time (Just t2) -> max t1 t2 }

-- | Encodes a ships state using position, size and orientation
data ShipShape = ShipShape 
  { shipPosition    :: Pos
  , shipSize        :: Int
  , shipOrientation :: Orientation
  }

-- | A ship with unique id, a shape and current damage.
data Ship = Ship
  { shipID     :: ShipID         -- ^ unique ID of ship
  , shipShape  :: ShipShape      -- ^ shape of ship (including position, orientation and size)
  , shipDamage :: Array Int Time -- ^ damage at each position
  }

instance Eq Ship where
  (==) = (==) `on` shipID

-- | Used to allow lookup functions to work with both FleetPlacement and Fleet
class HasShipShape a where
  getShipShape :: a -> ShipShape
instance HasShipShape Ship where
  getShipShape = shipShape
instance HasShipShape ShipShape where
  getShipShape = id

-- | State of the game for both players.
data GameState a = GameState
  { currentPlayer  :: PlayerState -- ^ the current player's state 
  , otherPlayer    :: PlayerState -- ^ the other player's state
  , aiState        :: a           -- ^ state of the AI
  , gameRules      :: Rules
  , noviceModeOpt  :: Bool
  , devModeOpt     :: Bool
  , expectedAction :: Action
  , turnNumber     :: Int
  }

-- | The state belonging to one player
data PlayerState = PlayerState
  { playerShots :: TrackingList -- ^ the player's shots
  , playerFleet :: Fleet        -- ^ the player's fleet
  , playerType  :: Player
  , playerMoves :: [ShipMove]   -- ^ the player's moves
  } deriving Eq

-- | type to distinguish between human and AI player
data Player
  = HumanPlayer
  | AIPlayer
  deriving (Eq, Enum)

-- | the next action expected from the human player
data Action
  = ActionFire
  | ActionMove
  deriving (Eq, Enum)

-- | keeps track of all relevant information for a fired shot
data Shot = Shot
  { shotPos    :: Pos
  , shotResult :: HitResponse
  , shotTime   :: Int
  } deriving Eq

-- | a ship movement
data ShipMove = ShipMove
  { shipMoveID        :: ShipID
  , shipMoveDirection :: Movement
  , shipMoveTime      :: Int
  } deriving (Eq, Show)

-- | A fleet is a map of ship ID's to ships
type Fleet = Map.Map ShipID Ship

-- | A fleet placement is a list of the ship shapes
type FleetPlacement = [ShipShape]

-- | A two-dimensional position stored with zero-based indices
type Pos = (Int,Int)

-- | A grid is an array indexed by positions with zero-based indices
type Grid a = Array Pos a

-- | A list of all shots. Easier to handle, because no index lookup is needed.
type TrackingList = [Shot]

-- | Used for identifying a ship.
type ShipID = Int

-- | Result of the turn with respect to the current player
data Turn = Over | Again | Next deriving Eq

-- | A ships movement
data Movement 
  = Forward  -- ^ -1 for the respective coordinate
  | Backward -- ^ +1 for the respective coordinate
  deriving (Eq, Enum, Bounded, Show)

-------------------------------------------------------------------------------
-- * Smart Constructors
-------------------------------------------------------------------------------

-- | Creates a grid, filled with one value.
newGrid :: (Int, Int) -> a -> Grid a
newGrid (w, h) a
  = array ((0, 0), (w - 1, h - 1))
    [((x, y), a) | x <- [0 .. w - 1], y <- [0 .. h - 1]]

-------------------------------------------------------------------------------
-- * Random
-------------------------------------------------------------------------------

instance Random Orientation where
  randomR (a, b) g = (toEnum r, g') where
    (r, g')        = randomR (fromEnum a, fromEnum b) g
  random           = randomR (minBound, maxBound)

instance Random Movement where
  randomR (a, b) g = (toEnum r, g') where
    (r, g')        = randomR (fromEnum a, fromEnum b) g
  random           = randomR (minBound, maxBound)

-------------------------------------------------------------------------------
-- * JSON
-------------------------------------------------------------------------------

instance FromJSON ShipShape where
   parseJSON (Object v) = curry ShipShape <$>
                          v .: "X" <*>
                          v .: "Y" <*>
                          v .: "Size" <*>
                          v .: "Orientation"
   -- A non-Object value is of the wrong type, so fail.
   parseJSON _          = mzero

instance FromJSON Orientation where
  parseJSON (Number n) = return $ toEnum $ floor n
  parseJSON _          = mzero

instance ToJSON Orientation where
  toJSON = Number . fromIntegral . fromEnum

instance ToJSON ShipShape where
  toJSON (ShipShape {..}) = object 
    [ "X"           .= fst shipPosition
    , "Y"           .= snd shipPosition
    , "Size"        .= shipSize
    , "Orientation" .= shipOrientation
    ]

-------------------------------------------------------------------------------
-- * Path Pieces
-------------------------------------------------------------------------------

instance PathPiece Options where
  fromPathPiece = impBinary >=> eitherToMaybe . decode . toStrict
  toPathPiece   = expBinary . fromStrict . encode

eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe e = case e of
  Right x -> Just x
  _       -> Nothing

-------------------------------------------------------------------------------
-- * Serialization
-------------------------------------------------------------------------------

instance Serialize a => Serialize (GameState a) where
  get = GameState <$> get <*> get <*> get <*> get <*> get <*> get <*> get <*> getIntegral8
  put GameState {..} = do
    put currentPlayer
    put otherPlayer
    put aiState
    put gameRules
    put noviceModeOpt
    put devModeOpt
    put expectedAction
    putIntegral8 turnNumber

instance Serialize PlayerState where
  get = PlayerState <$> getList8 get <*> get <*> get <*> get
  put PlayerState{..} = do
    putList8 put playerShots
    put playerFleet
    put playerType
    put playerMoves

instance Serialize Options where
  get = Options <$> get <*> get <*> get <*> get <*> get
  put Options {..} = do
    put againWhenHit
    put move
    put noviceMode
    put devMode
    put difficulty

instance Serialize Rules where
  get = Rules <$> get <*> get <*> get <*> getIntegral8 <*> getIntegral8
  put Rules {..} = do
    put rulesAgainWhenHit
    put rulesMove
    put rulesDifficulty
    putIntegral8 rulesMaximumTurns
    putIntegral8 rulesCountdownTurns

instance Serialize DifficultyLevel where
  get = getEnum8
  put = putEnum8

instance Serialize HitResponse where
  get = getEnum8
  put = putEnum8

instance Serialize Orientation where
  get = getEnum8
  put = putEnum8

instance Serialize Player where
  get = getEnum8
  put = putEnum8

instance Serialize Action where
  get = getEnum8
  put = putEnum8

instance Serialize Movement where
  get = getEnum8
  put = putEnum8

instance Serialize ShipShape where
  get = ShipShape <$> getPos <*> getIntegral8 <*> get
  put ShipShape{..} = do
    putPos shipPosition
    putIntegral8 shipSize
    put shipOrientation

instance Serialize Time where
  get = mkTime <$> getIntegral8 where
    mkTime 0xFF = Time Nothing
    mkTime i    = Time $ Just i
  put (Time t) = putIntegral8 $ fromMaybe 0xFF t

instance Serialize Ship where
  get = Ship <$> getIntegral8 <*> get <*> get
  put Ship{..} = do
    putIntegral8 shipID
    put shipShape
    put shipDamage

instance Serialize Shot where
  get = Shot <$> getPos <*> get <*> getIntegral8
  put Shot{..} = do
    putPos shotPos
    put shotResult
    putIntegral8 shotTime

instance Serialize ShipMove where
  get = ShipMove <$> getIntegral8 <*> get <*> getIntegral8
  put ShipMove{..} = do
    putIntegral8 shipMoveID
    put shipMoveDirection
    putIntegral8 shipMoveTime


-------------------------------------------------------------------------------
-- * Specific Serialization Helpers
-------------------------------------------------------------------------------

putPos :: Putter Pos
putPos (x,y) = put (hi .|. lo :: Word8) where
  hi = (fromIntegral x .&. 0x0F) `shiftL` 4
  lo =  fromIntegral y .&. 0x0F

getPos :: Get Pos
getPos = fromB <$> getWord8 where
  fromB b = ( fromIntegral $ b `shiftR` 4
            , fromIntegral $ b .&. 0x0F   )

putSmallGrid :: Putter a -> Putter (Grid a)
putSmallGrid pel grid = do
  let (_, upper) = bounds grid
  putPos upper
  mapM_ pel (elems grid)

getSmallGrid :: Get a -> Get (Grid a)
getSmallGrid gel = do
  upper <- getPos
  let count = rangeSize ((0,0), upper)
  xs <- replicateM count gel
  return $ listArray ((0,0), upper) xs