packages feed

chessica-0.1.0.0: src/Chess/Board.hs

-----------------------------------------------------------------------------
-- |
-- Module      : Chess.Board
-- Copyright   : (c) Michael Szvetits, 2023
-- License     : BSD-3-Clause (see the file LICENSE)
-- Maintainer  : typedbyte@qualified.name
-- Stability   : stable
-- Portability : portable
--
-- Types and functions to create, manipulate and query chess boards.
-----------------------------------------------------------------------------
module Chess.Board
  ( -- * Representing Boards
    Board
  , empty
    -- * Manipulating Boards
  , place
  , replace
  , remove
    -- * Querying Boards
  , lookup
  , isOccupied
  , pieces
  , piecesOf
  -- * Re-exports
  , module Chess.Board.Direction
  , module Chess.Board.PlacedPiece
  , module Chess.Board.Position
  ) where

-- base
import Control.Applicative qualified as A
import Data.Maybe          (isJust)
import Prelude      hiding (lookup)

-- containers
import Data.Map.Strict qualified as M

import Chess.Board.Direction
import Chess.Board.PlacedPiece
import Chess.Board.Position
import Chess.Color             (Color)
import Chess.Exception         (ChessException, fieldOccupied, pieceMissing, unexpectedPiece)
import Chess.Piece             (Piece(color), same)
import Chess.Some              (Some(Some))

-- | Represents a chess board.
newtype Board = Board { pieces :: M.Map Position (Some Piece) }

-- | An empty chess board, i.e. a board with no placed pieces.
empty :: Board
empty = Board M.empty

-- | Introduces a new chess piece to the chess board at a specific position.
place
  :: Position
  -- ^ The position of the newly introduced chess piece.
  -> Piece t
  -- ^ The newly introduced chess piece.
  -> Board
  -- ^ The original chess board.
  -> Either ChessException Board
  -- ^ The new chess board, if the specified position has not been occupied.
place position piece board =
  let
    (maybeOldPiece, newPieces) =
      M.insertLookupWithKey
        ( \_key new _old -> new )
        ( position )
        ( Some piece )
        ( board.pieces )
  in
    case maybeOldPiece of
      Just (Some oldPiece) ->
        Left $ fieldOccupied (PlacedPiece position oldPiece)
      Nothing ->
        Right $ Board newPieces

-- | Introduces a new chess piece to the chess board at a specific position.
replace
  :: Position
  -- ^ The position of the newly introduced chess piece.
  -> Piece t
  -- ^ The newly introduced chess piece.
  -> Board
  -- ^ The original chess board.
  -> Board
  -- ^ The new chess board. If the specified position has been occupied, the piece is replaced.
replace position piece (Board oldPieces) =
  Board (M.insert position (Some piece) oldPieces)

-- | Removes a chess piece from the chess board at a given position.
remove
  :: PlacedPiece t
  -- ^ The position of the chess piece to be removed.
  -> Board
  -- ^ The original chess board.
  -> Either ChessException Board
  -- ^ The new chess board, if the specified position was indeed occupied by the piece.
remove placed@PlacedPiece{position, piece} board =
  let
    (maybeOldPiece, newPieces) =
      M.updateLookupWithKey
        ( \_ _ -> Nothing )
        ( position )
        ( board.pieces )
  in
    case maybeOldPiece of
      Nothing -> Left $ pieceMissing placed
      Just (Some oldPiece)
        | same oldPiece piece -> Right $ Board newPieces
        | otherwise           -> Left $ unexpectedPiece (PlacedPiece position oldPiece)

-- | Gets a chess piece at a specific position of the chess board.
lookup
  :: A.Alternative f
  => Position
  -- ^ The position to look for a chess piece.
  -> Board
  -- ^ The board used for the lookup.
  -> f (Some PlacedPiece)
  -- ^ The chess piece, if the specified position was indeed occupied by it.
lookup position board =
  case M.lookup position board.pieces of
    Just (Some piece) -> pure $ placedPiece position piece
    Nothing           -> A.empty

-- | Checks if a specified position of the chess board is occupied by a chess piece.
isOccupied
  :: Position -- ^ The position to be checked for occupation.
  -> Board    -- ^ The board whose position is checked for occupation.
  -> Bool     -- ^ True if the specified position is occupied, or else false.
isOccupied position =
  isJust . lookup position

-- | Gets all chess pieces that are currently on the chess board.
pieces :: Board -> [Some PlacedPiece]
pieces
  = fmap (\(position, Some piece) -> placedPiece position piece)
  . M.assocs
  . (.pieces)

-- | Gets all chess pieces of a given color that are currently on the chess board.
piecesOf :: Color -> Board -> [Some PlacedPiece]
piecesOf color
  = fmap (\(position, Some piece) -> placedPiece position piece)
  . filter (\(_, Some piece) -> piece.color == color)
  . M.assocs
  . (.pieces)