packages feed

tak-ai-0.1.0.1: src/Negamax.hs

{-# LANGUAGE TypeFamilies, FlexibleInstances, BangPatterns #-}
module Negamax (
    Game(..),
    chooseplay
) where

import Data.List

class Game a where
    type Play a :: * -- ^ A play in the game.
    plays :: a -> [(Play a, a)] -- ^ All possible plays from a given game state,
                                --   and their successor states.
    eval :: a -> Double -- ^ An assessment of how good the current game state looks.
    ourmove :: a -> Bool -- ^ Whether the next play is ours or our opponents.
    finished :: a -> Bool -- ^ Whether the game has finished.

chooseplay
    :: (Show a, Game a)
    => Int -- ^ Number of levels to look ahead.
    -> a -- ^ The game state
    -> (Play a, a, Double) -- ^ The chosen play, successor state, and the
                           --   corresponding maximised minimum score
chooseplay levels state
    | length (plays state) == 0 = error $ "No plays from " ++ show state
    | otherwise = (play, nextState, score)
    where (_, _, score, Just (play, nextState), _) = chooseplay' levels state (-1e101) 1e101

maxGuaranteedScore
    :: (Game a, Show a)
    => Int -- ^ Number of levels to look ahead.
    -> a -- ^ The game state
    -> Double -- ^ The current maximum score that we are guaranteed
    -> Double -- ^ The minimum score that our opponent is guaranteed
    -> Double -- ^ The maximum guaranteed score
maxGuaranteedScore levels state alpha beta
    | levels <= 0 = eval state
    | length (plays state) == 0 = eval state
    | otherwise = score
    where (_, _, score, _, _) = chooseplay' levels state alpha beta

chooseplay'
    :: (Game a, Show a)
    => Int
    -> a
    -> Double
    -> Double
    -> (Double, Double, Double, Maybe (Play a, a), Int)
chooseplay' levels state alphaStart betaStart = bestScore
    where
        bestScore = foldl evalPlay (alphaStart, betaStart, initScore, Nothing, 0) sortedPlays
        initScore
            | ourmove state = -1e101
            | otherwise = 1e101
        evalPlay (alpha, beta, minimax, bestState, count) (play, nextState) =
            let score = maxGuaranteedScore (levels - 1) nextState alpha beta
                (minimax', bestState') = if ourmove state
                    then if score > minimax
                        then (score, Just (play, nextState))
                        else (minimax, bestState)
                    else
                        if score < minimax
                            then (score, Just (play, nextState))
                            else (minimax, bestState)
                alpha' = if ourmove state
                    then max alpha minimax'
                    else alpha
                beta' = if ourmove state
                    then beta
                    else min beta minimax'
            in
            if count > 100
                then (alpha, beta, minimax, bestState, count)
                else if beta <= alpha
                    then (alpha, beta, minimax, bestState, count)
                    else (alpha', beta', minimax', bestState', count + 1)
        sortedPlays =
            map fst $ sortPlays (not $ ourmove state) $ zip ps estimates
        estimates = map (\ s -> maxGuaranteedScore (levels - 2) s (-1e101) 1e101) states
        states = map snd ps
        ps = plays state

sortPlays :: Bool -> [(a, Double)] -> [(a, Double)]
sortPlays invert = sortBy $ \ (_, a) (_, b) ->
    if invert then compare a b else compare (-a) (-b)