You could refactor most of your Fact data as a record object
data FactRec = FR {
canIntercept :: [(ObjId,ObjId)], -- use lists for things you previously had multiple times
factBestPosition :: [(Team,Spot,Spot)], -- not sure about this one, maybe two entries not one list
kickOff :: Bool,
ballCarrier :: ObjID,
factBestShootingVector :: Velocity3,
factBestPassingVector :: Velocity3,
.....
}
Which gives you bulti-in accessor functions
if kickOff fr then something else somethingelse
So you wouldn't need to write all the check functions, but instead do
rule_shoot facts = message (ballCarrier facts, shoot $ goalVector facts)
If there are facts that genuinely might or might not be there, they could be of type Maybe something, and if there are facts that can be there an arbitrary number of times, they can be lists.