packages feed

hstzaar-0.3: src/AI/Minimax.hs

module AI.Minimax(greedy, ply2,ply3,ply4, dyn1, dyn2) where

import Data.List (sort, sortBy, maximumBy, minimumBy, nub, nubBy)
import qualified Data.Map as Map
import Data.Map (Map)
import AI.Utils
import Board
import Debug.Trace


-- A greedy strategy
-- chooses the move with highest static evaluation score
greedy :: AI
greedy = AI { name = "greedy"
            , description = "Maximize the static evaluation function"
            , strategy = (ifPieces (==60) 
                          (firstTurn greedyStrategy)
                          (ifPieces (>48)
                           (onlyCaptureStack greedyStrategy)
                           (narrowDoubleCaptures greedyStrategy)
                          )
                         )
            }



greedyStrategy :: Strategy
greedyStrategy (GameTree _ branches) rndgen 
    = trace ("Greedy score: " ++ show bestscore) (bestmove, rndgen)
    where 
      choices = [(m, eval (root t)) | (m,t)<-branches]
      (bestmove,bestscore) = maximumBy (\x y -> compare (snd x) (snd y)) choices
      root (GameTree x _) = x



-- straight minimaxing strategies with increasing depth
ply2 :: AI
ply2 = AI { name = "ply2"
          , description = "Minimax with depth 2"
          , strategy = (ifPieces (==60) 
                        (firstTurn $ minimaxStrategy 2 3)
                        (narrowDoubleCaptures $ minimaxStrategy 2 3)
                       )
          }

ply3 :: AI
ply3 = AI { name = "ply3"
          , description = "Minimax with depth 3"
          , strategy = (ifPieces (==60) 
                        (firstTurn $ minimaxStrategy 3 3)
                        (narrowDoubleCaptures $ minimaxStrategy 3 3)
                       )
          }

ply4 :: AI
ply4 = AI { name = "ply4"
          , description = "Minimax with depth 4"
          , strategy =  (ifPieces (==60) 
                         (firstTurn $ minimaxStrategy 4 3)
                         (narrowDoubleCaptures $ minimaxStrategy 4 3)
                        )
          }

-- dynamic strategies:
-- increase the maximax depth and breadth towards the end game
dyn1 :: AI
dyn1 = AI { name = "dyn1"
          , description = "Minimax with dynamic depth 1-4"
          , strategy = (ifPieces (==60)
                        (firstTurn greedyStrategy)
                        (ifPieces (>48) 
                         (onlyCaptureStack greedyStrategy)
                         (narrowDoubleCaptures $ 
                          ifPieces (>28)
                          (minimaxStrategy 2 3)
                          (ifPieces (>20)
                           (minimaxStrategy 3 4)
                           (minimaxStrategy 4 6)
                          )
                         )
                        )
                       )
           }


dyn2 :: AI
dyn2 = AI { name = "dyn2"
          , description = "Minimax with dynamic depth 2-6"
          , strategy = (ifPieces (==60)
                        (firstTurn greedyStrategy)
                        (ifPieces (>48) 
                        (onlyCaptureStack $ minimaxStrategy 2 3)
                         (narrowDoubleCaptures $  
                          ifPieces (>28)
                          (minimaxStrategy 3 3)
                          (ifPieces (>20)
                           (minimaxStrategy 4 4)
                           (minimaxStrategy 6 6)
                          )
                         )
                        )
                       )
          }




-- Minimaxing strategy to ply depth `n' and breadth `m'
-- using alpha-beta prunning
minimaxStrategy :: Int -> Int -> Strategy
minimaxStrategy n m g rndgen 
    = trace ("Minimax score: " ++ show bestscore) (bestmove, rndgen)
    where (bestmove,bestscore) = minimaxMove_ab undefined (-inf) inf g'
          g'  = prunebreadth m $  -- ^ cut to breadth `m'
                highfirst $       -- ^ order moves using static evaluation
                mapTree eval $    -- ^ apply evaluation function
                prunedepth n g    -- ^ prune to depth `n'
          


-- Naive minimax algorithm (not used)
-- nodes should contain the static evaluation scores
minimax :: (Num a, Ord a) => GameTree a m -> a 
minimax (GameTree x []) = x
minimax (GameTree _ branches) = - minimum (map (minimax.snd) branches)

-- auxiliary function that returns the best first move
minimaxMove :: (Num a, Ord a) => GameTree a m -> (m,a)
minimaxMove (GameTree _ branches) = (m, -x)
    where (m,x) = minimumBy (\x y ->compare (snd x) (snd y)) [(m,minimax t) | (m,t)<-branches]



-- Minimax with alpha-beta prunning
minimax_ab :: (Num a, Ord a) => a -> a -> GameTree a m -> a
minimax_ab a b (GameTree x []) = a `max` x `min` b
minimax_ab a b (GameTree _ branches) = cmx a b (map snd branches)
    where cmx a b []  = a
          cmx a b (t:ts) | a'>=b = b
                         | otherwise = cmx a' b ts
                         where a' = - minimax_ab (-b) (-a) t


-- This variant also returns the best initial move
minimaxMove_ab :: (Num a, Ord a) => m -> a -> a -> GameTree a m -> (m,a)
minimaxMove_ab m0 a b (GameTree x []) = (m0, a`max`x`min`b)
minimaxMove_ab m0 a b (GameTree _ branches) = cmx m0 a b branches
    where cmx m a b []  = (m,a)
          cmx m a b ((m',t):branches) 
              | a'>=b = (m',b)
              | otherwise = cmx m' a' b branches
              where a' = - minimax_ab (-b) (-a) t

      


-- Static evaluation function for a board position
-- boolean indicates if active player is conducting the analysis
eval :: (Bool,Board) -> Int
eval (True, b) = value b
eval (False,b) = - value (swapBoard b)


-- value of a board position for the active player
value :: Board -> Int
value b@(active,other)
    | minimum pieces ==0 || null captures  = -inf
    | minimum pieces'==0 || null captures' =  inf
    | otherwise = material + positional + threats 
    where 
      -- piece counts for each player 
      pieces = counts active
      pieces'= counts other

      captures = nextCaptureMoves b
      captures'= nextCaptureMoves (swapBoard b)    

      -- the zones of control for each player
      -- active player has advantage for equal height
      zoc = zoneOfControl (>=) b
      zoc'= zoneOfControl (>) (swapBoard b)

      -- capture counts by piece type
      nzoc = counts zoc
      nzoc'= counts zoc'

      -- material score
      material = sumHeights active - sumHeights other

      -- positional score
      positional = sumHeights zoc - sumHeights zoc'

      -- scores for immediate threats
      threats = penalty p - penalty q

      p = minimum [x-min 2 y | (x,y)<-zip pieces' nzoc]
      q = minimum [x-min 2 y | (x,y)<-zip pieces nzoc']

      penalty n | n<=2      = inf`div`(2^(1+n))
                | otherwise = 0




-- a higher value than legitimate evaluation score
inf :: Int
inf = 2^10

            
-- count the number of pieces of each type
-- results ordered by piece types 
counts :: HalfBoard -> [Int]
counts b = Map.elems $ Map.fold (\(t,_)-> Map.adjust (+1) t) zeroPieces b

-- finite map assigning 0 to each piece type
-- lifted to top-level to allow sharing across multiple calls
zeroPieces :: Map Type Int
zeroPieces = Map.fromList [(Tzaar,0),(Tzarra,0),(Tott,0)] 

-- sum the heights of pieces (material value of a player)
sumHeights :: HalfBoard -> Int
sumHeights b = sum [h | (_,h)<-Map.elems b]



-- Estimate the "zone of control" of the active player
-- i.e. the opponent's pieces reachable in one or two captures
zoneOfControl ::  (Int->Int->Bool) -> Board -> HalfBoard
zoneOfControl cmp board@(_,other) 
    = Map.filterWithKey forPiece other
    where
      forPiece :: Position -> Piece -> Bool
      forPiece p (_, i) = or $ map (downLine i) $ sixLines p
          where
            downLine, downLine' :: Int -> [Position] -> Bool

            downLine i [] = False
            downLine i (p:ps) 
                = case atPosition board p of
                    Nothing -> downLine i ps
                    Just (True, (_, h)) -> h`cmp`i
                    Just (False, (_, j)) -> 
                        or $ map (downLine' (max i j)) $ sixLines p

            downLine' i [] = False
            downLine' i (p:ps) 
                = case atPosition board p of
                    Nothing -> downLine' i ps
                    Just (True, (_, h)) -> h`cmp`i
                    Just (False, _) -> False

                                        




-- | narrow the search space: single capture first move
firstTurn :: Strategy -> Strategy
firstTurn s (GameTree node branches) rndgen 
    = s (GameTree node branches') rndgen
    where branches' = [((m,Nothing),g) | ((m,Nothing), g)<-branches]

-- | narrow the search space: consider only capture-stacking turns
onlyCaptureStack ::  Strategy -> Strategy   
onlyCaptureStack s g rndgen = s (narrowTree g) rndgen
    where
      narrowTree :: BoardTree -> BoardTree
      narrowTree (GameTree node@(b, (you,_)) branches)
          | b = GameTree node [ ((m1,Just m2), narrowTree g) 
                                | ((m1,Just m2), g)<-branches,
                                snd m2 `Map.member` you
                              ]
          | otherwise = GameTree node [ (t, narrowTree g) | (t,g)<-branches ]


-- | eliminate double-captures that lead to the same board
narrowDoubleCaptures :: Strategy -> Strategy
narrowDoubleCaptures s g rndgen = s (nubTree g) rndgen
    where
      nubTree :: BoardTree -> BoardTree
      nubTree (GameTree node branches) 
          = GameTree node $ nubBy equiv [(t, nubTree g) | (t,g)<-branches]
          where
            equiv ((m1,Just m2),_) ((m2', Just m1'),_)
                = fst m1/=fst m2 && m1==m1' && m2==m2'
            equiv _ _ = False
                            


-- | use different strategies dependening on the number of pieces left
ifPieces :: (Int -> Bool) -> Strategy -> Strategy -> Strategy
ifPieces cond s1 s2 g@(GameTree (_,(you,other)) branches) rndgen
    | cond n    = s1 g rndgen   -- use the 1st strategy
    | otherwise = s2 g rndgen   -- use the 2nd strategy
    where
      n = Map.size you + Map.size other