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