packages feed

hstzaar-0.9.4: src/Board.hs

{-# LANGUAGE BangPatterns, RecordWildCards #-}
-- | 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
  , swapPlayer
  , captureMoves
  , stackingMoves
  , nextMoves
  , countStacks
  , sumHeights
  , maxHeights
  , sixLines
  , emptyBoard
  , startingBoard
  , randomBoard
  , randomBoardIO
  , showMove
  , applyMove
  , applyMoveSkip
  , positions
  , zoneOfControl
  ) where

import           Data.List (foldl')
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map

import           Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import           Data.Hashable

import qualified Data.Vector.Unboxed as Vec
import           Data.Vector.Unboxed(Vector)

import           Data.Array

import           Control.Monad (liftM, mplus)
import           System.Random

-- | 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, Enum, Show, Read)

-- | A piece stack: color, kind and height (starting at 1).
data Piece = Piece !Color !Kind !Int
             deriving (Eq, Show, Read)

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

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

height :: Piece -> Int
height (Piece _ _ 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 (Ix, Eq, Ord, Enum, Bounded, Show, Read)

instance Hashable Position where
  hashWithSalt s p = hashWithSalt s (fromEnum p)


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

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

-- | A TZAAR game board
-- | 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 :: !(Vector Int),       -- active player counts
                inactiveCounts :: !(Vector Int),     -- inactive player counts
                activeHeights :: !(Vector Int),
                inactiveHeights :: !(Vector Int)
              } deriving (Eq, Show, Read)


-- | initialize a board from a list of piece & positions
initBoard :: [(Position,Piece)] -> Board
initBoard assocs
  = let ps = HashMap.fromList assocs
    in Board { active=White,  move=0, 
               pieces=ps,
               activeCounts=Vec.fromList (countStacks White ps),
               inactiveCounts=Vec.fromList (countStacks Black ps),
               activeHeights=Vec.fromList (sumHeights White ps),
               inactiveHeights=Vec.fromList (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 = HashMap.size (pieces board)




-- | swap active player
swapPlayer :: Board -> Board
swapPlayer b = b { active = invert (active b),
                     activeCounts = inactiveCounts b,
                     inactiveCounts = activeCounts b,
                     activeHeights = inactiveHeights b,
                     inactiveHeights = activeHeights b
                   }


-- | all available moves for the active player
nextMoves :: Board -> [Move]
nextMoves b
  | Vec.any (==0) (activeCounts b) = []
  | 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 -> stackingMoves b ++ captureMoves b ++ [Pass] -- second moves
        _ -> error "nextMoves: invalid board"
  where m = move b



-- | next capture moves for the active player
captureMoves :: Board -> [Move]
captureMoves Board{..} = HashMap.foldrWithKey forPiece [] pieces
  where
    forPiece :: Position -> Piece -> [Move] -> [Move]
    forPiece !p (Piece c _ i) moves
      | c==active = foldl' downLine moves (sixLines p)
      | otherwise = moves
        where
          downLine :: [Move] -> [Position] -> [Move]
          downLine moves []   = moves
          downLine moves (q:rest)
              = case {-# SCC "piece-lookup" #-} HashMap.lookup q pieces of
                  Nothing -> downLine moves rest
                  Just (Piece c _ j) | c/=active && i>=j -> (Capture p q):moves
                  _  -> moves



-- | next stacking moves for the active player
stackingMoves :: Board -> [Move]
stackingMoves board = HashMap.foldlWithKey' forPiece [] (pieces board)
  where
    c = active board
    tzaars = activeCounts board Vec.! fromEnum Tzaar
    tzarras= activeCounts board Vec.! fromEnum Tzarra
    totts  = activeCounts board Vec.! fromEnum Tott
    forPiece :: [Move] -> Position -> Piece -> [Move]
    forPiece moves p (Piece c' _ _)
      | c==c'     = foldl' downLine moves (sixLines p)
      | otherwise = moves
        where
          downLine :: [Move] -> [Position] -> [Move]
          downLine moves [] = moves
          downLine moves (q:ps)
              = case {-# SCC "piece-lookup" #-} HashMap.lookup q (pieces board) of
                  Nothing  -> downLine moves ps
                  Just (Piece c' _ _) | c/=c' -> moves
                  Just (Piece _ Tzaar _) | tzaars==1  -> moves
                  Just (Piece _ Tzarra _) | tzarras==1 -> moves
                  Just (Piece _ 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 (HashMap.elems pieces)
    where
      count :: Int -> Int -> Int -> [Piece] -> [Int]
      count !x !y !z ((Piece c' Tzaar _) : ps)  | c==c' = count (1+x) y z ps
      count !x !y !z ((Piece c' Tzarra _) : ps) | c==c' = count x (1+y) z ps
      count !x !y !z ((Piece 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 (HashMap.elems pieces)
  where sum :: Int -> Int -> Int -> [Piece] -> [Int]
        sum !x !y !z ((Piece c' Tzaar !h):ps)  | c==c' = sum (x+h) y z ps
        sum !x !y !z ((Piece c' Tzarra !h):ps) | c==c' = sum x (y+h) z ps
        sum !x !y !z ((Piece 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 (HashMap.elems pieces)
  where maxh :: Int -> Int -> Int -> [Piece] -> [Int]
        maxh !x !y !z ((Piece c' Tzaar !h):ps) | c==c' && h>x = maxh h y z ps
        maxh !x !y !z ((Piece c' Tzarra !h):ps) | c==c' && h>y= maxh x h z ps
        maxh !x !y !z ((Piece 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 m@(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 HashMap.! x
      (Piece _ kindY sizeY) = pieces b HashMap.! y
      pieces' = {-# SCC "piece-insert" #-} HashMap.insert y pX (HashMap.delete x (pieces b))
      counts' = increment kindY (-1)     (inactiveCounts b)
      heights'= increment kindY (-sizeY) (inactiveHeights b)


applyMove m@(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
      (Piece colorX kindX sizeX) = pieces b HashMap.! x
      (Piece _     kindY sizeY) = pieces b HashMap.! y
      pieces' = {-# SCC "piece-ins-del" #-} HashMap.insert y (Piece colorX kindX (sizeX+sizeY)) (HashMap.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 m 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 -> Vector Int -> Vector Int
increment !k !n v = v Vec.// [(i, n + v Vec.! i)]
  where !i = fromEnum k


-- | 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 six lines traveling radially out from a single board position.
sixLines :: Position -> [[Position]]
sixLines p = sixLinesArray ! p

-- | global memoied array  
sixLinesArray :: Array Position [[Position]]
sixLinesArray = array (minBound,maxBound) [(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

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

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

-- | The default (non-randomized, non-tournament) starting position.
startingBoard :: Board
startingBoard   = initBoard (whites ++ blacks)
  where
  whites = [(p, (Piece White Tzaar 1)) | p<-wTzaars] ++
           [(p, (Piece White Tzarra 1)) | p<-wTzarras] ++
           [(p, (Piece White Tott 1)) | p<-wTotts]
  blacks = [(p, (Piece Black Tzaar 1)) | p<-bTzaars] ++
           [(p, (Piece Black Tzarra 1)) | p<-bTzarras] ++
           [(p, (Piece 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 (Piece White Tzaar 1) ++
               replicate 9 (Piece White Tzarra 1) ++
               replicate 15 (Piece White Tott 1)
          bs = replicate 6 (Piece Black Tzaar 1) ++
               replicate 9 (Piece Black Tzarra 1) ++
               replicate 15 (Piece 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 = HashMap.filterWithKey forPiece1 pieces
    where
      -- player's pieces that make at least one capture
      movable = HashMap.filterWithKey forPiece2 pieces

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

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

      downLine0 !i [] = False
      downLine0 !i (p:ps)
          = case HashMap.lookup p pieces of
              Nothing -> downLine0 i ps
              Just (Piece c' _ h) -> if c==c' then
                    h>=i || (p`HashMap.member`movable && downLine1 i ps)
                  else
                    let !h' = max i h
                    in any (downLine1 h') (sixLines p)


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

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