packages feed

hcheckers-0.1.0.2: src/Core/Evaluator.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}
module Core.Evaluator
  ( SimpleEvaluator (..),
    SimpleEvaluatorData (..),
    weightForSide,
    defaultEvaluator
  ) where

import           Data.Aeson
import           Data.Aeson.Types as AT
import           Data.Default
import qualified Data.Vector as V
import qualified Data.Map as M

import           Core.Types
import           Core.Board

data SimpleEvaluatorWeights = SimpleEvaluatorWeights {
    sewFirst :: {-# UNPACK #-} ! ScoreBase
  , sewSecond :: {-# UNPACK #-} ! ScoreBase
  }
  deriving (Show)

weightForSide :: Side -> SimpleEvaluatorWeights -> ScoreBase
weightForSide First w = sewFirst w
weightForSide Second w = sewSecond w

data SimpleEvaluatorData = SimpleEvaluatorData {  
    sedCenter :: SimpleEvaluatorWeights
  }
  deriving (Show)

data SimpleEvaluator = SimpleEvaluator {
    seRules :: SomeRules,
    seUsePositionalScore :: Bool,
    seMobilityWeight :: ScoreBase,
    seBackyardWeight :: ScoreBase,
    seCenterWeight :: ScoreBase,
    seOppositeSideWeight :: ScoreBase,
    seBorderManWeight :: ScoreBase,
    seBackedWeight :: ScoreBase,
    seAsymetryWeight :: ScoreBase,
    sePreKingWeight :: ScoreBase,
    seKingCoef :: ScoreBase,
    seHelpedKingCoef :: ScoreBase,
    seThreatWeight :: ScoreBase,
    seAttackedManCoef :: ScoreBase,
    seAttackedKingCoef :: ScoreBase,
    seCache :: M.Map Address SimpleEvaluatorData
  }
  deriving (Show)

defaultEvaluator :: GameRules rules => rules -> SimpleEvaluator
defaultEvaluator rules = SimpleEvaluator
    { seRules              = iface
    , seUsePositionalScore = True
    , seMobilityWeight     = 30
    , seBackyardWeight     = 14
    , seCenterWeight       = 16
    , seOppositeSideWeight = 32
    , seBorderManWeight    = -16
    , seBackedWeight       = 24
    , seAsymetryWeight     = -12
    , sePreKingWeight      = 28
    , seKingCoef           = 3
    , seHelpedKingCoef     = 5
    , seThreatWeight       = 10
    , seAttackedManCoef = -40
    , seAttackedKingCoef = -80
    , seCache = buildCache iface
    }
  where
    iface = SomeRules rules

parseEvaluator :: SimpleEvaluator -> Value -> AT.Parser SimpleEvaluator
parseEvaluator def = withObject "Evaluator" $ \v -> SimpleEvaluator
    <$> pure (seRules def)
    <*> v .:? "use_positional_score" .!= seUsePositionalScore def
    <*> v .:? "mobility_weight" .!= seMobilityWeight def
    <*> v .:? "backyard_weight" .!= seBackyardWeight def
    <*> v .:? "center_weight" .!= seCenterWeight def
    <*> v .:? "opposite_side_weight" .!= seOppositeSideWeight def
    <*> v .:? "border_man_weight" .!= seBorderManWeight def
    <*> v .:? "backed_weight" .!= seBackedWeight def
    <*> v .:? "asymetry_weight" .!= seAsymetryWeight def
    <*> v .:? "pre_king_weight" .!= sePreKingWeight def
    <*> v .:? "king_coef" .!= seKingCoef def
    <*> v .:? "helped_king_coef" .!= seHelpedKingCoef def
    <*> v .:? "threat_weight" .!= seThreatWeight def
    <*> v .:? "attacked_man_coef" .!= seAttackedManCoef def
    <*> v .:? "attacked_king_coef" .!= seAttackedKingCoef def
    <*> pure (seCache def)

instance ToJSON SimpleEvaluator where
  toJSON p = object [
      "use_positional_score" .= seUsePositionalScore p,
      "mobility_weight" .= seMobilityWeight p,
      "backyard_weight" .= seBackyardWeight p,
      "center_weight" .= seCenterWeight p,
      "opposite_side_weight" .= seOppositeSideWeight p,
      "border_man_weight" .= seBorderManWeight p,
      "backed_weight" .= seBackedWeight p,
      "asymetry_weight" .= seAsymetryWeight p,
      "pre_king_weight" .= sePreKingWeight p,
      "king_coef" .= seKingCoef p,
      "helped_king_coef" .= seHelpedKingCoef p,
      "threat_weight" .= seThreatWeight p,
      "attacked_man_coef" .= seAttackedManCoef p,
      "attacked_king_coef" .= seAttackedKingCoef p
    ]

data PreScore = PreScore {
      psNumeric :: ! ScoreBase
    , psMobility :: ScoreBase
    , psBackyard :: ScoreBase
    , psCenter :: ScoreBase
    , psTemp :: ScoreBase
    , psBorder :: ScoreBase
    , psBacked :: ScoreBase
    , psAsymetry :: ScoreBase
    , psPreKing :: ScoreBase
    , psAttackedMen :: ScoreBase
    , psAttackedKings :: ScoreBase
    , psThreats :: ScoreBase
  }

sub :: PreScore -> PreScore -> PreScore
sub ps1 ps2 = PreScore
  { psNumeric  = psNumeric ps1 - psNumeric ps2
  , psMobility = psMobility ps1 - psMobility ps2
  , psBackyard = psBackyard ps1 - psBackyard ps2
  , psCenter   = psCenter ps1 - psCenter ps2
  , psTemp     = psTemp ps1 - psTemp ps2
  , psBorder   = psBorder ps1 - psBorder ps2
  , psBacked   = psBacked ps1 - psBacked ps2
  , psAsymetry = psAsymetry ps1 - psAsymetry ps2
  , psPreKing  = psPreKing ps1 - psPreKing ps2
  , psAttackedMen = psAttackedMen ps1 - psAttackedMen ps2
  , psAttackedKings = psAttackedKings ps1 - psAttackedKings ps2
  , psThreats = psThreats ps1 - psThreats ps2
  }

instance Default PreScore where
  def = PreScore {
            psNumeric = 0
          , psMobility = 0
          , psBackyard = 0
          , psCenter = 0
          , psTemp = 0
          , psBorder = 0
          , psBacked = 0
          , psAsymetry = 0
          , psPreKing = 0
          , psAttackedMen = 0
          , psAttackedKings = 0
          , psThreats = 0
        }

waveRho :: SomeRules -> Side -> (Address -> Bool) -> Address -> ScoreBase -> ScoreBase
waveRho (SomeRules rules) side isGood addr best = go addr
  where
    go :: Address -> ScoreBase
    go addr
      | isGood addr = best
      | otherwise =
        let check :: PlayerDirection -> ScoreBase
            check dir =
              case myNeighbour rules side dir addr of
                Nothing -> 0
                Just dst -> max 0 $ go dst - 1
        in  maximum $ map check $ getForwardDirections rules

buildCache :: SomeRules -> M.Map Address SimpleEvaluatorData
buildCache iface@(SomeRules rules) = M.fromList [(addr, labelData addr) | addr <- getAllAddresses rules]
  where
    labelData addr = SimpleEvaluatorData $ SimpleEvaluatorWeights {
        sewFirst = waveRho iface First (isCenter . aLabel) addr best,
        sewSecond = waveRho iface Second (isCenter . aLabel) addr best
      }

    (nrows, ncols) = boardSize rules

    best = fromIntegral $ nrows `div` 2 - 1

    crow           = nrows `div` 2
    ccol           = ncols `div` 2
    halfCol        = ccol `div` 2
    halfRow        = crow `div` 2

    isCenter (Label col row) =
      (col >= ccol - halfCol && col < ccol + halfCol)
        && (row >= crow - 1 && row < crow + 1)
        -- && (row >= crow - halfRow && row < crow + halfRow)

preEval :: SimpleEvaluator -> Side -> Board -> PreScore
preEval (SimpleEvaluator { seRules = iface@(SomeRules rules), ..}) side board =
  let
    kingCoef = seKingCoef
      -- King is much more useful when there are enough men to help it
--       let (men, _) = myCounts side board
--       in  if men > 3 then seHelpedKingCoef else seKingCoef

    numericScore =
      let (myMen, myKings) = myCounts side board
      in  kingCoef * fromIntegral myKings + fromIntegral myMen

    (nrows, ncols) = bSize board
    crow           = nrows `div` 2
    ccol           = ncols `div` 2
    halfCol        = ccol `div` 2
    halfRow        = crow `div` 2

    isLeftHalf (Label col _) = col >= ccol

    asymetry =
      let (leftMen , leftKings ) = myLabelsCount side board isLeftHalf
          (rightMen, rightKings) = myLabelsCount side board (not . isLeftHalf)
      in  abs $ (leftMen + leftKings) - (rightMen + rightKings)

    isBackedAt addr dir =
      case myNeighbour rules side dir addr of
        Nothing -> True
        Just back -> isPieceAt back board side

    backedScoreOf addr =
      length $ filter (isBackedAt addr) $ getBackDirections rules

    backedScore =
      fromIntegral $ sum $ map backedScoreOf $ allMyAddresses side board

    isBackyard (Label _ row) =
        case boardSide (boardOrientation rules) side of
          Top    -> row == nrows-1
          Bottom -> row == 0

    backyard =
      let (backMen, _) = myLabelsCount side board isBackyard
      in  backMen

    tempNumber (Label col row) =
      case boardSide (boardOrientation rules) side of
        Top    -> nrows - row
        Bottom -> row + 1

    isBorder (Label col row) =
        col == 0 || col == ncols - 1

    borderNumber =
      let (men, _) = myLabelsCount side board isBorder
      in  men

    centerNumber addr = weightForSide side $ sedCenter $ seCache M.! addr

    opponentSideCount =
      sum $ map tempNumber $ myMen side board

    threatsBy addr = sum $ map check $ getForwardDirections rules
      where
        check dir =
          case myNeighbour rules side dir addr of
            Nothing -> 0
            Just f1 ->
              if isFree f1 board
                then case myNeighbour rules side dir f1 of
                       Nothing -> 0
                       Just f2 -> if isFree f2 board
                                    then 1
                                    else 0
                else 0

    threatsCount = sum $ map threatsBy $ myMenA side board

    isPreKing board src = any check $ getForwardDirections rules
      where
        check dir =
          case myNeighbour rules side dir src of
            Nothing -> False
            Just dst -> isLastHorizontal side dst && isFree dst board

    preKings =
      length $ filter (isPreKing board) $ myMenA side board

    mobility = mobilityScore rules side board

    attackedFields = boardAttacked side board
    attackedMen = getPiecesCount (Piece Man side) attackedFields board
    attackedKings = getPiecesCount (Piece King side) attackedFields board

    centerScore =
      let (men, kings) = myAddressesCount' side board centerNumber in men + kings
  in
    PreScore
      { psNumeric  = numericScore
      , psMobility = fromIntegral mobility
      , psCenter   = fromIntegral centerScore
      , psBackyard = fromIntegral backyard
      , psTemp     = fromIntegral opponentSideCount
      , psBorder   = fromIntegral borderNumber
      , psBacked   = fromIntegral backedScore
      , psAsymetry = fromIntegral asymetry
      , psPreKing  = fromIntegral preKings
      , psAttackedMen = fromIntegral attackedMen
      , psAttackedKings = fromIntegral attackedKings
      , psThreats = fromIntegral threatsCount
      }

preEvalBoth :: SimpleEvaluator -> Board -> PreScore
preEvalBoth eval board =
  preEval eval First board `sub` preEval eval Second board

instance Evaluator SimpleEvaluator where
  evaluatorName _ = "simple"

  updateEval e v =
    case AT.parseMaybe (parseEvaluator e) v of
      Nothing -> e
      Just e' -> e'

  evalBoard eval@(SimpleEvaluator {seRules = SomeRules rules, ..}) whoAsks board =
    let ps1 = preEval eval whoAsks board
        ps2 = preEval eval (opposite whoAsks) board

        initCount = initPiecesCount rules
        openingCount = 2 * initCount `div` 3
        endgameCount = initCount `div` 3
--         midgameCount = initCount `div` 2
        count = totalCount board

        crescentAdjustment :: ScoreBase -> ScoreBase -> ScoreBase
        crescentAdjustment from to
          | count >= openingCount = to
          | count <= endgameCount = from
          | otherwise = (from - to) * fromIntegral (count - openingCount) `div` fromIntegral (endgameCount - openingCount) + to

        backyardWeight = crescentAdjustment seBackyardWeight 0
        centerWeight = crescentAdjustment seCenterWeight (seCenterWeight `div` 2)
        tempWeight = crescentAdjustment (seOppositeSideWeight * 2) seOppositeSideWeight 
        asymetryWeight = crescentAdjustment 0 seAsymetryWeight

        positionalScore ps =
          if seUsePositionalScore
            then
              centerWeight * psCenter ps +
              backyardWeight * psBackyard ps +
              tempWeight * psTemp ps +
              seBorderManWeight * psBorder ps +
              seMobilityWeight * psMobility ps +
              seBackedWeight * psBacked ps +
              asymetryWeight * psAsymetry ps +
              sePreKingWeight * psPreKing ps +
              seAttackedManCoef * psAttackedMen ps +
              seAttackedKingCoef * psAttackedKings ps +
              seThreatWeight * psThreats ps
            else 0

        myNumeric = psNumeric ps1
        opponentNumeric = psNumeric ps2

        myScore = Score myNumeric (positionalScore ps1)
        opponentScore = Score opponentNumeric (positionalScore ps2)

    in  if myNumeric == 0
          then loose
          else if opponentNumeric == 0
                 then win
                 else (myScore - opponentScore)

instance VectorEvaluator SimpleEvaluator where

  evalToVector (SimpleEvaluator {..}) = V.fromList $ map fromIntegral $ [
        seMobilityWeight, seBackyardWeight,
        seCenterWeight, seOppositeSideWeight,
        seBackedWeight,
        seAsymetryWeight, sePreKingWeight,
        seKingCoef, seAttackedManCoef,
        seAttackedKingCoef, seBorderManWeight,
        seThreatWeight]

  evalFromVector rules v = SimpleEvaluator {
          seRules = iface
        , seUsePositionalScore = True
        , seMobilityWeight = round (v V.! 0)
        , seBackyardWeight = round (v V.! 1)
        , seCenterWeight = round (v V.! 2)
        , seOppositeSideWeight = round (v V.! 3)
        , seBackedWeight = round (v V.! 4)
        , seAsymetryWeight = round (v V.! 5)
        , sePreKingWeight = round (v V.! 6)
        , seKingCoef = round (v V.! 7)
        , seHelpedKingCoef = round (v V.! 7)
        , seAttackedManCoef = round (v V.! 8)
        , seAttackedKingCoef = round (v V.! 9)
        , seBorderManWeight = round (v V.! 10)
        , seThreatWeight = round (v V.! 11)
        , seCache = buildCache iface
      }
    where
      iface = SomeRules rules
        

-- data ComplexEvaluator rules = ComplexEvaluator {
--     ceRules :: rules
--   , ceCaptureManCoef :: Score
--   , ceCaptureKingCoef :: Score
--   }
--   deriving (Eq, Show)
-- 
-- instance GameRules rules => Evaluator (ComplexEvaluator rules) where
--   evaluatorName _ = "complex"
-- 
--   evalBoard ce whoAsks whoMovesNext board =
--     let rules = ceRules ce
--         allMyMoves = possibleMoves rules whoAsks board
--         allOpponentMoves = possibleMoves rules (opposite whoAsks) board
-- 
--         myMoves = if whoAsks == whoMovesNext
--                     then allMyMoves
--                     else filter (not . isCapture) allMyMoves
--         opponentMoves = if whoAsks == whoMovesNext
--                           then filter (not . isCapture) allOpponentMoves
--                           else allOpponentMoves
-- 
--         (myMen, myKings) = myCounts whoAsks board
--         (opponentMen, opponentKings) = myCounts (opposite whoAsks) board
-- 
--     in  if (myMen == 0 && myKings == 0) || null allMyMoves
--           then {- trace (printf "Side %s loses" (show whoAsks)) -} (-win)
--           else if (opponentMen == 0 && opponentKings == 0) || null allOpponentMoves
--                  then {-  trace (printf "Side %s wins" (show whoAsks)) -} win
--                  else let movesScore s ms = if all isCapture ms
--                                                then let (men, kings) = unzip [capturesCounts rules move board | move <- ms]
--                                                         maxMen = if null men then 0 else maximum men
--                                                         maxKings = if null kings then 0 else maximum kings
--                                                     in  fromIntegral $
-- --                                                         trace (printf "Side %s possible captures: %s men, %s kings" (show s) (show men) (show kings)) $
--                                                         (ceCaptureManCoef ce) * fromIntegral maxMen + (ceCaptureKingCoef ce) * fromIntegral maxKings
--                                                else fromIntegral $ length ms
--                           myMovesScore = movesScore whoAsks myMoves
--                           opponentMovesScore = movesScore (opposite whoAsks) opponentMoves
--                       in --  trace (printf "Side %s moves score %d, opponent moves score %d, total score = %d" (show whoAsks) myMovesScore opponentMovesScore (myMovesScore - opponentMovesScore)) $
--                           (myMovesScore - opponentMovesScore)
--