{-|
This module exports all the functions required by 'playCheckers'
function in 'Checkers' game.
-}
module BoardCheckers(
Square(Square),
Player(Red, Black),
PieceType(Pawn, King),
Piece(Empty, Piece),
Move(March, Jump),
isJump,
getDestination,
getSource,
BoardMap(BoardMap),
Board(seekBoard, validMoves, playMove),
Game(Game),
mValidJumps,
startGame
) where
import Data.Map as Map
import Data.List as List
-- | This data type represents a 'Player'
data Player = Red -- ^ To represent 'Red' 'Player'
| Black -- ^ To represent 'Black' 'Player'
deriving (Eq) -- Equable
-- | This data type represents a 'Move'
-- Can be either a 'Jump' or a 'March'
data Move = Jump Square Square -- ^ To represent 'Jump' from 'Square' to 'Square'
| March Square Square -- ^ To represent 'March' from 'Square' to 'Square'
deriving (Eq) -- Equable
-- | Take a 'Move' and decide if it is a 'Jump' or not
isJump :: Move -> Bool
isJump (Jump _ _ ) = True
isJump (March _ _) = False
-- | Take a 'Move' and return its destination 'Square'
getDestination :: Move -> Square
getDestination (March _ s) = s
getDestination (Jump _ s) = s
-- | Take a 'Move' and return its source 'Square'
getSource :: Move -> Square
getSource (March s _) = s
getSource (Jump s _) = s
-- | This data type represents a 'Square'
-- A 'Square' consists of two ints
data Square = Square Int Int
-- | Makes the 'Square' orderable
instance Ord Square where
(Square a b) <= (Square c d) = a < c || (a == c && b <= d)
-- | Makes the 'Square' equable
instance Eq Square where
(Square a b) == (Square c d) = a == c && b == d
-- | This data type represents a 'PieceType'
-- 'PieceType' can be either Pawn or King
data PieceType = Pawn -- ^ To represent Pawn
| King -- ^ To represent King
deriving (Eq) -- Equable
-- | This data type represents 'Piece'
-- 'Piece' can either be empty or fully qualified by its 'PieceType' and 'Player'
data Piece = Empty -- ^ To represent Empty 'Piece', Empty pieces are not shown on board
| Piece Player PieceType -- ^ To represent a 'Piece' denoted by its 'PieceType' and 'Player'
deriving (Eq) -- Equable
-- | Test if the given 'Piece' is 'Red'
isRed :: Piece -> Bool
isRed Empty = False
isRed (Piece pl _) = pl == Red
-- | Test if the given 'Piece' is 'Black'
isBlack :: Piece -> Bool
isBlack Empty = False
isBlack (Piece pl _) = pl == Black
-- | This data type represents 'BoardMap'
data BoardMap = BoardMap (Map Square Piece) -- ^ 'BoardMap' is a Map from 'Square' to 'Piece'
-- | This data type represents 'Game'
data Game = Game BoardMap [Move] Player Square -- ^ A 'Game' consists of the 'BoardMap', possible moves of the current 'Player', the current 'Player' and the 'Square' which was clicked
-- | Class Board
class Board board where
-- | Return the 'Piece' that is at the 'Square' in the board
seekBoard :: board -> Square -> Piece
-- | Remove the 'Piece' that is at the 'Square' in the board and return the new board
removePiece :: board -> Square -> board
-- | Add the 'Piece' at the 'Square' in the board and return the new board
addPiece :: board -> Square -> Piece -> board
-- | Replace the current 'Piece' by the new 'Piece' at the 'Square' in the board and return the new board
replacePiece :: board -> Square -> Piece -> board
-- | Return the valid moves of the specified 'Player'
validMoves :: board -> Player -> [Move]
-- | Play the 'Move' on the board and return the new board
playMove :: board -> Move -> board
-- | Set corresponding member functions for the functions of the class
instance Board BoardMap where
seekBoard = mSeekBoard
removePiece = mRemovePiece
addPiece = mAddPiece
replacePiece = mReplacePiece
validMoves = mValidMoves
playMove = mPlayMove
-- Implementation of the Class's functions ------
-- | Return the 'Piece' that is at the 'Square' in the board
mSeekBoard :: BoardMap -> Square -> Piece
mSeekBoard (BoardMap b) sq = case Map.lookup sq b of
Just piece -> piece
Nothing -> Empty
-- | Remove the 'Piece' that is at the 'Square' in the board and return the new 'BoardMap'
mRemovePiece :: BoardMap -> Square -> BoardMap
mRemovePiece (BoardMap b) sq = BoardMap $ Map.delete sq b
-- | Add the 'Piece' at the 'Square' in the board and return the new 'BoardMap'
mAddPiece :: BoardMap -> Square -> Piece -> BoardMap
mAddPiece (BoardMap b) sq piece = BoardMap $ Map.insert sq piece b
-- | Replace the current 'Piece' by the new 'Piece' at the 'Square' in the 'BoardMap' and return the new 'BoardMap'
mReplacePiece :: BoardMap -> Square -> Piece -> BoardMap
mReplacePiece b sq piece = addPiece ( removePiece b sq) sq piece
--- Functions that start the game --------
-- | Return the initial 'BoardMap'
startGame :: BoardMap
-- Add appropriate 'Piece' at all 'Square' of the Board
startGame = addPieces (BoardMap (Map.empty)) squares
where squares = [(Square x y) | x <- [1..8], y <- [1..8], (mod x 2 == 0 && mod y 2 == 1) || (mod x 2 == 1 && mod y 2 == 0)]
-- | Takes an empty 'BoardMap' and a list of 'Square', adds appropeiate 'Piece' at the squares and returns the new 'BoardMap'
addPieces :: BoardMap -> [Square] -> BoardMap
addPieces (BoardMap m) [] = (BoardMap m)
addPieces (BoardMap m) (x:xs) = addPieces (addPiece (BoardMap m) x $ initialPieceAtSquare x) xs
-- | Takes a 'Square' and returns the 'Piece' that the board has at that 'Square' in the beginning of game
initialPieceAtSquare :: Square -> Piece
initialPieceAtSquare (Square x y) | y <= 3 = (Piece Red Pawn)
| y >= 6 = (Piece Black Pawn)
| otherwise = Empty
--- Member Functions for actual moves ------------
-- | Play the 'Move' on the board and return the new board
mPlayMove :: BoardMap -> Move -> BoardMap
-- During Jump remove the current piece at the current positon, add it at the final position and remove the piece jumped over. Check for promotion to King
mPlayMove board (Jump s1@(Square x1 y1) s2@(Square x2 y2)) | y2 /= 8 && y2 /= 1 = replacePiece (replacePiece (replacePiece board s2 (seekBoard board s1)) s1 Empty) (Square (quot (x1+x2) 2) (quot (y1+y2) 2)) Empty
| otherwise = replacePiece (replacePiece (replacePiece board s2 (promote $ seekBoard board s1)) s1 Empty) (Square (quot (x1+x2) 2) (quot (y1+y2) 2)) Empty
-- During Move remove the current piece at the current positon, add it at the final position. Check for promotion to King
mPlayMove board (March s1@(Square x1 y1) s2@(Square x2 y2)) | y2 /= 8 && y2 /= 1 = replacePiece (replacePiece board s2 (seekBoard board s1)) s1 Empty
| otherwise = replacePiece (replacePiece board s2 (promote (seekBoard board s1))) s1 Empty
-- | Promote the Piece pass as argument to a King Piece
promote :: Piece -> Piece
promote (Piece pl ty) = Piece pl King
promote Empty = Empty
-- | Return the valid moves for the 'Player' passed as argument
mValidMoves :: BoardMap -> Player -> [Move]
mValidMoves b pl | length jumpsList > 0 = jumpsList -- If jump is possible then only jumps are allowed
| otherwise = marchList -- Else return the march list
where jumpsList = List.foldr (++) [] (List.map (mValidJumps b) (mPlayableSquares b pl)) -- Get the valid jumps for all the Pieces of the current 'Player'
marchList = List.foldr (++) [] (List.map (mValidMarches b) (mPlayableSquares b pl)) -- Get the valid jumps for all the Pieces of the current 'Player'
-- | Return a list of all the 'Square' that have the Piece of the currrent 'Player'
mPlayableSquares :: BoardMap -> Player -> [(Square, Piece)]
mPlayableSquares (BoardMap b) pl = List.filter (isSamePlayer pl) (toList b)
-- | Is the 'Player' same the 'Player' of the Piece ?
isSamePlayer :: Player -> (Square, Piece) -> Bool
isSamePlayer p (_, Piece p2 _) = p == p2
isSamePlayer p (_,Empty) = False
-- Functions to calculate the possible jumps -----
-- | Return the valid jumps from the passed 'Square' of the passed 'Player'
mValidJumps :: BoardMap -> (Square, Piece) -> [Move]
mValidJumps b (s@(Square x y), piece@(Piece player ptype)) | player == Black && ptype == Pawn && y >= 3 = validJumpsBlackPawn b s -- Get valid jumps for Black Pawn
| player == Black && ptype == King = validJumpsBlackKing b s -- Get valid jumps for Black King
| player == Red && ptype == Pawn && y <= 6 = validJumpsRedPawn b s -- Get valid jumps for Red Pawn
| player == Red && ptype == King = validJumpsRedKing b s -- Get valid jumps for Red King
| otherwise = []
mValidJumps _ (_, Empty) = []
-- | Get valid jumps for the passed Black Pawn
validJumpsBlackPawn :: BoardMap -> Square -> [Move]
-- Check if it is possible to Jump in the two directions, if so add to the List
validJumpsBlackPawn board s@(Square x y) = [Jump (Square x y) (Square a b) | (a, b) <- [(x-2, y-2), (x+2, y-2)], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty, isRed $ seekBoard board (Square (quot (a+x) 2) (quot (b+y) 2))]
-- | Get valid jumps for the passed Red Pawn
validJumpsRedPawn :: BoardMap -> Square -> [Move]
-- Check if it is possible to Jump in the two directions, if so add to the List
validJumpsRedPawn board s@(Square x y) = [Jump (Square x y) (Square a b) | (a, b) <- [(x-2, y+2), (x+2, y+2)], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty, isBlack $ seekBoard board (Square (quot (a+x) 2) (quot (b+y) 2))]
-- | Get valid jumps for the passed Black King
validJumpsBlackKing :: BoardMap -> Square -> [Move]
-- Check if it is possible to Jump in the four directions, if so add to the List
validJumpsBlackKing board s@(Square x y) = [Jump (Square x y) (Square a b) | a <- [x-2, x+2], b <- [y-2, y+2], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty, isRed $ seekBoard board (Square (quot (a+x) 2) (quot (b+y) 2))]
-- | Get valid jumps for the passed Red King
validJumpsRedKing :: BoardMap -> Square -> [Move]
-- Check if it is possible to Jump in the four directions, if so add to the List
validJumpsRedKing board s@(Square x y) = [Jump (Square x y) (Square a b) | a <- [x-2, x+2], b <- [y-2, y+2], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty, isBlack $ seekBoard board (Square (quot (a+x) 2) (quot (b+y) 2))]
-- Functions to calculate the possible marches -----
-- | Return the valid marches from the passed Square of the passed 'Player'
mValidMarches :: BoardMap -> (Square, Piece) -> [Move]
mValidMarches b (s@(Square x y), piece@(Piece player ptype)) | player == Black && ptype == Pawn && y >= 2 = validMarchesBlackPawn b s -- Get valid marches for Black Pawn
| ptype == King = validMarchesKing b s -- Get valid marches for King
| player == Red && ptype == Pawn && y <= 7 = validMarchesRedPawn b s -- Get valid marches for Red Pawn
| otherwise = []
-- | Get valid marches for the passed King
validMarchesKing :: BoardMap -> Square -> [Move]
-- Check if it is possible to March in the four directions, if so add to the List
validMarchesKing board s@(Square x y) = [March (Square x y) (Square a b) | a <- [x-1, x+1], b <- [y-1, y+1], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty]
-- | Get valid marches for the passed Black Pawn
validMarchesBlackPawn :: BoardMap -> Square -> [Move]
-- Check if it is possible to March in the two directions, if so add to the List
validMarchesBlackPawn board s@(Square x y) = [March (Square x y) (Square a b) | (a, b) <- [(x-1, y-1), (x+1, y-1)], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty]
-- | Get valid marches for the passed Red Pawn
validMarchesRedPawn :: BoardMap -> Square -> [Move]
-- Check if it is possible to March in the two directions, if so add to the List
validMarchesRedPawn board s@(Square x y) = [March (Square x y) (Square a b) | (a, b) <- [(x-1, y+1), (x+1, y+1)], a >= 1 && a <= 8 && b >= 1 && b <= 8, seekBoard board (Square a b) == Empty]