Lou Franco is an engineer at Atlassian working on Trello iOS. He has been developing for mobile devices since 2000 and co-authored a book for beginner … More about Lou Franco

# Building Killer Robots: Game Behavior In iOS With Fuzzy Logic Rule Systems

Imagine that it’s a hot day. The sun is out, and the temperature is rising. Perhaps, every now and then, there’s a cool breeze. A good song is playing on the radio. At some point, you get up to get a glass of water, but the exact reason why you did that at that particular time isn’t easy to explain. It was “too hot” and you were “somewhat thirsty,” but also maybe “a little bored.” Each of these qualities isn’t either/or, but instead fall on a spectrum of values. In contrast, our software is usually built on Boolean values. We set `isHot` to `true` and if `isHot && isThirsty && isBored`, then we call `getWater()`. If we use code like this to control our game characters, then they will appear jerky and less natural. In this article, we’ll learn how to add intelligent behavior to the non-player characters of a game using an alternative to conventional Boolean logic.

In contrast, our software is usually built on Boolean values. We set `isHot` to `true` and if `isHot && isThirsty && isBored`, then we call `getWater()`. If we use code like this to control our game characters, then they will appear jerky and less natural. In this article, we’ll learn how to add intelligent behavior to the non-player characters of a game using an alternative to conventional Boolean logic.

To see why Boolean logic isn’t good enough, look at this graph of `isHot` versus temperature.

It jumps from `FALSE` to `TRUE` at 88°F (31°C), and when I combine this with a similar graph for boredom (based on the number of minutes ago a good song was playing), I get something like this:

But if we used decimal numbers to represent `hotness`, `thirstiness` and `boredom`, we might be able to combine them and get more lifelike behavior. Here is the same graph of `hotness` and `boredom` using values from 0 to 1.

If we use values from this formula to drive behavior, our characters will gradually move from one state to another.

This is called fuzzy logic, and in iOS games we can use GameplayKit’s rule system classes to implement behavior based on it. Fuzzy Logic works well for driving complex behavior of the non-playable characters of a game. Let’s build a small robot-fighting game in which we pit a fuzzy-logic-based AI against a random one.

### GameplayKit Rule Systems

In a previous article, we learned a simple way to use GameplayKit’s rule systems and how they make it easy to model and reuse conditional logic. We used its features to create a puzzle game in which the rules were black and white and well represented by Boolean values. To do this, we created `GKRule` objects whose `init` takes a predicate and a grade. It looked like this:

``````init(predicate: NSPredicate, assertingFact fact: NSObjectProtocol,
``````

The predicate determines whether the rule applies. If it’s true, the rule establishes a fact at a given grade. In our examples, we always used a grade of 1.0, which meant certainty (absolutely trueness). The default value of any fact is 0.0, or absolute falseness. As a reminder, we made a game with five buttons, where you won if you tapped each one once and none of them twice. Our rule system was defined with this:

``````func predicateRulesForLevel001() -> [GKRule] {
return [
GameOutcome.reset.assertIf("ANY \$b.tapCount == 2"),
GameOutcome.win.assertIf("ALL \$b.tapCount == 1"),
]
}
``````

The `\$b` in the expression is looked up from the game state passed to our `evaluate` function and contains an array of buttons. We reset the game if any of them had a tap count of two, and we won the game if all of them were tapped once.

The general idea is that the rules are expressions that set the grade of a fact based on the game state. We used grades of 1.0 and 0.0 to represent `TRUE` and `FALSE`.

Once we have rules, we can run them through a rule system, like so:

``````func evaluate(state: [String: Any], rules: [GKRule]) -> GKRuleSystem {
let rs = GKRuleSystem()
rs.evaluate()

return rs
}
``````

Then, we can look up the facts in the returned rule system (`rs`). In the previous article, only one fact would be asserted with 1.0 because the outcomes were mutually exclusive. The two possible facts were whether we won or lost the game, and only one could be true at a time.

For very simple games in which everything happens with certainty, Boolean-based conditionals make a lot of sense. However, as we saw in the Boolean graphs, if we used this to drive the behavior of in-game characters that are meant to seem more natural and less deterministic, then we’d want a way to transition from one behavior to another.

#### Transitioning to Fuzzy Values

We used 0.0 to represent `FALSE` and 1.0 to represent `TRUE`, but we can pass any number from 0.0 to 1.0 as a fact’s grade, and doing so says that the fact’s trueness isn’t certain or has a probability. Values closer to 0.0 are more false, and those closer to 1.0 are more true.

Furthermore, every fact can be asserted with some kind of certainty, not just one we know is true. Doing so means we don’t need predicates any more (since we always want to fire all rules). So, we can simplify rule creation with this function:

``````public func fuzzyRule(action: @escaping (GKRuleSystem, GameState) -> ()) -> GKRule {
return GKRule(blockPredicate: { _ in true }, action: { (rs) in
return action(rs, GameState(state: rs.state))
})
}
``````

Using fuzzy rules, rather than rules that assert true and false facts, is especially useful for modeling non-player characters in games, so that their behavior seems to emerge from complex stimuli.

### Robots Made With Fuzzy Logic

As an example, let’s make a game that has two robots that are randomly spawned on a board. The code from this article is available in an Xcode playground on GitHub.

We are going to use the same `GKRule` and `GKRuleSystem` classes from iOS’ GameplayKit that we used for the conditional logic game from the previous article. But by using a more sophisticated state and by asserting multiple facts with a range of grades, we’ll be able to make a much more complex game.

Here’s what the game looks like when it starts.

Neither robot can see the other, but at each point they can:

• turn right,
• turn left,
• move forward,
• use a radar to get the position of the other robot,
• fire a laser.

In our game, we’ll represent this as an `enum`:

``````public enum RobotAction: NSString {
case turnRight
case turnLeft
case moveForward
case fireLaser

public static let allValues = [
turnRight,
turnLeft,
moveForward,
fireLaser]
}
``````

If they use their radar and it’s successful, then they will only know their opponent’s position at that point in time. Each use of the laser or radar depletes it, and it charges by one cell’s worth of range each tick of the clock. Here’s what it looks like if we make the robots roll a die and choose behaviors randomly.

If I let this go on for a while, eventually one of the robots will accidentally win. We can do better than that.

If there were no uncertainty, then an easy strategy would be to turn towards your opponent, march towards them and fire when they are in range.

Given a `GameState` struct that can tell us our position, direction and the last known position of the opponent, a simple attack strategy would look like this:

``````// Move towards enemy and fire when facing them
func attackAction(state: GameState) -> RobotAction? {
// Calculate the distance between us and the opponent ...
// if we move forward
let fwdDist = state.enemyDistance(from: state.forwardCell(from: state.myDir))
// if we turn left and then move forward
let leftFwdDist = state.enemyDistance(from: state.forwardCell(from: state.myDir.left()))
// if we turn right and then move forward
let rightFwdDist = state.enemyDistance(from: state.forwardCell(from: state.myDir.right()))

if fwdDist <= leftFwdDist && fwdDist <= rightFwdDist {
// Only move forward if we are not within laser range
if fwdDist > CGFloat(state.laserCharge) {
return .moveForward
}
} else {
return (leftFwdDist <= rightFwdDist) ? .turnLeft : .turnRight
}
return nil
}
``````

When the opponent is right in front of us, we return `nil` to avoid crashing into them. This also gives us a chance to use our laser from this position.

This function breaks down if our opponent quickly abandons their position, because we’d march towards their old location with certainty.

### Fuzzy Logic Rule Systems

What we want to do is establish certainty as part of the game state. One feature of rule systems is that rules can see the facts that other rules have asserted (which creates an order dependency, which we’ll describe later).

To build up more complex rules later, we’ll first define uncertainty (and certainty), nearness and the percentage of laser and radar we have available. Once we have those as a base, we’ll learn about the fuzzy equivalents to Boolean `AND` and `OR` that we can use to combine them to express a behavior such as shooting when we have a charged laser and when the enemy is near.

For us, certainty is based on how long ago the radar was used successfully. If we got a position a while ago, then we’d be less sure that it’s worth using:

``````public let posUncertaintyRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
let posUncertainty: Float = min(1.0, 0.05 * Float(state.ticksSinceEnemyKnown))
}
``````

The subexpression `0.05 * Float(state.ticksSinceEnemyKnown)` is `0.0` when we have had a successful radar hit. Each tick of the game clock makes this 5% more uncertain, until it maxes out at 100% uncertain. If we want the robots to have less trust in each other’s position, we could make the uncertainty go up faster by using a larger coefficient, like `0.08`. This would make them use radar more often in an attempt to increase certainty.

A rule can assert only one fact, but sometimes it’s useful to readily have the `NOT` version of a fuzzy logic variable. In fuzzy logic, the `NOT` of a `fuzzyValue` is `1.0 - fuzzyValue`. Here’s the certainty rule based on that:

``````public let posCertaintyRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
let posCertainty = 1.0 - posUncertainty
}
``````

When we implement rules to use the laser or radar, we’ll want to know how charged they are. The fuzzy value we’ll use is their percentage of the maximum charge:

``````public let hasLaserRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
let hasLaserValue = Float(state.laserCharge) / Float(state.maxLaser)
}

public let hasRadarRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
}
``````

It’s also useful to have a notion of nearness. We’ll use the opponent’s distance over the maximum distance to get us a value from 0.0 to 1.0, and then subtract from 1.0 so that near values are close to 1.0.

``````public let isNearRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
let diff = state.enemyDistance()
let maxDiff = state.maximumDistance()
let isNearValue = Float(1.0 - diff / maxDiff)
}
``````

These rules are a base that can be combined with each other into further rules. For example, suppose we want to use the radar if we have a fully charged radar or if we are uncertain of the opponent’s position. We want to fire a laser if the opponent is near, if we have a charge and if their position is more certain. The `AND` and `OR` seem like Boolean operations, but as we’ll see, fuzzy logic values require a different way to implement them.

#### More Fuzzy Logic Operators

In regular conditional logic, the `AND` operator is true if all of its operands are true and false otherwise. You can think of this as returning the least true operand. If we had fuzzy values of `(isNear: 0.9, hasLaser: 0.5, posCertain: 0.8)`, then we could mimic an `AND` by using the minimum value, or `0.5` in this case.

`GKRuleSystem` can do that for us with pre-established facts using its `minimumGrade(forFacts:)` method. We’ll use this in our laser rule like this:

``````let shouldFireRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
if !state.isFacingWall() {
RobotFact.hasLaser.rawValue,
RobotFact.posCertainty.rawValue,
RobotFact.isNear.rawValue]
)
}
}
``````

Because `minimumGrade(forFacts:)` returns the smallest value of the passed-in facts, it acts like a Boolean `AND` but for fuzzy values. You could read this as:

``````let shouldFire = hasLaser && posIsCertain && isNear
``````

Similarly, an `OR` is true if any of its operands are true. So, it returns the operand that is most true. We can use `maximumGrade(forFacts:)` in the radar rule to implement that.

``````let shouldRadarRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in
RobotFact.posUncertainty.rawValue]
)
}
``````

The equivalent Boolean expression would be:

``````let shouldRadar = hasRadar || posIsUncertain
``````

This makes more sense with fuzzy values because it might be worth firing the radar when the robot is confused, even if the radar charge is low. With Boolean values, it would never make sense to use the radar if it is uncharged.

To finish off the simulation, we just need to reincorporate our attack function and introduce a wander rule. When we are sure of the opponent’s position, we want to attack:

``````let attackRule = fuzzyRule { (rs: GKRuleSystem, state: GameState) in

if let action = attackAction(state: state) {
}
}
``````

As the radar hit becomes a distant memory, this rule will have less of an effect. If we did nothing, the robot would stand still and use its radar repeatedly, just hoping that its opponent moves within range. To make it more interesting, we’ll have the robot wander around when it has nothing else to do.

This function has it circle around an inner loop, three cells away from the wall:

``````let wanderRule = fuzzyRule { (rs: GKRuleSystem, state:GameState) in

if (state.isInBounds(pos: state.forwardCell(from: state.myDir, steps: 3))) {
} else {
}
}
``````

The expression `state.isInBounds(pos: state.forwardCell(from: state.myDir, steps: 3))` returns `TRUE` if the robot could move forward and still be three cells away from a wall. If this is `FALSE`, it turns left. The effect is that the robot wanders around the middle of the board when the uncertainty of the other robot’s position is high. At the same time, the radar will be charging, so the radar rule will supersede this one at times, and with the combination of wandering around and using the radar, this robot will eventually find the other one.

With all of these rules created, we can run the rule system. Remember that some rules depend on others, which we can express by the order in which we add them to the system.

``````func nextAction(state: [String: Any]) -> RobotAction {
let ruleSystem = GKRuleSystem()

// Add the rules such that dependent ones come later in the array
posUncertaintyRule,
posCertaintyRule,

isNearRule,
hasLaserRule,

attackRule,
shouldFireRule,
wanderRule,
])
ruleSystem.evaluate()

// Find the action facts that have the highest grade
let maxFacts = RobotAction.allValues.flatMap { ruleSystem.grade(forFact: \$0.rawValue) == maxGrade ? \$0 : nil }

// Choose randomly from the highest graded facts if there is a tie.
return maxFacts[GKRandomDistribution(lowestValue:0, highestValue: maxFacts.count - 1).nextInt()]
}
``````

Because it’s possible that the rule system will assert multiple facts of the same grade, on the last line of the function, we’ll randomly choose from among them.

With these rules, the red robot is now much better at fighting and almost always wins:

• The playground associated with this article has the entire game, using SpriteKit’s `SKScene` and `SKSpriteNode` to implement the visuals of the game. You can learn about those classes in the Smashing Magazine series on them: part 1, part 2, part 3. (da, vf, yk, al, il)