packages feed

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

{-# LANGUAGE DataKinds       #-}
{-# LANGUAGE LambdaCase      #-}
{-# LANGUAGE OverloadedLists #-}
-----------------------------------------------------------------------------
-- |
-- Module      : Chess.Rulebook.Standard.Movement.Pawn
-- 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 pawns, according to the standard
-- rulebook.
-----------------------------------------------------------------------------
module Chess.Rulebook.Standard.Movement.Pawn
  ( movements
  , oneStep
  , twoSteps
  , capture
  , enPassant
  ) where

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

import Chess.Board                    (Board, isOccupied, lookup)
import Chess.Board.Direction          (Direction, up, down)
import Chess.Board.PlacedPiece        (PlacedPiece(..))
import Chess.Board.Position           (Position(..), boundedPosition, offset)
import Chess.Color                    (Color(..))
import Chess.Game                     (Game(..), Update(command))
import Chess.Game.Command             (Command(..), move, promote, destroy)
import Chess.Piece                    (Piece(Piece), PieceType(..), PieceType'(Pawn'), isOfType)
import Chess.Rulebook.Standard.Threat (threats)
import Chess.Some                     (Some(Some))

-- | Determines all possible movements (including captures, promotions and en
-- passant) for a given pawn.
movements :: PlacedPiece Pawn' -> Game -> [Command]
movements pawn game@Game{board}
    = oneStep pawn board
  <|> twoSteps pawn board
  <|> capture pawn board
  <|> maybeToList (enPassant pawn game)

-- | Determines all possible one-step forward movements (including promotions)
-- for a given pawn.
oneStep :: PlacedPiece Pawn' -> Board -> [Command]
oneStep pawn board = do
  let pawnDirection = direction pawn
  forward <- offset pawnDirection pawn.position
  guard $ not $ isOccupied forward board
  maybePromote forward pawn (move forward pawn)

-- | Determines all possible two-step forward movements for a given pawn.
twoSteps :: PlacedPiece Pawn' -> Board -> [Command]
twoSteps pawn board = do
  guard $
    case pawn.color of
      White -> pawn.position.row == 1
      Black -> pawn.position.row == 6
  let pawnDirection = direction pawn
  oneForward <- offset pawnDirection pawn.position
  guard $ not $ isOccupied oneForward board
  twoForward <- offset pawnDirection oneForward
  guard $ not $ isOccupied twoForward board
  pure (move twoForward pawn)

-- | Determines all possible capture movements (including promotions, excluding
-- en passant) for a given pawn.
capture :: PlacedPiece Pawn' -> Board -> [Command]
capture pawn board = do
  target     <- threats pawn board
  Some enemy <- lookup target board
  maybePromote target pawn [destroy enemy, move target pawn]

-- | Determines the en passant movement for a given pawn, if possible.
enPassant :: PlacedPiece Pawn' -> Game -> Maybe Command
enPassant pawn game =
  let
    expectedRow =
      case pawn.color of
        White -> 4
        Black -> 3
    lastMove = \case
      Move dst (Some (PlacedPiece src movedPiece)) ->
        Just (src, dst, Some movedPiece)
      Sequence cmd _ ->
        lastMove cmd
      Atomic cmd ->
        lastMove cmd
      _ ->
        Nothing
  in do
    update <- game.lastUpdate
    (src, dst, Some enemy) <- lastMove update.command
    guard
      -- the pawn must be in the correct row
       $ pawn.position.row == expectedRow
      -- the previous move of the enemy must include a pawn
      && isOfType Pawn enemy
      -- the enemy's pawn must have ended its move next to the pawn
      && pawn.position.row == dst.row
      && abs (pawn.position.column - dst.column) == 1
      -- the enemy's pawn must have moved two fields
      && abs (src.row - dst.row) == 2
    let
      newPosition =
        boundedPosition
          ( (src.row + dst.row) `div` 2 )
          ( dst.column )
    pure [destroy (PlacedPiece dst enemy), move newPosition pawn]

promotions :: PlacedPiece Pawn' -> [Command]
promotions pawn = do
  guard $
    ( pawn.color == White && pawn.position.row == 7 ) ||
    ( pawn.color == Black && pawn.position.row == 0 )
  [  promote pawn (Piece Queen  pawn.color)
   , promote pawn (Piece Rook   pawn.color)
   , promote pawn (Piece Bishop pawn.color)
   , promote pawn (Piece Knight pawn.color) ]

direction :: PlacedPiece Pawn' -> Direction
direction pawn =
  case pawn.color of
    White -> up
    Black -> down

maybePromote :: Position -> PlacedPiece Pawn' -> Command -> [Command]
maybePromote position pawn moveCmd =
  case promotions (pawn {position = position}) of
    [] -> [moveCmd]
    ps -> [[moveCmd, p] | p <- ps]