hstzaar-0.1: 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
greedy :: AI
greedy = AI { name = "greedy"
, description = "Maximize the static evaluation function"
, strategy = (ifPieces (==60)
(firstTurn greedyStrategy)
(ifPieces (>52)
(onlyCaptureStack greedyStrategy)
(narrowDoubleCaptures greedyStrategy)
)
)
}
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)
)
}
dyn1 :: AI
dyn1 = AI { name = "dyn1"
, description = "Minimax with dynamic depth 1-4"
, strategy = (ifPieces (==60)
(firstTurn greedyStrategy)
(ifPieces (>52)
(onlyCaptureStack greedyStrategy)
(narrowDoubleCaptures $
ifPieces (>30)
(minimaxStrategy 2 3)
(ifPieces (>20)
(minimaxStrategy 3 3)
(minimaxStrategy 4 5)
)
)
)
)
}
dyn2 :: AI
dyn2 = AI { name = "dyn2"
, description = "Minimax with dynamic depth 2-6"
, strategy = (ifPieces (==60)
(firstTurn greedyStrategy)
(ifPieces (>52)
(onlyCaptureStack $ minimaxStrategy 2 3)
(narrowDoubleCaptures $
ifPieces (>30)
(minimaxStrategy 3 3)
(ifPieces (>20)
(minimaxStrategy 4 3)
(minimaxStrategy 6 5)
)
)
)
)
}
-- | A greedy strategy: locally maximize the static evaluation function
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
-- | Minimaxing strategy to ply depth `n'
-- With alpha-beta and depth prunning
minimaxStrategy :: Int -> Int -> Strategy
minimaxStrategy n m g rndgen
= trace ("Minimax score: " ++ show bestscore) (bestmove, rndgen)
where g' = prunebreadth m $ -- ^ cut to breadth $m$
highfirst $ -- ^ order moves using static evaluation
mapTree eval $ -- ^ apply evaluation function
prunedepth n g -- ^ cut to depth $n$
(bestmove,bestscore) = minimaxMove_ab (-inf) inf g'
-- | Naive minimax algorithm
-- 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 move
-- should always be called with a non-empty tree
minimaxMove_ab :: (Num a, Ord a) => a -> a -> GameTree a m -> (m,a)
minimaxMove_ab a b (GameTree x []) = (undefined, a`max`x`min`b)
minimaxMove_ab a b (GameTree _ branches@((m,_):_)) = cmx m 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
eval :: (Bool,Board) -> Int
eval (True, b) = value b
eval (False,b) = - value (swapBoard b)
value :: Board -> Int
value b@(you,other)
| pieces==0 || null captures = -inf
| pieces'==0 || null captures' = inf
| otherwise = threats + positional
where pieces = length $ nub $ map fst $ Map.elems you
pieces'= length $ nub $ map fst $ Map.elems you
captures = nextCaptureMoves b -- my captures
captures'= nextCaptureMoves (swapBoard b) -- opponents's captures
-- the zones of control for each player
-- the active play has advantage for equal heights
zoc = zoneOfControl (>=) b
zoc'= zoneOfControl (>) (swapBoard b)
-- the three piece types
ts = [Tzaar, Tzarra, Tott]
-- immediate threats
threats = points safe' - points safe
-- pieces safe from immediate threat
safe = minimum [count t you - min 2 (count t zoc') | t <- ts]
safe'= minimum [count t other - min 2 (count t zoc) | t <- ts]
points n | n<=0 = inf`div`2
| n==1 = inf`div`4
| otherwise = 0
-- positional score
-- sum heights multiplied by "relevance" factor
-- inside other player's ZoC
positional = sum [material t zoc * relevance t other | t<-ts] -
sum [material t zoc'* relevance t you | t<-ts]
-- lower count pieces types are more relevant
relevance t r = 2^(15-count t r)
-- | count pieces of a particular type
count :: Type -> HalfBoard -> Int
count t r = Map.size $ Map.filter (\(t',_)->t'==t) r
-- | material score by piece type
-- sum height for stacks
material :: Type -> HalfBoard -> Int
material t r = Map.fold (\(t',h) s->if t==t' then s+h else s) 0 r
-- | The "zone of control" of a player
-- | the opponent's pieces that can be captured in a turn
zoneOfControl :: (Int->Int->Bool) -> Board -> HalfBoard
zoneOfControl gt board@(you,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`gt`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`gt`i
Just (False, _) -> False
-- a higher value than legitimate evaluation scores
inf :: Int
inf = 2^20
-- | 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 depedening 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