packages feed

chessica-0.1.0.0: src/Chess/Rulebook/Standard/Movement/King.hs

{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE LambdaCase      #-}
{-# LANGUAGE OverloadedLists #-}
-----------------------------------------------------------------------------
-- |
-- Module      : Chess.Rulebook.Standard.Movement.King
-- Copyright   : (c) Michael Szvetits, 2023
-- License     : BSD-3-Clause (see the file LICENSE)
-- Maintainer  : typedbyte@qualified.name
-- Stability   : stable
-- Portability : portable
--
-- Implementation of the movement rule for kings, according to the standard
-- rulebook.
-----------------------------------------------------------------------------
module Chess.Rulebook.Standard.Movement.King where

-- base
import Control.Applicative ((<|>))
import Control.Monad       (guard)
import Prelude hiding      (lookup)

import Chess.Board                    (isOccupied, lookup, piecesOf)
import Chess.Board.Direction          (left, right)
import Chess.Board.PlacedPiece        (PlacedPiece(..), assumeType)
import Chess.Board.Position           (Position(..), boundedOffset, boundedPosition)
import Chess.Color                    (Color(..), oppositeOf)
import Chess.Game                     (Game(Game, board), Update(command), history)
import Chess.Game.Command             (Command(..), move)
import Chess.Piece                    (PieceType(Rook), PieceType'(King'))
import Chess.Rulebook.Standard.Threat (threats, threatCommands)
import Chess.Some                     (Some(Some))

-- | Determines all possible movements (including captures and castlings) for a given king.
movements :: PlacedPiece King' -> Game -> [Command]
movements king game@Game{board}
    = threatCommands king board
  <|> castlings king game

-- | Determines all possible castlings for a given king.
castlings :: PlacedPiece King' -> Game -> [Command]
castlings king game@Game{board} =
  let
    expectedPosition =
      case king.color of
        White -> boundedPosition 0 4
        Black -> boundedPosition 7 4
    enemyThreats =
      concatMap
        ( \(Some enemy) -> threats enemy board )
        ( piecesOf (oppositeOf king.color) board )
    hasMoved rookPosition = \case
      Move _ (Some (PlacedPiece src _)) ->
        src == king.position || src == rookPosition
      Sequence cmd1 cmd2 ->
        hasMoved rookPosition cmd1 || hasMoved rookPosition cmd2
      Atomic cmd ->
        hasMoved rookPosition cmd
      _ ->
        False
  in do
    -- the king must be in the right position
    guard $ king.position == expectedPosition
    -- the same-colored rook must be in the right position
    let leftCorner  = boundedPosition king.position.row 0
    let rightCorner = boundedPosition king.position.row 7
    Some piece <- lookup leftCorner board <|> lookup rightCorner board
    rook       <- assumeType Rook piece
    guard $ rook.color == king.color
    -- get the fields between king and rook
    let direction = if king.position.column > rook.position.column then left else right
    let oneNext   = boundedOffset direction king.position
    let twoNext   = boundedOffset direction oneNext
    guard
      -- the fields between king and rook must be empty
       $ not (isOccupied oneNext board)
      && not (isOccupied twoNext board)
      -- the king must not be threatened
      && not (elem king.position enemyThreats)
      -- the fields between king and rook must not be threatened
      && not (elem oneNext enemyThreats)
      && not (elem twoNext enemyThreats)
      -- king and rook must not have moved during the game
      && not
           ( any
             ( hasMoved rook.position . (.command) )
             ( history game )
           )
    pure [move twoNext king, move oneNext rook]