packages feed

haskell-go-checkers-0.1.1.0: src/BoardGo.hs

{-|
    This module exports all the functions required by 'playGo'
    function in 'Go' game.
-}
module BoardGo(
    Point(Point),
    Stone(Black, White, Ko, Empty),
    getOppositeStone,
    Move(Pass, Move),
    getPiece,
    GameStatus(Alive, Dead, Over),
    Game(Game),
    createGame,
    killGame,
    seekBoard,
    removeKo,
    removePiece,
    playPass,
    validMove,
    playMove,
    finishGame,
    getWinner
) where

import Data.Map as Map
import Data.List as List

-- | This data type represents a 'Point' in Board of Go (i.e. intersection of two lines)
data Point = Point Int Int -- ^ Point constructor takes two coordinates x and y
           deriving (Ord, Eq)

-- | This data type represents 'Stone' placed on the board
data Stone = Black -- ^ To represent 'Black' stone
           | White -- ^ To represent 'White' stone
           | Ko -- ^ 'Ko' is not shown on the board. Placed when Ko situation occurs
           | Empty -- ^ To represent that nothing is placed on the point
           deriving (Eq, Show)

-- | 'getOppositeStone' returns opposite stone of given 'Stone'
getOppositeStone :: Stone -> Stone
getOppositeStone stone | stone == Black = White
                      | stone == White = Black
                      | otherwise = Empty

-- | This data type represents a 'Move' that can be performed on the board by a player
data Move = Pass Stone -- ^ Player with this 'Stone' plays pass
          | Move Point Stone -- ^ Player with this 'Stone' plays on a 'Point'
          deriving (Eq)

-- | 'getPiece' returns the 'Stone' from a given 'Move'
getPiece :: Move -> Stone
getPiece (Pass st) = st
getPiece (Move  _ st) = st

-- | This data type reprsents 'Game'
data Game = Game {
    board :: Map Point Stone, -- ^ Map from point to stone
    lastMove :: Move, -- ^ Last move that was played in the game
    boardSize :: Int, -- ^ Board size 9 or 13 or 19
    scoreBlack :: Int, -- ^ Score of Black stone
    scoreWhite :: Int, -- ^ Score of White stone
    status :: GameStatus -- ^ Status of the game whether game is running or over
}

-- | 'GameStatus' represents state of the game if game is alive or dead or over
data GameStatus = Alive -- ^ game is running
                | Dead -- ^ game is over and hopeless string is counted
                | Over -- ^ Game over and results are shown on the screen
                deriving (Eq)

-- | 'createGame' takes a size and returns a 'Game' object
createGame :: Int -> Game
createGame size = Game{
    board = addPieces (Map.empty) points,
    lastMove = Move (Point (-1) (-1)) White,
    boardSize = size,
    scoreBlack = 0,
    scoreWhite = 0,
    status = Alive
} where points = [(Point x y) | x <- [1..size], y <- [1..size]]

-- | 'addPieces' adds Empty to set of points in a map
addPieces :: (Map Point Stone) -> [Point] -> (Map Point Stone)
addPieces m [] = m
addPieces m (x:xs) = addPieces (Map.insert x Empty m) xs

-- | 'killGame' changes game status from 'Alive' to 'Dead' and increases the score of 'Black'
-- as 'White' plays the last 'Pass'.
killGame :: Game -> Game
killGame game@(Game m lm s sb sw Alive) = Game m lm s (sb+1) sw Dead
killGame game = game

-- | 'removeKo' removes 'Ko' from the 'Game'
removeKo :: Game -> Game
removeKo game@(Game m lm s b w status) = (Game newBoard lm s b w status)
    where pointsWithKo = List.filter (\(p, s) -> s == Ko) (assocs m)
          newBoard = if pointsWithKo == [] then m else addPiece m (fst (pointsWithKo !! 0)) Empty

-- | 'addPiece' adds a element of type t to map
addPiece :: (Map Point t) -> Point -> t -> (Map Point t)
addPiece m point stone = Map.insert point stone (Map.delete point m)

-- | 'removePiece' removes the stone from given point in the map
removePiece :: (Map Point Stone) -> Point -> (Map Point Stone)
removePiece m point = addPiece m point Empty

-- | 'playPass' adds 'Pass' move by the 'Stone' on the 'Game'
playPass :: Game -> Stone -> Game
playPass game@(Game board lm s sb sw status) stone = Game {
    board = board,
    lastMove = Pass stone,
    boardSize = s,
    scoreBlack = newsb,
    scoreWhite = newsw,
    status = status
} where
    -- | Increase score of opposite stone
    newsb = if stone == Black then sb else (sb+1)
    newsw = if stone == White then sw else (sw+1)

-- | 'playMove' adds 'Move' to 'Point' by 'Stone' on the 'Game'.
-- After playing the move, also removes the captured points of the opposite stone if present
playMove :: Game -> Point -> Stone -> Game
playMove game@(Game board lm s sb sw status) point stone = removeGroups Game {
    board = addPiece board point stone,
    lastMove = Move point stone,
    boardSize = s,
    scoreBlack = sb,
    scoreWhite = sw,
    status = status
} point ostone where ostone = getOppositeStone stone

-- | 'removeGroups' removes captured stones of 'Stone' from the 'Game' starting from
-- 'Point' and moving in all four directions
removeGroups :: Game -> Point -> Stone -> Game
removeGroups game@(Game board lm s sb sw status) point@(Point x y) stone
    -- If number of points captured is one then check if Ko is formed and add Ko to the board if neccesary
    | length pointsToBeRemoved == 1 && isKo = updateScore (addKo (removeStones game pointsToBeRemoved) (pointsToBeRemoved !! 0)) 1 stone'
    | otherwise = updateScore (removeStones game pointsToBeRemoved) (length pointsToBeRemoved) stone'
    where (up, down, left, right) = getAdj point
          removeUp = removeDead up stone game
          removeDown = removeDead down stone game
          removeLeft = removeDead left stone game
          removeRight = removeDead right stone game
          pointsToBeRemoved = removeUp ++ removeDown ++ removeLeft ++ removeRight
          point' = if length removeUp == 1 then up else if length removeDown == 1 then down else if length removeLeft == 1 then left else right
          (up', down', left', right') = getAdj point'
          stone' = getOppositeStone stone
          isKo = length ((removeDead up' stone' game) ++ (removeDead down' stone' game) ++ (removeDead left' stone' game) ++ (removeDead right' stone' game)) == 1

-- | 'getAdj' returns adjacent points of a given 'Point'
getAdj :: Point -> (Point, Point, Point, Point)
getAdj (Point x y) = (Point x (y+1), Point x (y-1), Point (x-1) y, Point (x+1) y)

-- | 'removeDead' returns dead groups of 'Stone' in 'Game' starting from 'Point'
removeDead :: Point -> Stone -> Game -> [Maybe Point]
removeDead point stone game | checkIfTrapped game point stone = removablePoints
                            | otherwise = []
                            where removablePoints = (findTrappedGroup game point stone [])

-- | 'updateScore' updates the score of 'Stone' by given 'Int'
updateScore :: Game -> Int -> Stone -> Game
updateScore game@(Game m lm s b w status) p st
    | st == Black = (Game m lm s (b+p) w status)
    | otherwise = (Game m lm s b (w+p) status)

-- | 'addKo' adds 'Ko' to the game at given point
addKo :: Game -> (Maybe Point) -> Game
addKo game@(Game m lm s b w status) (Just p) = (Game (addPiece m p Ko) lm s b w status)

-- | 'removeStones' given a list of points removes stones from all those pieces in the game
removeStones :: Game -> [Maybe Point] -> Game
removeStones game@(Game m lm _ _ _ status) [] = game
removeStones game@(Game m lm s b w status) (Nothing:xs) = game
removeStones game@(Game m lm s b w status) ((Just p):xs) = removeStones (Game (removePiece m p) lm s b w status) xs

-- | 'seekBoard' returns the 'Stone' at given 'Point' in the 'Game'
seekBoard :: Game -> Point -> Stone
seekBoard (Game m _ _ _ _ _) p = case Map.lookup p m of
    Just stone -> stone
    Nothing -> Empty

-- | This data type represents whether corresponding stone belongs to Black or White or None
data Status = Seen | Unseen | SeenW | SeenB | None deriving (Eq)

-- | 'seekMap' returns the 'Status' of 'Point' from a given map
seekMap :: (Map Point Status) -> Point -> Status
seekMap m p = case Map.lookup p m of
    Just s -> s
    Nothing -> Unseen

-- | 'findTrappedGroup' finds trapped group of stones in the game
-- starting from point and going in all directions
findTrappedGroup :: Game -> Point -> Stone -> [Maybe Point] -> [Maybe Point]
findTrappedGroup game@(Game m move@(Move pt st) boardSize _ _ _) point@(Point x y) stone seenPoints
    | x < 1 || x > boardSize || y < 1 || y > boardSize  = seenPoints
    | elem (pure point) seenPoints = seenPoints
    | seekBoard game point == Empty = Nothing:seenPoints
    | seekBoard game point == Ko = Nothing:seenPoints
    | seekBoard game point /= stone = seenPoints
    | otherwise = findTrappedGroup game left stone
        $ findTrappedGroup game right stone
        $ findTrappedGroup game up stone
        $ findTrappedGroup game down stone ((pure point):seenPoints)
    where up = Point x (y+1)
          down = Point x (y-1)
          right = Point (x+1) y
          left = Point (x-1) y

-- | 'findTerritory' finds territory of a given point in the game
-- starting from point and going in all directions
findTerritory :: Game -> Point -> Stone -> ((Map Point Status), [Maybe Point]) -> ((Map Point Status), [Maybe Point])
findTerritory game@(Game _ _ boardSize _ _ _) point@(Point x y) stone (m, points)
    | x < 1 || x > boardSize || y < 1 || y > boardSize = (m, points) -- If point is out of board
    | elem (pure point) points = (m, points) -- If we have already visited this point
    | seekBoard game point == getOppositeStone stone = (m, Nothing:points) -- If this point has opposite stone
    | seekBoard game point == stone = (m, points) -- If this point has current stone
    | otherwise = findTerritory game left stone
        $ findTerritory game right stone
        $ findTerritory game up stone
        $ findTerritory game down stone ((Map.insert point Seen m), ((pure point):points)) -- Otherwise recure on all adjacent points of the stone
    where (up, down, left, right) = getAdj point

-- | 'findTerritories' finds terrirtories of either Black or White stone from given Point
findTerritories :: Game -> Point -> (Map Point Status) -> (Map Point Status)
findTerritories game point m
    | seekBoard game point /= Empty && seekBoard game point /= Ko = addPiece m point None -- If point has Ko or Empty
    | seekMap m point == SeenW = m -- If point belongs to White's territory
    | seekMap m point == SeenB = m -- If point belongs to Black's territory
    | seekMap m point == None = m -- If point has been seen and belongs to none
    | otherwise = if elem Nothing (snd tw) then
                      if elem Nothing (snd tb) then setInMap m (snd tb) None
                      else setInMap m (snd tb) SeenB
                  else setInMap m (snd tw) SeenW
    where tb = findTerritory game point Black (m, [])
          tw = findTerritory game point White (m, [])

-- | 'setInMap' sets staus of points in the map
setInMap :: (Map Point Status) -> [Maybe Point] -> Status -> (Map Point Status)
setInMap m [] st = m
setInMap m (point:points) st | point /= Nothing = setInMap (addPiece m (purify point) st) points st
                             | otherwise = setInMap m points st

-- | 'findAllTerritoriesOfPoints' calulates all the territories of given list of points in Game
findAllTerritoriesOfPoints :: Game -> [Point] -> (Map Point Status) -> (Map Point Status)
findAllTerritoriesOfPoints game [] m = m
findAllTerritoriesOfPoints game (point:points) m = findAllTerritoriesOfPoints game points (findTerritories game point m)

-- | 'findTerritories' creates list of points and finds territories for all the points in the game
findAllTerritories :: Game -> (Map Point Status)
findAllTerritories game@(Game _ _ boardSize _ _ _) = findAllTerritoriesOfPoints game points Map.empty
    where points = [(Point x y) | x <- [1..boardSize], y <- [1..boardSize]]

-- | 'finishGame' finds all the captured territories in the 'Game' and updates the correspoing scores of the 'Stone'
finishGame :: Game -> Game
finishGame game@(Game m lm s sb sw status) = Game m lm s sb' sw' Over
    where sb' = sb + countSeenB t s
          sw' = sw + countSeenW t s
          t = findAllTerritories game

-- | 'countSeenB' counts the points that belong to territory of Black
countSeenB :: (Map Point Status) -> Int -> Int
countSeenB m size | l == size*size = 0 -- If board is empty then it cannot be territory
                  | otherwise = l
                  where l = length $ List.filter (\(p,s) -> s == SeenB) (assocs m)

-- | 'countSeenW' counts the points that belong to territory of White
countSeenW :: (Map Point Status) -> Int -> Int
countSeenW m size | l == size*size = 0 -- If board is empty then it cannot be territory
                  | otherwise = l
                  where l = length $ List.filter (\(p,s) -> s == SeenW) (assocs m)
-- | 'purify' changes Maybe Type to Type
purify :: Maybe a -> a
purify (Just a) = a

-- | 'validMove' checks if the move of given 'Point' to given 'Stone' is valid or not
validMove :: Game -> Point -> Stone -> Bool
validMove game@(Game m lm s _ _ _) p@(Point x y) st | x < 1 || x > s || y < 1 || y > s = False
    | seekBoard game p /= Empty = False -- If point is not empty
    | not $ checkIfTrapped game1 p st = True -- If point is not trapped then validmove
    -- If point is trapped then check if on placing the stone whether opposites stone sare captured or not
    | (seekBoard game up == ostone) && (checkIfTrapped game1 up ostone) = True
    | (seekBoard game down == ostone) && (checkIfTrapped game1 down ostone) = True
    | (seekBoard game left == ostone) && (checkIfTrapped game1 left ostone) = True
    | (seekBoard game right == ostone) && (checkIfTrapped game1 right ostone) = True
    | otherwise = False
    where game1 = playMove game p st
          ostone = getOppositeStone st
          up = Point x (y+1)
          down = Point x (y-1)
          right = Point (x+1) y
          left = Point (x-1) y

-- | 'checkIfTrapped' checks whether a stone is trapped or not
checkIfTrapped :: Game -> Point -> Stone -> Bool
checkIfTrapped game p st = not $ elem Nothing (findTrappedGroup game p st [])

-- | 'checkIfNothing' checks whether 'Maybe Point' is 'Nothing'
checkIfNothing :: (Maybe Point) -> Bool
checkIfNothing Nothing = True
checkIfNothing (Just point) = False

-- | 'getWinner' declares the winner depending on the scores of the stones
getWinner :: Game -> String
getWinner game@(Game _ _ _ sb sw _)
    | sb > sw = "Black wins." -- If 'Black' score is more than 'White' score
    | sw > sb = "White wins." -- If 'White' score is more than 'Black' score
    | otherwise = "Game Draws" -- If both the scores are equal