packages feed

Rasenschach-0.1.2: Rules.hs

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
module Rules (
   runRules
  ,runner
  ,parseRule
  ,basicRules
  ,Rule(..)
  ,Priority(..)
  ,RuleId(..)
  ,ParseErrorId
  ,ParseErrorMsg)

where

import FRP.Yampa.Geometry

import Data.List
import Data.Function (on)
import Data.Maybe
import Debug.Trace
 
import Object
import BasicTypes
import Message
import AL
import Helper

-- *************************************************************************
--
-- Framework for defining rules
--
-- ************************************************************************


concatMaybe :: Maybe [a] -> [a]
concatMaybe Nothing = []
concatMaybe (Just xs) = xs

uncollect :: [(a, [b])] -> [(a, b)]
uncollect = concatMap (\(a, bs) -> zip (repeat a) bs)

runRules' ::  [ObjId] -> Facts -> [VisibleState] -> RuleBase -> [(ObjId, [(Priority, MessageBody)])]
runRules' teamMates' facts vss =
   collect . map (\(p,(o,m)) -> (o, (p,m))) . uncollect
           . map (\rule -> (opPriority rule, concatMaybe $ opRule rule teamMates' facts vss))

-- collect all the messages for each player that have the lowest prio
runRules :: [ObjId] -> Facts -> [VisibleState] -> RuleBase -> [(ObjId, MessageBody)]
runRules teamMates' facts vss =
  concatMap (\(o, pms) ->
         let pmsSort = sortBy (compare `on` fst) pms
             lowest = fst $ head pmsSort
             relevantMsgs = map snd $ takeWhile ((lowest ==) . fst) pmsSort
         in zip (repeat o) relevantMsgs)
  . runRules' teamMates' facts vss


-- *************************************************************************
--
-- Basic rules for all teams, not changeable by user
--
-- ************************************************************************

rule_kick_off :: RuleFunction
rule_kick_off _ facts vss = do
    factKickOff facts []
    att <- factAttacking facts []
    FPTeam me <- factWhoAmI facts []
    factEq facts [FPTeam me, att]
    let x = if me == Home then 1 else (-1)
    return $ [(vsObjId p, tm (TPTKickedOff, tspNull))| p <- teamPlayers Home vss ++ teamPlayers Away vss]
                                                   --     , vsObjId p /= ballCarrier]
          ++ [(vsObjId game, GameMessage (GTRunGame, (snd . vsGameState) game))]
          ++ [(ballCarrier, pm (PPTLoseMe, BSPShoot (x *^ (vector3 (-19) (-3) 0)))) | isJust bc]
    where game = fetchGameVS vss
          bc@(Just ballCarrier) = playerWithBall vss

-- Weitere Verbesserungsmöglichkeiten:
-- a. checken, dass nicht der einwerfende Spieler der erste ist, der den Ball wieder
--    aufnimmt
-- b. nicht sofort werfen, sondern eine halbe Sekunde oder so warten (vielleicht FactThrowinIn
--    erst dann einstellen, wenn eine Mindestzeit rum ist?) Sonst sieht es doof aus
-- c. der einwerfende Spieler soll in Richtung des Spots schauen, zu dem er werden will;
--    vielleicht wäre ein TS "AimingForThrowIn" gut
-- d. solange zielen (AimingForThrowIn), bis Güte des Spots über einen Schwellenwert
--    liegt ODER die Zeit seit Start der Einwurfaktion überschritten ist
rule_throw_in :: RuleFunction
rule_throw_in _ facts _ = do
    FPPlayerId p <- factThrowingIn facts []
    FPFromTo curr dest <- factBestPosition facts []
    return $ (p, pm (PPTLoseMe,
#if DEBUG_MODE
                    trace ("RULE=" ++ show (BSPShoot (towards curr dest)) ++ show "; " ++ show dest)
#endif
                      BSPShoot (towards curr dest))) :
              (1, GameMessage (GTBallInPlay, GPTeamPosition Home (-1) [] (Point2 0 0) 0 False InPlay)) :
             [(p, tm (TPTReposition, tspNull))]

rule_stop_idling :: RuleFunction
rule_stop_idling _ facts _ = do
    FPPlayers ps <- factIdling facts []
    return $ map (\p -> (p, tm (TPTHoldPosition, tspNull))) ps


rule_punt :: RuleFunction
rule_punt _ facts _ = do
    FPPlayerVector p v <- factPunt facts []
    return $ [(p, pm (PPTLoseMe, BSPShoot v))]


-- was passiert beim Anstoß:
-- * 2 Spieler stehen am Anstoßpunkt
-- * ein Spieler hat den Ball
-- * auf Knopfdruck (bei eigenem Spieler) oder auf AI-Befehl geht der Ball von einem
--   auf den anderen Spieler über
-- * vorher kann sich keiner irgendwie drehen oder sonstwas
-- * Frage: Laufen dann alle in Position? Oder wird "neu aufgestellt?" Letzteres ist
--   wohl deutlich einfacher... Dann braucht es auch das ganze Status-Gehampel nicht...
-- * Frage: Ist man vor dem eigentlich Anstoß innerhalb der reactimate-Schleife oder
--   außerhalb?
--   PRO außerhalb: Ganz wenig Rumgehampel mit Status und Knorz (sonst muss mindestens mal
--        die handgesteuerten Spieler in einen Status versetzen, in dem er nicht
--        gesteuert werden kann AUSSER wenn er selber den Anstoß macht, dann ist der
--        einzige Steuerimpuls aber der Anstoß an sich)
--   CON außerhalb: Rendering und Keyboard-Knorzung muss noch mal gebastelt oder

--       mindestens mal angestoßen werden...
--   klingt so, als sollte man es erst mal mit innerhalb probieren...

basicRules :: [Rule]
basicRules = [Rule (RuleId (-1000))  "throw in"    (Priority 1)  rule_throw_in
             ,Rule (RuleId (-1001))  "kickoff"     (Priority 0)  rule_kick_off
             ,Rule (RuleId (-1002))  "punt"        (Priority 4)  rule_punt
             ,Rule (RuleId (-1003)) "stop idling" (Priority 10) rule_stop_idling
             ]

-- just some dummy stuff to later integrate more easily...

type Clause = Facts -> [FactParam] -> Maybe FactParam
type MsgMaker =  [VisibleState] -> [FactParam] -> [Message]

type ParamId = Int
type Statement = [(Maybe ParamId, Clause, [RuleParam])]
data RuleParam = PId ParamId | PConst FactParam deriving (Show)

-- wrapper to run a parsed rule (set of clauses and message function)
runner ::  Statement -> (MsgMaker, [RuleParam]) -> RuleFunction
runner clauses msgMaker myTeamMates facts vss =
  run myTeamMates vss facts clauses msgMaker emptyAL

-- first runs a set of clauses and collects facts, then runs messaging function
-- lets only those messages through that are intended for own team (no cheating!)
run ::  [ObjId] -> [VisibleState] -> Facts -> Statement ->
       (MsgMaker, [RuleParam]) -> AL ParamId FactParam -> Maybe [Message]
run myTeamMates vss _ [] (msgMaker, params) paramFacts =
    return $ filterTeam myTeamMates $ msgMaker vss $ fetchParams paramFacts params
run myTeamMates vss facts ((pId, clause, clauseParams):rs) msg paramFactsSoFar = do
    fp <- clause facts $ fetchParams paramFactsSoFar clauseParams
    run myTeamMates vss facts rs msg $ maybeInsertAL pId fp paramFactsSoFar

maybeInsertAL :: Maybe k -> a -> AL k a -> AL k a
maybeInsertAL Nothing _ paramFactsSoFar = paramFactsSoFar
maybeInsertAL (Just pId) fp paramFactsSoFar = insertAL pId fp paramFactsSoFar

filterTeam :: Eq a => [a] -> [(a, t)] -> [(a, t)]
filterTeam teamMates' =
    filter (\(oid,_) -> elem oid teamMates')

fetchParams :: AL ParamId FactParam -> [RuleParam] -> [FactParam]
fetchParams _ [] = []
fetchParams pvs (p:ps) =
    case p of
        PId p'    -> pvs ! p' : fetchParams pvs ps
        PConst c -> c : fetchParams pvs ps


type ParamName = String
type RulePriority = Int
type ParseErrorId = Int
type ParseErrorMsg = String

parseRule :: [String] ->
             Either (ParseErrorId, ParseErrorMsg)
                    (RuleName
                    ,RulePriority
                    ,Statement
                    ,(MsgMaker, [RuleParam]))

parseRule ls = do
--  let ls = lines ruleString
  let ln = length ls
  checkRuleStructure ls
  (ruleName, rulePrio) <- parseRuleHead (ls !! 0)
  (clauses, params) <- gatherClauses (take (ln - 2) (tail ls)) []
  (msg, msgParams) <- parseMsg (last ls) params
  return (ruleName, rulePrio, clauses, (msg, msgParams))

checkRuleStructure :: Num t => [a] -> Either (t, String) ()
checkRuleStructure ls =
   if length ls  < 3 then
      Left $ (10, "rule must consist of rule head, at least one clause and message")
   else
      Right ()

gatherClauses :: [String] -> [(ParamId, ParamName)] ->
                 Either (ParseErrorId, ParseErrorMsg)
                        (Statement, [(ParamId, ParamName)])
gatherClauses [c] acc = do
-- (Just (paramId, paramName), clause, params) <- parseClause c acc
   (maybeVar, clause, params) <- parseClause c acc
   return ([(fst `fmap` maybeVar, clause, params)], pushIfJust maybeVar acc)
gatherClauses (c:cs) acc = do
   (maybeVar, clause, params) <- parseClause c acc
   (restClauses, newAcc) <- gatherClauses cs (pushIfJust maybeVar acc)
   return ((fst `fmap` maybeVar, clause, params) : restClauses, newAcc)

pushIfJust :: Maybe a -> [a] -> [a]
pushIfJust Nothing xs = xs
pushIfJust (Just x) xs = x : xs

parseRuleHead :: String -> Either (ParseErrorId, ParseErrorMsg)
                                  (RuleName, RulePriority)
parseRuleHead rh =
   let ws = words rh
   in
       if null ws || head ws /= "rule" || length ws /= 4 ||
          ws !! 2 /= "priority" ||
          null (reads (ws !! 3) :: [(Int, String)])
       then Left (1, "rule head must be of form 'rule rulename priority xxx'")
       else Right (ws !! 1, fst $ head (reads (ws !! 3) :: [(Int, String)]))

parseClause :: String -> [(ParamId, ParamName)] ->
               Either (ParseErrorId, ParseErrorMsg)
                      (Maybe (ParamId, ParamName), Clause, [RuleParam])
parseClause clause params = do
   let tokens = words clause
   hasVar <- checkClauseStructure tokens
   maybeVar <- checkVariable params (tokens !! 0) hasVar
   fact <- checkFact (tokens !! (if hasVar then 2 else 1))
   ps <- parseParams params (drop (if hasVar then 3 else 2) tokens)
   return (maybeVar, fact, ps)

checkClauseStructure :: Num t => [String] -> Either (t, String) Bool
checkClauseStructure tokens =
  if length tokens >= 3 && tokens !! 1 == "is"
  then Right True
  else if length tokens >= 2 && tokens !! 0 == "check"
  then Right False
  else Left (2, "clause must be of form 'var is fact params' or 'check fact params': " ++
                concat (zipWith (++) tokens (repeat " ")))

checkVariable :: [(ParamId, ParamName)] -> String -> Bool ->
                 Either (ParseErrorId, ParseErrorMsg) (Maybe (ParamId, ParamName))
checkVariable params token True =
   if elem token (map snd params)
   then Left (2, "variable name already used: " ++ token)
   else Right $ Just (newParamId params, token)
checkVariable _ _ False = Right Nothing

newParamId :: (Num a, Ord a) => [(a, b)] -> a
newParamId params =
   if null params then 1
   else 1 + (maximum $ map fst params)

parseParams :: Num t => [(ParamId, String)] -> [String] -> Either (t, String) [RuleParam]
parseParams _ [] = Right []
parseParams params ("scalar":tokens) = do
    (scalar, rest) <- checkParamScalar tokens
    result <- parseParams params rest
    return $ (PConst (FPScalar scalar)) : result
parseParams params ("spot":tokens) = do
    (x, y, rest) <- checkParamSpot tokens
    result <- parseParams params rest
    return $ (PConst (FPSpot $ Spot x y)) : result
parseParams params (paramName:tokens) = do
    pid <- checkParamName params paramName
    result <- parseParams params tokens
    return $ (PId pid) : result

checkParamScalar :: Num t => [String] -> Either (t, String) (Double, [String])
checkParamScalar tokens =
    if null tokens then Left (4, "unexpected end of clause after scalar keyword")
    else if null (reads (head tokens) :: [(Double, String)])
         then Left (5, "no number after scalar: " ++ head tokens)
    else Right (fst $ head (reads (head tokens) :: [(Double, String)]), tail tokens)
checkParamSpot :: Num t => [String] -> Either (t, String) (Double, Double, [String])
checkParamSpot tokens =
    if length tokens < 2 then Left (4, "unexpected end of clause after spot keyword")
    else if null (reads (tokens !! 0) :: [(Double, String)]) ||
            null (reads (tokens !! 1) :: [(Double, String)])
         then Left (5, "no number after spot: " ++ (tokens !! 0) ++ ", " ++ (tokens !! 1))
    else Right (fst $ head (reads (tokens !! 0) :: [(Double, String)]),
                fst $ head (reads (tokens !! 1) :: [(Double, String)]),
                drop 2 tokens)
checkParamName :: Num t => [(b, String)] -> String -> Either (t, String) b
checkParamName params paramName =
    case find ((paramName ==) . snd) params of
        Just (pid, _) -> Right pid
        Nothing -> Left (6, "parameter not known: " ++ paramName)

parseMsg :: String -> [(ParamId, ParamName)] ->
            Either (ParseErrorId, ParseErrorMsg)
                   (MsgMaker, [RuleParam])
parseMsg input params = do
   let tokens = words input
   checkMsgStructure tokens
   msg <- checkMsg (tokens !! 1)
   ps <- parseParams params (drop 2 tokens)
   return (msg, ps)

checkMsgStructure :: Num t => [String] -> Either (t, String) ()
checkMsgStructure tokens =
  if null tokens || length tokens < 2 ||
     tokens !! 0 /= "send"
  then Left (8, "message must be of form 'send msg params': " ++
                concat (zipWith (++) tokens (repeat " ")))
  else Right ()

checkMsg :: Num t => String -> Either (t, String) MsgMaker
checkMsg "msgKick" = Right msgKick
checkMsg "msgPassTo" = Right msgPassTo
checkMsg "msgIntercept" = Right msgIntercept
checkMsg x = Left (3, "no valid message: " ++ x)

checkFact :: String -> Either a (Facts -> FactFunction)
checkFact "factCanIntercept" = Right factCanIntercept
checkFact "factIsCloseTo" = Right factIsCloseTo
checkFact "factInLineWith" = Right factInLineWith
checkFact "factBestFreePlayer" = Right factBestFreePlayer
checkFact "factNearestAIPlayer" = Right factNearestAIPlayer
checkFact "factBallIsFree" = Right factBallIsFree
checkFact "factAttacking" = Right factAttacking
checkFact "factThrowingIn" = Right factThrowingIn
checkFact "factBestPosition" = Right factBestPosition
checkFact "factKickOff" = Right factKickOff
checkFact "factBallCarrier" = Right factBallCarrier
checkFact "factPlayerSpot" = Right factPlayerSpot
checkFact "factSpotValue" = Right factSpotValue
checkFact "factBestShootingVector" = Right factBestShootingVector
checkFact "factBestPassingVector" = Right factBestPassingVector
checkFact "factPunt" = Right factPunt
checkFact "factIdling" = Right factIdling
checkFact "factWhoAmI" = Right factWhoAmI
checkFact "factEq" = Right factEq
checkFact "factGT" = Right factGT
checkFact "factGetVector" = Right factGetVector

msgKick :: MsgMaker
msgKick _ [FPPlayerId ballCarrier, FPVector goalVector] =
  [(ballCarrier, tm (TPTKickTowards,
                     (TacticalStateParam Nothing (Just goalVector)
                                         False Nothing Nothing Nothing Nothing)))]

msgPassTo :: MsgMaker
msgPassTo _ [FPPlayerId ballCarrier, FPPlayerId receiver] =
  [(ballCarrier, tm (TPTKickTowards,
                     (TacticalStateParam Nothing Nothing False
                                         (Just receiver) Nothing (Just RTLow) Nothing)))]

msgIntercept :: MsgMaker
msgIntercept _ [FPPlayerId np, FPSpot p1] =
  [(np, tm (TPTIntercept,
                     TacticalStateParam (Just $ spotToPoint p1) Nothing False Nothing Nothing Nothing Nothing))]