packages feed

hstzaar-0.9: src/Board.hs

{-# LANGUAGE BangPatterns #-}
-- | Board State 
module Board
  ( -- * Types
    Board (..)
  , PieceMap
  , Color (..)
  , Kind (..)
  , Piece
  , Position (..)
  , Move (..)
  , Game (..)
  , initBoard
  , initGame
  , color, kind, height   -- attributes of pieces 
  , nthTurn, nthMove
  , invert
  , inactive
  , countPieces
  , endGame
  , winner
  , swapBoard
  , captureMoves    
  , stackingMoves
  , nextMoves
  , countStacks
  , sumHeights
  , maxHeights
  , sixLines
  , emptyBoard
  , startingBoard
  , randomBoard
  , randomBoardIO
  , showMove
  , applyMove
  , applyMoveSkip
  , positions
  , zoneOfControl
  ) where

import Data.List
import Data.Map (Map, (!))
import qualified Data.Map as Map
import System.Random
import Control.Monad (liftM,mplus)


-- | player colors
data Color = White | Black 
           deriving (Eq,Show,Enum,Read)

-- | the inverse color
invert :: Color -> Color
invert White = Black
invert Black = White

-- | The three piece types 
-- | Each player starts with 6 Tzaars, 9 Tzarras, and 15 Totts.
data Kind = Tzaar | Tzarra | Tott 
            deriving (Eq, Ord, Show, Read)

-- | A piece stack: color, kind and height (starting at 1).
type Piece = (Color, Kind, Int)

color :: Piece -> Color
color (c, _, _) = c

kind :: Piece -> Kind
kind (_, k, _)= k

height :: Piece -> Int
height (_, _, h)= h


-- | Algebraic board positions.  Letters left to right, numbers bottom to top.
-- | Column E has the hole in the middle.
data Position
  = A1 | A2 | A3 | A4 | A5
  | B1 | B2 | B3 | B4 | B5 | B6
  | C1 | C2 | C3 | C4 | C5 | C6 | C7
  | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8
  | E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8
  | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8
  | G1 | G2 | G3 | G4 | G5 | G6 | G7
  | H1 | H2 | H3 | H4 | H5 | H6
  | I1 | I2 | I3 | I4 | I5
  deriving (Eq, Ord, Enum, Bounded, Show, Read)

-- | List of all positions (for enumeration)
positions :: [Position]
positions = [minBound .. maxBound]

-- | A mapping from positions to pieces 
type PieceMap = Map Position Piece

-- | The board state
-- | current turn, active player pieces, other player pieces
data Board 
      = Board { active :: !Color,            -- player to move next 
                move :: !Int,                -- total move count
                pieces :: PieceMap,          -- map positions to pieces
                activeCounts :: [Int],       -- active player counts
                inactiveCounts :: [Int],     -- inactive player counts
                activeHeights :: [Int],
                inactiveHeights :: [Int]
              } deriving (Eq, Show, Read)


-- | initialize a board from a list of piece & positions
initBoard :: [(Position,Piece)] -> Board
initBoard assocs
  = let ps = Map.fromList assocs
    in Board { active=White, move=0, pieces=ps,
               activeCounts=countStacks White ps,
               inactiveCounts=countStacks Black ps,
               activeHeights=sumHeights White ps,
               inactiveHeights=sumHeights Black ps 
             }
       
inactive :: Board -> Color
inactive = invert . active


-- | A move is either a capture, a stacking or a pass
--   "Skip" is a dummy move to alternate players in a turn
data Move = Capture !Position !Position  -- origin and destination positions
          | Stack   !Position !Position  
          | Pass 
          | Skip
            deriving (Eq, Show, Read)


showMove :: Move -> String
showMove (Capture a b) = show a ++ "x" ++ show b
showMove (Stack a b)   = show a ++ "-" ++ show b
showMove Pass          = "pass"
showMove Skip          = "skip"


-- | record to hold the game state
data Game = Game
  { human :: Color       -- human plays white or black?
  , initial :: Board     -- initial board
  , board :: Board       -- current board
  , trail :: [Move]      -- previous moves 
  , remain :: [Move]     -- future moves
  } deriving (Eq,Show) 


-- | initialize a game state
initGame :: Board -> Color -> Game
initGame b c = Game { human=c, initial=b, board=b, trail=[], remain=[] }



-- | Convert number of moves into number of turns 
nthTurn :: Int -> Int
nthTurn 0 = 1
nthTurn m | m>0 = 2 + (m-1)`div`3

nthMove :: Int -> Int
nthMove 0 = 1
nthMove m | m>0 = min (1+(m-1)`mod`3) 2



-- | number of pieces in a board
countPieces :: Board -> Int
countPieces board = Map.size (pieces board) 




-- | swap active player
-- this is used to analyse the board from the opponent's prespective
swapBoard :: Board -> Board
swapBoard b = b { active= invert (active b), 
                  activeCounts = inactiveCounts b, 
                  inactiveCounts = activeCounts b,
                  activeHeights= inactiveHeights b,
                  inactiveHeights = activeHeights b
                }


-- | next moves for the active player
nextMoves :: Board -> [Move]
nextMoves b 
  | tzaars==0 || tzarras==0 || totts==0 = []
  | m == 0                              = captureMoves b
  | otherwise 
    = case (m-1)`mod`3 of
        0 -> captureMoves b     -- first move
        1 -> [Skip]             -- dummy opponent move within a turn
        2 -> Pass : (captureMoves b ++ stackingMoves b) -- second moves
        _ -> error "nextMoves: invalid board"
  where -- lostPieces = any (==0) (countStacks (active b) (pieces b)) 
        tzaars:tzarras:totts:_ = activeCounts b
        m = move b



-- | next capture moves for the active player
captureMoves :: Board -> [Move]
captureMoves board = Map.foldrWithKey forPiece [] (pieces board)
  where
    c = active board
    forPiece :: Position -> Piece -> [Move] -> [Move]
    forPiece !p (!c', _, !i) moves 
      | c==c'     = foldl' downLine moves (sixLines p)
      | otherwise = moves
        where
          downLine :: [Move] -> [Position] -> [Move]
          downLine moves []   = moves
          downLine moves (q:ps) 
              = case Map.lookup q (pieces board) of
                  Nothing -> downLine moves ps 
                  Just (c', _, j) | c/=c' && i>=j -> (Capture p q):moves
                  _  -> moves



-- | next stacking moves for the active player
stackingMoves :: Board -> [Move]
stackingMoves board = foldl' forPiece [] (Map.assocs (pieces board))
  where 
    c = active board
    tzaars:tzarras:totts:_ = activeCounts board
    forPiece :: [Move] -> (Position,Piece) -> [Move]
    forPiece moves (p,(c',_,_)) 
      | c==c'     = foldl' downLine moves (sixLines p)
      | otherwise = moves
        where
          downLine :: [Move] -> [Position] -> [Move]
          downLine moves [] = moves
          downLine moves (q:ps) 
              = case Map.lookup q (pieces board) of
                  Nothing  -> downLine moves ps
                  Just (c', _, _) | c/=c' -> moves
                  Just (_, Tzaar, _) | tzaars==1  -> moves
                  Just (_, Tzarra, _) | tzarras==1 -> moves
                  Just (_, Tott, _) | totts==1  -> moves
                  Just (_, _, _)  -> (Stack p q) : moves




-- | count the number of stacks of each type in a half-board
countStacks :: Color -> PieceMap -> [Int]
countStacks c pieces
    = count 0 0 0 (Map.elems pieces)
    where
      count :: Int -> Int -> Int -> [Piece] -> [Int]
      count !x !y !z ((c',Tzaar,_) : ps)  | c==c' = count (1+x) y z ps
      count !x !y !z ((c',Tzarra,_) : ps) | c==c' = count x (1+y) z ps
      count !x !y !z ((c',Tott,_) : ps)   | c==c' = count x y (1+z) ps
      count !x !y !z (_ : ps)                     = count x y z ps      
      count !x !y !z []              = [x,y,z]


-- | sum of heights of stacks for each kind
sumHeights :: Color -> PieceMap -> [Int]
sumHeights c pieces = sum 0 0 0 (Map.elems pieces)
  where sum :: Int -> Int -> Int -> [Piece] -> [Int]
        sum !x !y !z ((c',Tzaar,!h):ps)  | c==c' = sum (x+h) y z ps
        sum !x !y !z ((c',Tzarra,!h):ps) | c==c' = sum x (y+h) z ps
        sum !x !y !z ((c',Tott,!h):ps)   | c==c' = sum x y (z+h) ps
        sum !x !y !z (_ : ps)        = sum x y z ps
        sum !x !y !z []              = [x,y,z]

-- | maximum height for each kind
maxHeights :: Color -> PieceMap -> [Int]        
maxHeights c pieces = maxh 0 0 0 (Map.elems pieces)
  where maxh :: Int -> Int -> Int -> [Piece] -> [Int]
        maxh !x !y !z ((c',Tzaar,!h):ps) | c==c' && h>x = maxh h y z ps
        maxh !x !y !z ((c',Tzarra,!h):ps) | c==c' && h>y= maxh x h z ps        
        maxh !x !y !z ((c',Tott,!h):ps) | c==c' && h>z  = maxh x y h ps
        maxh !x !y !z (_ : ps) = maxh x y z ps
        maxh !x !y !z []       = [x,y,z]


-- | The next board state after a move.  
-- | Assumes the move is valid.
applyMove :: Move -> Board -> Board
applyMove (Capture x y) b 
  = b {active=invert (active b), 
       move=1+move b, 
       pieces= pieces', 
       activeCounts = counts',         -- swap counts
       inactiveCounts= activeCounts b,
       activeHeights = heights',       -- swap heights
       inactiveHeights = activeHeights b
      }
    where
      pX  = pieces b!x      
      (_, kindY, sizeY) = pieces b!y
      pieces' = Map.insert y pX (Map.delete x (pieces b))
      counts' = increment kindY (-1)     (inactiveCounts b)
      heights'= increment kindY (-sizeY) (inactiveHeights b)


applyMove (Stack x y) b 
  = b {active=invert (active b), 
       move=1+move b, 
       pieces=pieces',
       activeCounts = inactiveCounts b,
       inactiveCounts = counts',
       activeHeights = inactiveHeights b,
       inactiveHeights= heights'
         }
    where
      (colorX, kindX, sizeX) = pieces b!x
      (_,      kindY, sizeY) = pieces b!y
      pieces' = Map.insert y (colorX,kindX,sizeX+sizeY) (Map.delete x (pieces b))
      counts' = increment kindY (-1) (activeCounts b)
      heights' | kindX==kindY = activeHeights b
               | otherwise = increment kindY (-sizeY) $ 
                             increment kindX sizeY (activeHeights b)
          
          
-- Pass & Skip have the same effect
applyMove _ b = b {active= invert (active b), 
                   move=1+move b, 
                   activeCounts= inactiveCounts b,
                   inactiveCounts= activeCounts b,
                   activeHeights= inactiveHeights b,
                   inactiveHeights= activeHeights b
                  }

-- | modify a counter
increment :: Kind -> Int -> [Int] -> [Int]
increment Tzaar  i (tzaars:tzarras:totts:_) = (tzaars+i) : tzarras : totts : []
increment Tzarra i (tzaars:tzarras:totts:_) = tzaars : (tzarras+i) : totts : []
increment Tott   i (tzaars:tzarras:totts:_) = tzaars : tzarras : (totts+i) : []


-- | apply one move and subsequent skip move
applyMoveSkip :: Move -> Board -> Board 
applyMoveSkip m b 
  = case nextMoves b' of
    [Skip] -> applyMove Skip b'  -- compulsory move
    _      -> b'
    where b' = applyMove m b


                          
endGame :: Board -> Bool
endGame = null . nextMoves 

winner :: Board -> Color
winner = invert . active


-- | All the lines that form connected positions on the board.
connectedPositions :: [[Position]]
connectedPositions =
  [ [A1, A2, A3, A4, A5]
  , [B1, B2, B3, B4, B5, B6]
  , [C1, C2, C3, C4, C5, C6, C7]
  , [D1, D2, D3, D4, D5, D6, D7, D8]
  , [E1, E2, E3, E4]
  , [E5, E6, E7, E8]
  , [F1, F2, F3, F4, F5, F6, F7, F8]
  , [G1, G2, G3, G4, G5, G6, G7]
  , [H1, H2, H3, H4, H5, H6]
  , [I1, I2, I3, I4, I5]
  , [A1, B1, C1, D1, E1]
  , [A2, B2, C2, D2, E2, F1]
  , [A3, B3, C3, D3, E3, F2, G1]
  , [A4, B4, C4, D4, E4, F3, G2, H1]
  , [A5, B5, C5, D5]
  , [F4, G3, H2, I1]
  ,     [B6, C6, D6, E5, F5, G4, H3, I2]
  ,         [C7, D7, E6, F6, G5, H4, I3]
  ,             [D8, E7, F7, G6, H5, I4]
  ,                 [E8, F8, G7, H6, I5]   
  ,                 [E1, F1, G1, H1, I1]
  ,             [D1, E2, F2, G2, H2, I2]
  ,         [C1, D2, E3, F3, G3, H3, I3]
  ,     [B1, C2, D3, E4, F4, G4, H4, I4]
  , [A1, B2, C3, D4]
  , [F5, G5, H5, I5]
  , [A2, B3, C4, D5, E5, F6, G6, H6]
  , [A3, B4, C5, D6, E6, F7, G7]
  , [A4, B5, C6, D7, E7, F8]
  , [A5, B6, C7, D8, E8]
  ]

-- | The three lines that cross at a single board position.
threeLines :: Position -> [[Position]]
threeLines p = [ line | line <- connectedPositions, elem p line ]


-- | The six lines traveling radially out from a single board position.
-- | optimization: this map should be memoied lazily 
sixLines_memo :: Map Position [[Position]]  -- Map Position [[Position]]
sixLines_memo = Map.fromList [(p, radials p) | p<-positions]
    where radials p = [r | l<-threeLines p, r<-divide p l, not (null r)]
          divide a b = [reverse x, y]
              where (x, _:y) = span (/= a) b

sixLines :: Position -> [[Position]]
sixLines p = sixLines_memo!p




-- | An empty board
emptyBoard :: Board
emptyBoard = initBoard [] 

-- | The default (non-randomized, non-tournament) starting position.
startingBoard :: Board
startingBoard   = initBoard (whites ++ blacks)
  where
  whites = [(p, (White,Tzaar,1)) | p<-wTzaars] ++ 
           [(p, (White,Tzarra,1)) | p<-wTzarras] ++ 
           [(p, (White,Tott,1)) | p<-wTotts]
  blacks = [(p, (Black,Tzaar,1)) | p<-bTzaars] ++ 
           [(p, (Black,Tzarra,1)) | p<-bTzarras] ++ 
           [(p, (Black,Tott,1)) | p<-bTotts]
  wTzaars  = [D3, E3, G4, G5, C5, D6]
  wTzarras = [C2, D2, E2, H3, H4, H5, B5, C6, D7]
  wTotts   = [B1, C1, D1, E1, I2, I3, I4, I5, D8, C7, B6, A5, E4, F5, D5]
  bTzaars  = [C3, C4, F3, G3, E6, F6]
  bTzarras = [B2, B3, B4, F2, G2, H2, E7, F7, G6]
  bTotts   = [A1, A2, A3, A4, F1, G1, H1, I1, E8, F8, G7, H6, D4, E5, F4]


-- | A randomized starting position
randomBoard :: StdGen -> (Board, StdGen)
randomBoard rnd = (b, rnd')
    where b = initBoard (whites++blacks)
          ws = replicate 6 (White,Tzaar,1) ++
               replicate 9 (White,Tzarra,1) ++
               replicate 15 (White,Tott,1)
          bs = replicate 6 (Black,Tzaar,1) ++
               replicate 9 (Black,Tzarra,1) ++
               replicate 15 (Black,Tott,1)
          (positions',rnd') = shuffle rnd positions
          whites = zip (take 30 positions') ws
          blacks = zip (drop 30 positions') bs

randomBoardIO :: IO Board
randomBoardIO = do rnd <- getStdGen
                   let (b, rnd') = randomBoard rnd
                   setStdGen rnd'
                   return b


-- an auxilary function to shuffle a list randomly
shuffle :: StdGen  -> [a] -> ([a], StdGen)
shuffle g xs = shuffle' g xs (length xs)
    where
      shuffle' :: RandomGen g => g -> [a] -> Int -> ([a], g)
      shuffle' g xs n 
          | n>0 = let (k, g') = randomR (0,n-1) g
                      (xs',x:xs'') = splitAt k xs
                      (ys,g'') = shuffle' g' (xs' ++ xs'') (n-1)
                  in (x:ys, g'')
          | otherwise = ([],g)




-- Estimate the zone of control of a player
-- i.e., the opponents' pieces that can be captured in two moves
zoneOfControl ::  Color -> PieceMap -> PieceMap
zoneOfControl c pieces = Map.filterWithKey forPiece1 pieces
    where
      -- player's pieces that make at least one capture
      movable = Map.filterWithKey forPiece2 pieces

      forPiece1, forPiece2 :: Position -> Piece -> Bool
      forPiece1 p (c', _, i) = c'/=c && or (map (downLine0 i) $ sixLines p)
      forPiece2 p (c',_, h) = c'==c && or (map (downLine2 h) $ sixLines p)

      downLine0, downLine1, downLine2 :: Int -> [Position] -> Bool

      downLine0 i [] = False
      downLine0 i (p:ps) 
          = case Map.lookup p pieces of
              Nothing -> downLine0 i ps
              Just (c', _, h) | c'==c -> 
                  h>=i || (p`Map.member`movable && downLine1 i ps)
              Just (c', _, j) | c'/=c -> 
                  or $ map (downLine1 (max i j)) $ sixLines p

      downLine1 i [] = False
      downLine1 i (p:ps) 
          = case Map.lookup p pieces of
              Nothing -> downLine1 i ps
              Just (c', _, h) -> c'==c && h>=i

      downLine2 h [] = False
      downLine2 h (p:ps) 
          = case Map.lookup p pieces of
              Nothing -> downLine2 h ps
              Just (c', _, i) -> c'/=c && h>=i