packages feed

snorkels-0.2.0.0: src/Snorkels/Board.hs

module Snorkels.Board (
                      -- * Checkers
                        isValid
                      , isTrapped
                      , hasLost
                      -- * Generators
                      , neighbours
                      , allPositions
                      , freePositions
                      , growGroup
                      , groupFrom
                      , getGroups
                      -- * Filters
                      , areValid
                      , areNeighbours
                      , arePieces
                      , areSnorkels
                      , areFromPlayer
                      -- * Manipulation
                      , getPiece
                      , putPiece
                      , shufflePositions
                      , throwStones
                      ) where

import Control.Monad (mfilter)
import Data.Function
import Data.Maybe
import System.Random (RandomGen, randomRs)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set

import Snorkels.Types


-- |
-- Given some @(min, max)@ bounds, check if an 'Int' is in them.
-- @min@ is inclusive, @max@ isn't.
inRange :: (Int, Int) -> Int -> Bool
inRange (min, max) check = min <= check && check < max

-- |
-- Given some @(maxWidth, maxHeight)@ bounds and a 'Position', check whether the
-- 'Position' is within those bounds.
inBounds :: (Int, Int) -> Position -> Bool
inBounds (maxX, maxY) (x, y) = inRange (0, maxX) x && inRange (0, maxY) y

-- |
-- Offset by @(x, y)@ a 'Position' to obtain a new one
offset :: (Int, Int) -> Position -> Position
offset (x, y) (x2, y2) = (x+x2, y+y2)

-- |
-- Given a 'Position', get the 'Set.Set' of 'Position's that are immediatelly
-- above, under, to the left, or to the right.
neighbours :: Position -> Set.Set Position
neighbours position = Set.map (`offset` position) neighbourOffsets
                      where neighbourOffsets = Set.fromList [(-1, 0), (1, 0), (0, -1), (0, 1)]

-- |
-- Check if some 'Position' is within the bounds of a board
isValid :: Game -> Position -> Bool
isValid game = inBounds $ game&boardSize

-- |
-- Check if some 'Position's are within the bounds of a game
areValid :: Game -> Set.Set Position -> Set.Set Position
areValid game = Set.filter (isValid game)

-- |
-- Get all the 'Position's that are within a board
allPositions :: Game -> Set.Set Position
allPositions game = Set.fromList [(x, y) | x <- [0..width-1], y <- [0..height-1]]
                     where (width, height) = game&boardSize

-- |
-- Get all the 'Position's that are within a board and that haven't been already
-- occupied.
freePositions :: Game -> Set.Set Position
freePositions game = Set.filter (flip Map.notMember $ game&pieces)
                   . allPositions
                   $ game

-- |
-- Get all the neighbour 'Position's of some 'Position's within a board.
areNeighbours :: Game -> Set.Set Position -> Set.Set Position
areNeighbours game positions = areValid game
                             . flip Set.difference positions
                             . Set.unions
                             . map neighbours
                             $ Set.toList positions

-- |
-- Filter 'Position's only leaving those which have a 'Piece' in some board.
arePieces :: Game -> Set.Set Position -> Set.Set Position
arePieces game = Set.intersection (Map.keysSet (game&pieces)) . areValid game

-- |
-- Filter 'Position's only leaving those which have a 'Snorkel' in some board.
areSnorkels :: Game -> Set.Set Position -> Set.Set Position
areSnorkels game = Set.filter (maybe False isSnorkel . getPiece game) . arePieces game

-- |
-- Filter 'Position's only leaving those which have a 'Snorkel' of the given
-- 'Player' in some board.
areFromPlayer :: Game -> Player -> Set.Set Position -> Set.Set Position
areFromPlayer game player = Set.filter (maybe False fromPlayer . getPiece game) . areSnorkels game
                            where fromPlayer = maybe False (player ==) . getPlayer

-- |
-- Put into a 'Group' 'Position's that are immediate neighbours.
growGroup :: Game -> Group -> Group
growGroup game initial
            | Set.null new = initial
            | otherwise = growGroup game group
            where group = Group {positions = Set.union initialPositions new, player = owner}
                  new = areFromPlayer game owner $ areNeighbours game initialPositions
                  initialPositions = initial&positions
                  owner = initial&player

-- TODO: Should this return a Maybe Group (to account for the possibility of the
-- given position on the board being empty) or allow groups of empty positions
-- too? Such groups might be useful for AI if we ever dare go there.
-- |
-- Form a 'Group' from an initial 'Position' putting all the 'Position's with
-- 'Snorkel's from the same 'Player' that are connected, vertically or
-- horizontally, and by 'Snorkel's of the same 'Player', to the initial
-- 'Position'
groupFrom :: Game -> Position -> Maybe Group
groupFrom game pos = growGroup game <$> (groupForPlayer <$> owner)
                     where groupForPlayer p = Group {positions = Set.singleton pos, player = p}
                           owner = mfilter isSnorkel (getPiece game pos) >>= getPlayer

-- |
-- Get all the 'Group's on the board.
getGroups :: Game -> Set.Set Group
getGroups game = Set.map fromJust
               . Set.filter isJust
               . Set.map (groupFrom game)
               $ allPositions game

-- |
-- Check whether a given 'Group' is trapped by having all its surrounding
-- positions taken by 'Stone's or 'Snorkel's from some other 'Player'.
isTrapped :: Game -> Group -> Bool
isTrapped game group = all (isBlocking (group&player) . getPiece game)
                           (Set.toList $ areNeighbours game $ group&positions)

-- |
-- Check whether the given 'Player' has one of its snorkel 'Group's trapped.
hasLost :: Game -> Player -> Bool
hasLost game p = any (isTrapped game)
                     (filter ((== p) . player) $ Set.toList $ getGroups game)


-- |
-- Get whatever is at the given 'Position' on the board.
getPiece :: Game -> Position -> Maybe Piece
getPiece game pos = Map.lookup pos $ game&pieces


-- |
-- Put a 'Piece' at the given 'Position' on the board.
putPiece :: Game -> Position -> Piece -> Game
putPiece game pos piece = game {pieces = Map.insert pos piece $ game&pieces}


-- |
-- Given a 'Set.Set' of 'Position's, get them in an ordered random list.
shufflePositions :: RandomGen g => Set.Set Position -> g -> [Position]
shufflePositions positions g = map (p !!) $ randomRs (0, length p - 1) g
                               where p = Set.toList positions


throwStone :: RandomGen g => Game -> g -> Either String Game
throwStone game g
    | null $ freePositions game = Left "There is no place to throw a stone."
    | otherwise = Right $ putPiece game pos Stone
                  where pos = head $ shufflePositions (freePositions game) g

-- |
-- Randomly throw the given number of 'Stone's on the board.
throwStones :: RandomGen g => Game -> Int -> g -> Either String Game
throwStones game 0 _ = Right game
throwStones game n g = case throwStone game g of
                         Right game -> throwStones game (n-1) g
                         Left message -> Left message