halma/src/Game/Halma/AI/Competitive.hs
module Game.Halma.AI.Competitive
( aiMove
) where
import Game.Halma.Board
import Game.Halma.Rules
import Game.Halma.AI.Base
import Data.List (sortOn)
-- | The perspective of the first team as opposed to the second team.
type Perspective = (Team, Team)
flipPersp :: Perspective -> Perspective
flipPersp (t0, t1) = (t1, t0)
rate :: Perspective -> HalmaBoard -> Rating
rate (t0, t1) board = rateTeam t0 board `against` rateTeam t1 board
where
(WinIn n) `against` _ = WinIn n
_ `against` (WinIn n) = LossIn n
(Rating r0) `against` (Rating r1) = Rating (r0 - r1)
_ `against` _ = error "unexpected team rating indicating loss"
aiMove :: RuleOptions -> HalmaBoard -> Perspective -> Move
aiMove opts board persp =
snd $ prunedMinMaxSearch 3 opts board persp Nothing
-- | Find the best move or one that reaches the given bound.
prunedMinMaxSearch
:: Int
-> RuleOptions
-> HalmaBoard
-> Perspective
-> Maybe Rating
-> (Rating, Move)
prunedMinMaxSearch depth opts board persp mBound =
go Nothing allOptions
where
allOptions =
sortIfUseful $ do
move <- allLegalMoves opts board (fst persp)
pure (rate persp (outcome board move), move)
sortIfUseful =
if depth <= 2 then
id
else
sortOn (flipRating . fst)
go :: Maybe (Rating, Move) -> [(Rating, Move)] -> (Rating, Move)
go Nothing (option@(rating, _move):options) =
if isWin rating then
option
else
go (Just $ nextLevel option Nothing) options
go (Just (currentMax, bestMove)) [] = (currentMax, bestMove)
go (Just (currentMax, bestMove)) (option@(rating, move):options) =
if isWin rating then
option
else if newRating <= currentMax then
go (Just (currentMax, bestMove)) options
else if boundReached newRating then
(newRating, move)
else
go (Just (newRating, move)) options
where newRating = fst $ nextLevel option (Just currentMax)
go Nothing [] = error "no legal moves found"
nextLevel (rating, move) mCurrentMax =
let newRating =
if depth <= 1 then
rating
else
pushRating $ flipRating $ fst $
prunedMinMaxSearch
(depth-1) opts (outcome board move)
(flipPersp persp) (fmap flipRating mCurrentMax)
in (newRating, move)
boundReached dep = maybe False (dep >=) mBound
isWin :: Rating -> Bool
isWin rating =
case rating of
WinIn _ -> True
_ -> False
flipRating :: Rating -> Rating
flipRating rating =
case rating of
WinIn n -> LossIn n
LossIn n -> WinIn n
Rating r -> Rating (-r)
pushRating :: Rating -> Rating
pushRating rating =
case rating of
WinIn n -> WinIn (n+1)
LossIn n -> WinIn (n+1)
Rating r -> Rating r