packages feed

battleships-1.0.0: Logic/Render.hs

----------------------------------------------------------------------------
-- |
-- Module      :  Logic.Render
-- Stability   :  experimental
-- Portability :  semi-portable
--
-- Diagrams based renderer for the game board.

{-# LANGUAGE CPP, TypeFamilies #-}
module Logic.Render
  ( renderReferenceGrid
  , renderEnemyGrid
  , renderPlayerGrid
  , renderLegend
  , renderTimedLegend
  , renderGrid
  , LegendIcon (..)
  , TimedLegendIcon (..)
  , BattleDia
  ) where

import           Prelude
import           Data.Colour.SRGB
import           Data.Foldable (fold, foldMap)
import           Data.Function (on)
import           Data.Array
import qualified Data.List as L
import qualified Data.Map as Map
import           Diagrams.Prelude hiding (Time)
import           Diagrams.Backend.SVG
import           Diagrams.TwoD.Text
import           Logic.Game
import           Logic.Types
import           Logic.Util

-- | Specialised diagram type. Backend is `SVG`, vector space is `R2` and
-- annotations `[Pos]`.
type BattleDia = QDiagram SVG R2 [Pos]

-- | Arrows displayed on the bow and stern of undamaged ships.
data MoveArrow = ArrowRight | ArrowUp | ArrowLeft | ArrowDown deriving Enum

-- | legend icon to render
data LegendIcon 
  = LIShipWithArrow 
  | LIShipMovable
  | LIShipImmovable
  | LIShipHit
  | LIShipSunk
  | LIFogOfWar
  | LIWater
  | LILastShot
  deriving (Show, Enum, Bounded)

-- | time-dependent legend icon to render
data TimedLegendIcon
  = TLIWater Int
  | TLIMarker Int
  deriving (Show)

-------------------------------------------------------------------------------
-- * Legend Rendering
-------------------------------------------------------------------------------

renderLegend :: LegendIcon -> Diagram SVG R2
renderLegend icon = case icon of
  LIShipWithArrow -> renderArrow ArrowRight <> movableSquare
  LIShipMovable   -> movableSquare
  LIShipImmovable -> shipSquare
  LIShipHit       -> marker # lc markerHitColor <> shipSquare
  LIShipSunk      -> (rect (2 * cellSize) cellSize # innerGridLineStyle 
                     <> vrule cellSize # innerGridLineStyle 
                     <> roundedRect (2 * cellSize) (cellSize) (0.25 * cellSize) # lw 5 # lc shipColor
                     ) # centerXY
                     <> rect (2 * cellSize + 5) (cellSize + 5) # fc waterColor
  LIFogOfWar      -> square cellSize # fc fogColor
  LIWater         -> waterSquare
  LILastShot      -> square cellSize # alignTL <> lastShotMarker 1

renderTimedLegend :: TimedLegendIcon -> Diagram SVG R2
renderTimedLegend icon = case icon of
  TLIWater timeDiff  -> waterSquare # opacityAfter timeDiff <> fogSquare
  TLIMarker timeDiff -> marker # lc markerWaterColor # opacityAfter timeDiff <> waterSquare

-------------------------------------------------------------------------------
-- * High-Level Rendering for Grids
-------------------------------------------------------------------------------

renderReferenceGrid :: BattleDia
renderReferenceGrid = renderGrid

renderEnemyGrid :: Fleet -> TrackingList -> Rules -> Bool -> Int -> Bool -> Diagram SVG R2
renderEnemyGrid fleet shots rules noviceMode turnNumber uncoverFleet = mconcat
  [ if uncoverFleet then renderFleetHints else mempty
  , renderSunkFleet fleet
  , renderPositions $ renderMarker lastShotResultA rules turnNumber True
  , renderPositions $ (if noviceMode then renderCellAndImpossible shots sinkTimeA else renderCell) lastShotResultA rules turnNumber
  , contentSquare # fc fogColor
  ]
  where
    sinkTimeA = sinkTime fleet
    lastShotResultA = lastShotResult sinkTimeA shots turnNumber
    renderFleetHints = lc red
                     . lw 2
                     . dashing [10] 0
                     . renderFleetOutline
                     . Map.filter (not . isShipSunk)
                     $ fleet

renderPlayerGrid :: Fleet -> TrackingList -> Action -> Rules -> Int -> Diagram SVG R2
renderPlayerGrid fleet shots requiredAction rules@Rules{..} turnNumber = mconcat
    [ markLastShots
    , foldMap renderArrows . Map.filter (not . isDamaged) $ fleet
    , renderPositions $ renderMarker lastShotResultA rules turnNumber False
    , foldMap renderShip . Map.filter (not . isShipSunk) $ fleet
    , renderSunkFleet fleet
    , renderPositions $ renderCell lastShotResultA rules turnNumber
    , contentSquare # fc waterColor
    ]
  where
    sinkTimeA = sinkTime fleet
    lastShotResultA = lastShotResult sinkTimeA shots turnNumber
    markLastShots = case L.groupBy ((==) `on` shotTime) shots of
      shotsLastRound:_ 
        -> flip foldMap (zip [1::Int ..] (reverse shotsLastRound)) $ \(idx, Shot lastShotPos _ _) ->
                 lastShotMarker idx # translateToPos lastShotPos
      _ -> mempty
    renderArrows ship@Ship{shipShape = ShipShape{shipPosition=(x,y),..},..} =
      translateToPos (x,y) $ case shipOrientation of
        Horizontal -> hcat [arrowCell i | i <- [0..shipSize-1]] # alignTL
        Vertical   -> vcat [arrowCell i | i <- [0..shipSize-1]] # alignTL
      where
        arrowCell i = case requiredAction of
          ActionMove -> maybe mempty renderArrow (movementArrowAt ship i fleet) <> transparentSquare
          _          -> mempty
    renderShip ship@Ship{shipShape = ShipShape{shipPosition=(x,y),..},..} = 
      translateToPos (x,y) $ case shipOrientation of
        Horizontal -> hcat (replicate shipSize shipCell) # alignTL
        Vertical   -> vcat (replicate shipSize shipCell) # alignTL
      where
        shipCell = if isDamaged ship then shipSquare else movableSquare

renderPositions :: (Pos -> Diagram SVG R2) -> Diagram SVG R2
renderPositions f = foldMap renderPos $ range gridRange where
  renderPos p = translateToPos p $ f p

-- | Stores the result of the last shot at the given position
-- and the corresponding shot time.
lastShotResult :: Array Pos Time -> TrackingList -> Int -> Array Pos (Maybe (HitResponse, Int))
lastShotResult sinkTimeA shots turnNumber = buildArray gridRange $ \pos ->
  case L.find ((pos == ) . shotPos) shots of
    Nothing -> Nothing
    Just (Shot _ Hit hitTime) -> Just $ case unwrapTime $ (sinkTimeA ! pos) of
      Just t | hitTime <= t -> (Sunk, t) -- make sure that the hit wasn't after the sinking!
      _ -> (Hit, turnNumber) -- hit information is always up-to-date -> turnNumber instead of hitTime
    Just (Shot _ res t) -> Just (res, t)

renderMarker :: Array Pos (Maybe (HitResponse, Int)) -> Rules -> Int -> Bool -> Pos -> Diagram SVG R2
renderMarker lastShotResultA Rules{..} turnNumber showOnlyHit pos
 = alignMarker
 $ case lastShotResultA ! pos of
    Nothing            -> mempty
    Just (Water, time) -> if showOnlyHit then mempty else marker # lc markerWaterColor # opac time
    Just (Hit,   _   ) -> marker # lc markerHitColor
    Just (Sunk,  _   ) -> mempty
  where
    opac = timedOpacity rulesMove turnNumber
    alignMarker = translate (r2 (halfCellSize, -halfCellSize))

renderCellAndImpossible :: TrackingList -> Array Pos Time -> Array Pos (Maybe (HitResponse, Int)) -> Rules -> Int -> Pos -> Diagram SVG R2
renderCellAndImpossible shots sinkTimeA lastShotResultA rules@Rules{..} turnNumber pos@(x,y)
 = alignTL $ case unwrapTime impossibleInfo of
    Nothing -> renderCell lastShotResultA rules turnNumber pos
    Just time -> case lastShotResultAtCurrentPos of
      Just (_, t) | t >= time -> renderCell lastShotResultA rules turnNumber pos -- hint is older than player's actual information
      _                       -> waterSquare # opac time
  where
    opac = timedOpacity rulesMove turnNumber
    lastShotResultAtCurrentPos = lastShotResultA ! pos
    impossibleInfo = findMostRecentHit diagonalCells
      `mappend` findMostRecentlySunkShip (diagonalCells ++ adjacentCells)
      `mappend` if sawWaterHereAfterAdjacentHitNotYetSunk then Time (Just turnNumber) else mempty
    diagonalCells = filter (inRange gridRange) [(x + dx, y + dy) | dx <- [-1,1], dy <- [-1,1]]
    adjacentCells = filter (inRange gridRange) ([(x + dx, y) | dx <- [-1,1]] ++ [(x, y + dy) | dy <- [-1,1]])
    findMostRecentHit = foldMap getTimeOfHit
    getTimeOfHit p = Time $ case lastShotResultA ! p of
      Just (Hit, time) -> Just time
      _                -> Nothing
    findMostRecentlySunkShip = foldMap $ (sinkTimeA !)
    sawWaterHereAfterAdjacentHitNotYetSunk = case lastShotResultAtCurrentPos of
      Just (Water, sawWater) -> any (\p -> case L.find ((p == ) . shotPos) shots of
                                             Just (Shot _ Hit hitTime) -> case unwrapTime $ (sinkTimeA ! p) of
                                                                            Just t | hitTime <= t -> False
                                                                            _                     -> hitTime <= sawWater
                                             _                         -> False)
                                adjacentCells
      _                      -> False

renderCell :: Array Pos (Maybe (HitResponse, Int)) -> Rules -> Int -> Pos -> Diagram SVG R2
renderCell lastShotResultA Rules{..} turnNumber pos
 = alignTL
 $ case lastShotResultA ! pos of
    Nothing            -> mempty
    Just (Water, time) -> waterSquare # opac time
    Just (Hit,   time) -> shipSquare # opac time
    Just (Sunk,  time) -> waterSquare # opac time
  where
    opac = timedOpacity rulesMove turnNumber


-------------------------------------------------------------------------------
-- * Low-Level Rendering
-------------------------------------------------------------------------------

rowNumbers :: Int -> BattleDia
rowNumbers n = vcat [num i | i <- [1..n]] # value [] where
  num i = (text (show i) # fc gridColor # numberStyle) <> square cellSize

colNumbers :: Int -> BattleDia
colNumbers n = hcat [num i | i <- [0..n-1]] # value [] where
  num i = (text (strNum i) # fc gridColor # numberStyle) <> square cellSize
  strNum :: Int -> String
  strNum i = [toEnum $ fromEnum 'A' + i]

#if MIN_VERSION_diagrams_lib(0,7,0)
marker, waterSquare, shipSquare, movableSquare, fogSquare, transparentSquare
  :: (Alignable b, HasOrigin b, TrailLike b, Transformable b, Semigroup b, HasStyle b, V b ~ R2)
  => b
#else
marker, waterSquare, shipSquare, movableSquare, fogSquare, transparentSquare
  :: (Alignable b, HasOrigin b, PathLike b, Transformable b, Semigroup b, HasStyle b, V b ~ R2)
  => b
#endif
marker         = lw 3 $ drawX (markerRadius * sqrt 2) where
  drawX s      = p2 (-0.5 * s, -0.5 * s) ~~ p2 (0.5 * s, 0.5 * s)
               <> p2 (-0.5 * s, 0.5 * s) ~~ p2 (0.5 * s, -0.5 * s)
waterSquare    = square cellSize # fc waterColor
fogSquare      = square cellSize # fc fogColor
shipSquare     = rect cellSize cellSize # fc shipColor
movableSquare  = rect cellSize cellSize # fc movableColor
transparentSquare = rect cellSize cellSize # opacity 0

renderSunkFleet :: Fleet -> Diagram SVG R2
renderSunkFleet = lw 5
                . lc shipColor
                . renderFleetOutline
                . Map.filter isShipSunk

renderFleetOutline :: Fleet -> Diagram SVG R2
renderFleetOutline = fold . fmap renderShipOutline

renderShipOutline :: Ship -> Diagram SVG R2
renderShipOutline Ship {shipShape = ShipShape {shipPosition = (x,y), ..} } =
  roundedRect (w * cellSize) (h * cellSize) (0.25 * cellSize) # alignTL # translateToPos (x,y) where
    (w,h) = case shipOrientation of
        Horizontal -> (realToFrac shipSize, 1)
        Vertical   -> (1, realToFrac shipSize)

timedOpacity :: (Integral i, HasStyle c) => Bool -> i -> i -> c -> c
timedOpacity True turnNumber shotTime = opacityAfter $ turnNumber - shotTime
timedOpacity False _ _                = opacity 1

opacityAfter :: (Integral i, HasStyle c) => i -> c -> c
opacityAfter timeDiff = opacity (foot + if timeDiff < steps then (1 - foot) * f (fromIntegral timeDiff / fromIntegral steps) else 0)
  where foot = 0.25  -- the minimal opacity value used
        steps = 20   -- the number of steps in which the opacity decreases from 1 to the above value
        -- f should be a monotone function from (0,1) to (1,0); e.g., could be f x = 1 - x
        f x = let param = 0.3  -- a parameter by which to tweak the slowness of the decrease; should be between 0 and exp 1
              in (1 - log (param + x * (exp 1 - param))) / (1 - log param)

lastShotMarker :: (Renderable (Path R2) b, Renderable Text b) 
               => Int -> Diagram b R2
lastShotMarker idx = 
    (   rect (cellSize - 4) (cellSize - 4) # lc lastShotColor # lw 3
     <> text (show idx) # numberStyle # fc white
    ) # alignTL # translate (r2 (2,-2))

contentSquare :: Diagram SVG R2
contentSquare
  = rect (cellSize * realToFrac nx) (cellSize * realToFrac ny)
  # alignTL
  # translateToPos (0,0)
  where
    (nx, ny) = boardSize

movementArrowAt :: Ship -> Int -> Fleet -> Maybe MoveArrow
movementArrowAt ship@Ship{..} i fleet =
  case shipOrientation shipShape of
    Horizontal
      | i == 0                      && canMove Forward  -> Just ArrowLeft
      | i == shipSize shipShape - 1 && canMove Backward -> Just ArrowRight
    Vertical
      | i == 0                      && canMove Forward  -> Just ArrowUp
      | i == shipSize shipShape - 1 && canMove Backward -> Just ArrowDown
    _ -> Nothing
    where 
      canMove dir = isMovable dir fleet ship

renderArrow :: MoveArrow -> Diagram SVG R2
renderArrow arrType = arrowShape # rotateBy circleFraction # arrowStyle  where 
  circleFraction = fromIntegral (fromEnum arrType) / 4
  arrowShape     = fromVertices [ p2 (0, -0.8 * halfCellSize)
                                , p2 (0.8 * halfCellSize,  0)
                                , p2 (0,  0.8 * halfCellSize)]

translateToPos :: (Transformable t, V t ~ R2) => Pos -> t -> t
translateToPos (x,y) = 
  let 
    (nx, ny) = (cellSize + realToFrac x * cellSize, - cellSize - realToFrac y * cellSize)
  in translate (r2 (nx,ny))

-------------------------------------------------------------------------------
-- * Grid Rendering
-------------------------------------------------------------------------------

renderGrid :: BattleDia
renderGrid  = border <> gridLines <> labels where
  border    = rect w h # alignTL # lw 1 # lc gridColor # value []
  gridLines = innerLines <> outerLines
  xnums     = colNumbers nx # translate (r2 (cellSize, 0))
  ynums     = rowNumbers ny
  field     = vcat [cellRow y | y <- [0..nx-1]]
  cellRow y = hcat [ posSquare (x,y) | x <- [0..ny-1]]
  -- invisible square carrying the position in grid-coordinates
  posSquare p = square cellSize # value [p]
  labels    = vcat [xnums, hcat [ynums, field, ynums], xnums] # alignTL
  w = (fromIntegral nx + 2) * cellSize
  h = (fromIntegral ny + 2) * cellSize
  outerOffsets n = [cellSize, (fromIntegral n + 1) * cellSize]
  innerOffsets n = [fromIntegral i * cellSize | i <- [2..n]]
  innerLines = (xticks (h-1) (innerOffsets nx) <> yticks (w-1) (innerOffsets ny)) # innerGridLineStyle # value []
  outerLines = (xticks (h-1) (outerOffsets nx) <> yticks (w-1) (outerOffsets ny)) # outerGridLineStyle # value []
  (nx, ny)   = boardSize

#if MIN_VERSION_diagrams_lib(0,7,0)
xticks, yticks :: (Monoid a, TrailLike a, V a ~ R2) 
               => Double -> [Double] -> a
#else
xticks, yticks :: (Monoid a, PathLike a, V a ~ R2) 
               => Double -> [Double] -> a
#endif
xticks h xs = mconcat [fromVertices [p2 (x, 0), p2 (x, -h) ] | x <- xs]
yticks w ys = mconcat [fromVertices [p2 (0, -y), p2 (w, -y) ] | y <- ys]

-------------------------------------------------------------------------------
-- * Style Constants
-------------------------------------------------------------------------------

cellSize, halfCellSize, markerRadius, innerGridWidth :: Double
cellSize       = 40
halfCellSize   = cellSize / 2
markerRadius   = cellSize / 2 - 3
innerGridWidth = 1

innerGridLineStyle, outerGridLineStyle, arrowStyle :: HasStyle c => c -> c
innerGridLineStyle = lw innerGridWidth . lc gridColor . dashing [3, 3] 0
outerGridLineStyle = lw innerGridWidth . lc gridColor
arrowStyle         = lw 5 . lc gray

numberStyle :: HasStyle c => c -> c
numberStyle = fontSize 30 . font "Monospace"

gridColor, fogColor, waterColor, markerHitColor, markerWaterColor, 
  shipColor, lastShotColor, movableColor
  :: Colour Double
gridColor        = sRGB24 0xD2 0xF8 0x70
fogColor         = sRGB 0.7 0.7 0.7
waterColor       = sRGB24 0x36 0xBB 0xCE
markerHitColor   = sRGB24 0xFE 0x3F 0x44
markerWaterColor = sRGB24 0x00 0x00 0xFF
shipColor        = gray
lastShotColor    = red
movableColor     = sRGB24 0xC4 0xF8 0x3E