-- ================================== -- Module name: PylosBoard -- Project: Pylos -- Copyright (C) 2008 Bartosz Wójcik -- Created on: 09.06.2008 -- Last update: 06.11.2008 -- Version: % {- This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -} -- ================================== -- | This module defines board of Pylos game. -- It contains also board manipulation and verification functions. module PylosBoard (Stone (..), Coordinate, Board, IntMapBoard, NbrOfBalls, Pylos (..), Player(..), initPylos, putStone, putable, putableForMove, takeStone, takeable, moveable, updateAllBases, updateAllSuccessors, anyTakeable, moveOnCoordinate, ifTakeAfterPut, nextPlayer, isFree, stone2Player, nbrOfBalls, terminator ) where -- =================================== import Graphics.Rendering.OpenGL (GLint) import qualified Data.Map as Map -- import Mapext (mapAny) -- =================================== -- =================================== -- | Each field of 'Board' must be one of following type. data Stone = NoStone | WhiteStone | BlackStone deriving (Eq,Ord,Enum,Read,Show) -- | Playes definition data Player = WhitePlayer | BlackPlayer deriving (Eq,Ord,Enum,Read,Show) -- | Casts Players on Stones player2Stone :: Player -> Stone -- ---------------------------------- player2Stone WhitePlayer = WhiteStone player2Stone BlackPlayer = BlackStone -- | Casts Stones of Players stone2Player :: Stone -> Player -- ---------------------------------- stone2Player WhiteStone = WhitePlayer stone2Player BlackStone = BlackPlayer stone2Player _ = error "stone2Player: Player of NoStone doesn't exist" -- | Same like not for Bool nextPlayer :: Player -> Player -- --------------------------------- nextPlayer BlackPlayer = WhitePlayer nextPlayer WhitePlayer = BlackPlayer -- | Coordinate within 'Board' consists of level (0 to size) and position within the level. -- Size is a parameter that indicates number of tiles of the board, which is (size * size). type Coordinate = (GLint,(GLint,GLint)) -- | Terminator terminates move. terminator :: Coordinate terminator = (-1,(0,0)) -- | Board is map of stones indexed by coordinates. type Board = Map.Map Coordinate Stone -- | 'Board' has two shadow-structures of following type. -- * 1st of them called 'bases'. -- * 2nd of them called 'successors'. type IntMapBoard = Map.Map Coordinate GLint -- | Any ball on the board increases nbr of balls of own row, colums and some number of 4 balls squares. -- Any time ball is placed on or removed from the board, corresponding counters are increased or diminished. -- This should accelerate taking decision whether move is good or bad. type NbrOfBalls = Map.Map Coordinate GLint -- =================================== -- =================================== -- | Aggregated status on the board. It contains 'Board' itself and additional data that accelerate analysis. data Pylos = Pylos { size :: GLint -- ^ size of game , board :: Board -- ^ board , bases :: IntMapBoard -- ^ Number of stones that constitute base of each stone, -- that have already been put onto board. If and only if this number = 4 -- then field is putable. For instance all level 0 fields have this value always = 4. , successors :: IntMapBoard -- ^ number of stones that covers each stone. -- 'Stone' can be removed from the 'Board' only if this number = 0. , rows :: NbrOfBalls -- ^ Indicator of content of each row. Row is full of white stones when this equals size of the game. -- It is full of black stones when equals (-size). -- Rows are indexed by (number of row,0) pair. , columns :: NbrOfBalls -- ^ Indicator of content of each column. Column is full of white stones when this equals size of the game. -- It is full of black stones when equals (-size). -- Columns are indexed by (0,number of column) pair. , squaresOf4 :: NbrOfBalls -- ^ Indicator of content of each 4 balls square. It is full of white stones when this equals 4. -- It is full of black stones when equals (-4). What 4 balls square is? Each 4 tiles of -- the board that have 1 common vertex constitute 4 balls square. -- 4 balls squares are indexed by their upper left tile (or Nord-West if one prefers). , advance :: IntMapBoard -- ^ Each place on board gets own advance value. It is supposed to express expectation of being a good -- move by populating this place. } deriving (Eq,Read,Show) -- =================================== -- | Initial game situation. Input: size. initPylos :: GLint -> Pylos -- ----------------------------------- initPylos s = Pylos { size = s, board = initBoard s, bases = basesOfFields s, successors = succesorsOfFields s, rows = Map.fromList [((level,(x,0)),0) | level <- [0 .. s-1], x <- [0 .. s-level-1] ] , columns = Map.fromList [((level,(0,y)),0) | level <- [0 .. s-1], y <- [0 .. s-level-1] ] , squaresOf4 = Map.fromList [((level,(x,y)),0) | level <- [0 .. s-1], x <- [0 .. s-level-2], y <- [0 .. s-level-2] ] , advance = Map.fromList $ (terminator,0):[((level,(x,y)),0) | level <- [0 .. s-1], x <- [0 .. s-level-1], y <- [0 .. s-level-1] ] } -- =================================== -- =================================== -- Initial board. Input: size. initBoard :: GLint -> Board -- ----------------------------------- initBoard size = Map.fromList [((level,(x,y)),NoStone) | level<-[0..(size-1)], x<-[0..(size-level-1)], y<-[0..(size-level-1)]] -- =================================== -- =================================== -- | Initial bases. Input: size. basesOfFields :: GLint -> IntMapBoard -- ----------------------------------- basesOfFields size = Map.fromList [((level,(x,y)),nbrBaseStones level) | level<-[0..(size-1)], x<-[0..(size-level-1)], y<-[0..(size-level-1)]] where nbrBaseStones 0 = 4 nbrBaseStones _ = 0 -- =================================== -- =================================== -- | Initial successors. Input: size. succesorsOfFields :: GLint -> IntMapBoard -- ----------------------------------- succesorsOfFields size = Map.fromList [((level,(x,y)),0) | level<-[0..(size-1)], x<-[0..(size-level-1)], y<-[0..(size-level-1)]] -- =================================== -- =================================== -- | If field is free. isFree :: Coordinate -> Pylos -> Bool -- ----------------------------------- isFree c pylos = board pylos Map.! c == NoStone -- =================================== -- =================================== -- | Is stone of given coordinates of given colour? isMyColour :: Player -> Coordinate -> Pylos -> Bool -- ----------------------------------- isMyColour player cor pylos = board pylos Map.! cor == player2Stone player -- =================================== -- =================================== -- | Whether given field has all bases. hasAllBases :: Coordinate -> Pylos -> Bool -- ----------------------------------- hasAllBases c pylos = bases pylos Map.! c == 4 -- =================================== -- =================================== -- | Whether given field has no successors. hasNoSuccessors :: Coordinate -> Pylos -> Bool -- ----------------------------------- hasNoSuccessors c pylos = successors pylos Map.! c == 0 -- =================================== -- =================================== -- | Puts stone on the board on given coordinates. Uses 'Map.insert' function, so doesn't matter what was before value of given coordinates. putStone :: Stone -> Coordinate -> Board -> Board -- ----------------------------------- putStone stone coordinate board = Map.insert coordinate stone board -- =================================== -- =================================== -- | Checks whether its allowed to put a ball on field given by coordinates. putable :: Pylos -> Coordinate -> Bool -- ----------------------------------- putable p c | hasAllBases c p && isFree c p = True | otherwise = False -- =================================== -- =================================== -- | Removes stone from give filed. In other words, inserts 'NoStone' value. takeStone :: Coordinate -> Board -> Board -- ----------------------------------- takeStone coordinate board = Map.insert coordinate NoStone board -- =================================== -- =================================== -- | Checks whether it's allowed to take given ball from board. takeable :: Player -> Pylos -> Coordinate -> Bool -- ----------------------------------- takeable pl py c | isMyColour pl c py && hasNoSuccessors c py = True | otherwise = False -- =================================== -- =================================== -- | Checks whether it's allowed to take given ball from board and put it level above. moveable :: Player -> Pylos -> Coordinate -> Bool -- ----------------------------------- moveable pl pylos c@(l,(x,y)) = takeable pl pylos c && anyPutable (l+1) pylos' where pylos' = takeStoneAndUpdate c pylos -- =================================== -- =================================== -- | Updates successor of given coordinate. This function works as well for bases. updateSuccessor :: Stone -> Coordinate -> IntMapBoard -> IntMapBoard -- ----------------------------------- updateSuccessor NoStone coordinate succesorsOfFields = Map.adjust (\x -> x-1) coordinate succesorsOfFields updateSuccessor _ coordinate succesorsOfFields = Map.adjust (+ 1) coordinate succesorsOfFields -- =================================== -- =================================== -- | Each time a stone is put or taken 4 (or 3 or 2 or 1) bases have to be updated. -- Takes stone, coordinate, size, bases and returns new bases. updateAllBases :: Stone -> Coordinate -> GLint -> IntMapBoard -> IntMapBoard -- ----------------------------------- updateAllBases stone (level,(0,0)) size bases = updateSuccessor stone (level+1,(0,0)) bases updateAllBases stone (level,(x,0)) size bases | x == size - 1 = updateSuccessor stone (level+1,(x-1,0)) bases | otherwise = updateSuccessor stone (level+1,(x-1,0)) $ updateSuccessor stone (level+1,(x,0)) bases updateAllBases stone (level,(0,y)) size bases | y == size - 1 = updateSuccessor stone (level+1,(0,y-1)) bases | otherwise = updateSuccessor stone (level+1,(0,y-1)) $ updateSuccessor stone (level+1,(0,y)) bases updateAllBases stone (level,(x,y)) size bases | x == size - 1 && y == size - 1 = updateSuccessor stone (level+1,(x-1,y-1)) bases | x == size - 1 = updateSuccessor stone (level+1,(x-1,y)) $ updateSuccessor stone (level+1,(x-1,y-1)) bases | y == size - 1 = updateSuccessor stone (level+1,(x,y-1)) $ updateSuccessor stone (level+1,(x-1,y-1)) bases | otherwise = updateSuccessor stone (level+1,(x-1,y-1)) $ updateSuccessor stone (level+1,(x-1,y)) $ updateSuccessor stone (level+1,(x,y-1)) $ updateSuccessor stone (level+1,(x,y)) bases -- =================================== -- =================================== -- | Each time a stone is put or taken 4 successors have to be updated. -- Takes stone, coordinate, size, bases and returns new successors. updateAllSuccessors :: Stone -> Coordinate -> GLint -> IntMapBoard -> IntMapBoard -- ----------------------------------- updateAllSuccessors stone (0,(x,y)) size successors = successors updateAllSuccessors stone (level,(x,y)) size successors = updateSuccessor stone (level-1,(x+1,y+1)) $ updateSuccessor stone (level-1,(x+1,y)) $ updateSuccessor stone (level-1,(x,y+1)) $ updateSuccessor stone (level-1,(x,y)) successors -- =================================== -- =================================== -- | Updates number of balls of given row. updateRow :: Stone -> Bool -- ^ True - stone is put, False - stone is being removed from the board. -> Coordinate -> GLint -- ^ size -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateRow WhiteStone True (level,(x,y)) size rows = Map.adjust (+ 1) (level,(x,0)) rows updateRow BlackStone True (level,(x,y)) size rows = Map.adjust (\x -> x - 1) (level,(x,0)) rows updateRow WhiteStone False (level,(x,y)) size rows = Map.adjust (\x -> x - 1) (level,(x,0)) rows updateRow BlackStone False (level,(x,y)) size rows = Map.adjust (+ 1) (level,(x,0)) rows -- =================================== -- =================================== -- | 2nd argument points on the action: True - stone is put, False - stone is being removed from the board. -- 4th - size. updateColumn :: Stone -> Bool -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateColumn WhiteStone True (level,(x,y)) size columns = Map.adjust (+ 1) (level,(0,y)) columns updateColumn BlackStone True (level,(x,y)) size columns = Map.adjust (\x -> x - 1) (level,(0,y)) columns updateColumn WhiteStone False (level,(x,y)) size columns = Map.adjust (\x -> x - 1) (level,(0,y)) columns updateColumn BlackStone False (level,(x,y)) size columns = Map.adjust (+ 1) (level,(0,y)) columns -- =================================== -- =================================== -- Coordinate of squareOf4 is its NE ball. -- | 2nd argument points on the action: True - stone is put, False - stone is being removed from the board. -- 4th - size. updateSquaresOf4 :: Stone -> Bool -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateSquaresOf4 WhiteStone True coordinate size squaresOf4 = (updateSquareNE 1 coordinate size . updateSquareNW 1 coordinate size . updateSquareSE 1 coordinate size . updateSquareSW 1 coordinate size) squaresOf4 updateSquaresOf4 BlackStone True coordinate size squaresOf4 = (updateSquareNE (-1) coordinate size . updateSquareNW (-1) coordinate size . updateSquareSE (-1) coordinate size . updateSquareSW (-1) coordinate size) squaresOf4 updateSquaresOf4 BlackStone False coordinate size squaresOf4 = (updateSquareNE 1 coordinate size . updateSquareNW 1 coordinate size . updateSquareSE 1 coordinate size . updateSquareSW 1 coordinate size) squaresOf4 updateSquaresOf4 WhiteStone False coordinate size squaresOf4 = (updateSquareNE (-1) coordinate size . updateSquareNW (-1) coordinate size . updateSquareSE (-1) coordinate size . updateSquareSW (-1) coordinate size) squaresOf4 -- =================================== updateSquareNE :: GLint -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateSquareNE one (level,(x,y)) size squaresOf4 | x > 0 && y > 0 = Map.adjust (+ one) (level,(x-1,y-1)) squaresOf4 | otherwise = squaresOf4 -- =================================== updateSquareNW :: GLint -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateSquareNW one (level,(x,y)) size squaresOf4 | x > 0 && y < size - level -1 = Map.adjust (+ one) (level,(x-1,y)) squaresOf4 | otherwise = squaresOf4 -- =================================== updateSquareSE :: GLint -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateSquareSE one (level,(x,y)) size squaresOf4 | x < size - level -1 && y > 0 = Map.adjust (+ one) (level,(x,y-1)) squaresOf4 | otherwise = squaresOf4 -- =================================== updateSquareSW :: GLint -> Coordinate -> GLint -> NbrOfBalls -> NbrOfBalls -- ----------------------------------- updateSquareSW one (level,(x,y)) size squaresOf4 | x < size - level -1 && y < size - level -1 = Map.adjust (+ one) (level,(x,y)) squaresOf4 | otherwise = squaresOf4 -- =================================== -- =================================== updateAllAdvances :: Stone -> Coordinate -> GLint -> GLint -> IntMapBoard -> IntMapBoard -- ----------------------------------- updateAllAdvances stone (l,(x,y)) one size = updateAdvances (valueOfStone stone) keysS . updateAdvances (valueOfStone stone) keysR . updateAdvances (valueOfStone stone) keysC where keysR = [(l,(x,y')) | y' <- [0 .. size-l-1], y' /= y ] -- update all of given row keysC = [(l,(x',y)) | x' <- [0 .. size-l-1], x' /= x ] -- update all of given column keysS = [(l,(x',y')) | x' <- [(max 0 (x-1)) .. (min (size-l-1) (x+1))], -- update all of all squaresOf4 y' <- [(max 0 (y-1)) .. (min (size-l-1) (y+1))], x' /= x || y' /= y] valueOfStone WhiteStone = 1 * one valueOfStone BlackStone = -1 * one valueOfStone _ = 0 -- =================================== updateAdvances :: GLint -> [Coordinate] -> IntMapBoard -> IntMapBoard -- ----------------------------------- updateAdvances one keys advance = foldl (\adv key -> Map.adjust (+ one) key adv) advance keys -- =================================== -- =================================== -- | If there is any puttable filed on the board. -- Takes level and current game situation. anyPutable :: GLint -> Pylos -> Bool -- ----------------------------------- anyPutable level pylos = any (putable pylos) keys where keys = [(l,(x,y)) | l<-[level..s-1], x <- [0..s-l-1], y<-[0..s-l-1]] s = size pylos -- =================================== -- =================================== -- | -- Whether ball can be put to given position within the move turn. putableForMove :: GLint -> Coordinate -> Pylos -> Bool -- ----------------------------------- putableForMove level c@(l,(x,y)) pylos = l > level && putable pylos c -- =================================== -- =================================== -- | If there is any takeable field on the board. -- Takes stone to be taken and current game situation. anyTakeable :: Player -> Pylos -> Bool -- ----------------------------------- anyTakeable player pylos = any (takeable player pylos) keys where keys = [(l,(x,y)) | l<-[0..s-1], x <- [0..s-l-1], y<-[0..s-l-1]] s = size pylos -- =================================== -- =================================== -- | -- Does only possible action on given field. -- Gets stone colour which depends on what playes is on move, coordinates and game status. -- Updates the whole status of Pylos structure. moveOnCoordinate :: Player -> Coordinate -> Pylos -> Pylos -- ----------------------------------- moveOnCoordinate player coordinate pylos | coordinate == terminator = pylos | putable pylos coordinate = putStoneAndUpdate stone coordinate pylos | takeable player pylos coordinate = takeStoneAndUpdate coordinate pylos | otherwise = pylos where stone = player2Stone player -- =================================== -- =================================== -- | -- Updates whole 'Pylos' structure after stone has been put. putStoneAndUpdate :: Stone -> Coordinate -> Pylos -> Pylos -- ----------------------------------- putStoneAndUpdate stone c@(l,(x,y)) pylos = Pylos { size = s, board = putStone stone c (board pylos), bases = updateAllBases stone c s (bases pylos), successors = updateAllSuccessors stone c s (successors pylos), rows = updateRow stone True c s (rows pylos), columns = updateColumn stone True c s (columns pylos), squaresOf4 = updateSquaresOf4 stone True c s (squaresOf4 pylos), advance = updateAllAdvances stone c 1 s (advance pylos) } where s = size pylos -- =================================== -- =================================== -- | -- Updates whole 'Pylos' structure after stone has been taken. takeStoneAndUpdate :: Coordinate -> Pylos -> Pylos -- ----------------------------------- takeStoneAndUpdate c@(l,(x,y)) pylos = Pylos { size = s, board = takeStone c (board pylos), bases = updateAllBases NoStone c s (bases pylos), successors = updateAllSuccessors NoStone c s (successors pylos), rows = updateRow stone False c s (rows pylos), columns = updateColumn stone False c s (columns pylos), squaresOf4 = updateSquaresOf4 stone False c s (squaresOf4 pylos), advance = updateAllAdvances stone c (-1) s (advance pylos) } where s = size pylos stone = board pylos Map.! c -- =================================== -- =================================== -- | -- Checks whether player can take balls after he||she puts one. ifTakeAfterPut :: Pylos -> Player -> Coordinate -> Bool -- ----------------------------------- ifTakeAfterPut pylos player c = isInFullCol player c (columns pylos) s || -- anyRowOrColFull s player (columns pylos) || isInFullRow player c (rows pylos) s || -- anyRowOrColFull s player (rows pylos) || isInFullSquareOf4 player c (squaresOf4 pylos) s -- anySquareOf4Full player (squaresOf4 pylos) where s = size pylos -- =================================== -- =================================== isInFullRow :: Player -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- isInFullRow player c@(l,(x,y)) row size = row Map.! (l,(x,0)) == value player where value WhitePlayer = size value _ = -size -- =================================== -- =================================== isInFullCol :: Player -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- isInFullCol player c@(l,(x,y)) col size = col Map.! (l,(0,y)) == value player where value WhitePlayer = size value _ = -size -- =================================== -- =================================== ifFullSquareNE :: GLint -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- ifFullSquareNE value (level,(x,y)) squaresOf4 size | x > 0 && y > 0 = squaresOf4 Map.! (level,(x-1,y-1)) == value | otherwise = False -- =================================== ifFullSquareNW :: GLint -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- ifFullSquareNW value (level,(x,y)) squaresOf4 size | x > 0 && y < size - level -1 = squaresOf4 Map.! (level,(x-1,y)) == value | otherwise = False -- =================================== ifFullSquareSE :: GLint -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- ifFullSquareSE value (level,(x,y)) squaresOf4 size | x < size - level -1 && y > 0 = squaresOf4 Map.! (level,(x,y-1)) == value | otherwise = False -- =================================== ifFullSquareSW :: GLint -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- ifFullSquareSW value (level,(x,y)) squaresOf4 size | x < size - level -1 && y < size - level -1 = squaresOf4 Map.! (level,(x,y)) == value | otherwise = False -- =================================== -- =================================== isInFullSquareOf4 :: Player -> Coordinate -> NbrOfBalls -> GLint -> Bool -- ----------------------------------- isInFullSquareOf4 player c squaresOf4 size = ifFullSquareNE (value player) c squaresOf4 size || ifFullSquareNW (value player) c squaresOf4 size || ifFullSquareSE (value player) c squaresOf4 size || ifFullSquareSW (value player) c squaresOf4 size where value WhitePlayer = 4 value _ = -4 -- =================================== -- ========================================================= -- | -- Returns total number of balls for given size of the game. nbrOfBalls :: (Enum a, Num a) => a -> a -- --------------------------------------------------------- nbrOfBalls size = sum $ map (\x->x*x) [1..size] -- =========================================================