{-# 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))]