Previous tutorial – Download files – Next tutorial

*Taking turns – Think before you act – How to do “What to do?” – Walking with a purpose – Attacking, the one thing we can agree on *

**Taking turns**

I think of Game:checkMove() as our main game loop. After all, this is where the program reacts to whatever the player wants to do and leads to calls to every other function from there. Which means, if we’re going to give the monsters a chance to act, we need to modify Game:checkMove() to make it happen.

function Game:checkMove(dx, dy) local entry, layer = self.world:getTileKey(self.hero.x + dx, self.hero.y + dy) if layer == LAYER_ENVIRONMENT then local tile = manual:getEntry("environment", entry) if tile.blocked ~= true then self.world:moveHero(self.hero, dx, dy) self:turnOver() else self.msg:add("that's a " .. tile.name, MSG_DESCRIPTION) end elseif layer == LAYER_MONSTERS then self:attackMonster(self.hero.x + dx, self.hero.y + dy) self:turnOver() else self.world:moveHero(self.hero, dx, dy) self:turnOver() end end

After the hero either moves or attacks, let’s create the new function Game:turnOver() that let’s the program and the monsters react to what the hero does. The core of Game:turnOver() is simply looping through the self.monsters.list and giving each monster a chance to act.

function Game:turnOver() --each monster gets a turn for id, m in pairs(self.monsters.list) do if m then --update their state based on current conditions self.monsters:updateState(m, id, self.hero) -- create a 500ms delay before calling the AI Timer.delayedCall(500, function() self:monsterAI(m) end) --at the end of the turn, check for hero death end end end

In the for-loop, the program decides two things. First, it calls updateState() in the Monsters class to figure out what the monster wants to do. Second, it calls monsterAI() to make it happen. The subtlety here is that the monsterAI() is called within Timer.delayedCall() which allows us to slow the game down so the player can see each monster take their turns.

## Think before you act

When people considering implementing intelligent monsters , there’s a couple of ways to do it. Programming theory is fun so I encourage you to read up on it and try to implement the coolest algorithm you find for your monsters. Here though, I’m going to forgo a lot of the theory regarding evolving or recurrent neural networks, genetic algorithms or learned information state machines. But we should still talk about the very basics of State. What do programmers mean by State? State for a monster, means they get to choose from a list of options about what to do. They can only choose one state at a time, and a State machine, our Monsters:updateState() function, implements how to change from one option to another.

I keep it pretty simple here with three states to choose from

"attack" "move" "flee"

It’s up to you to decide what causes a monster to stop attacking and start fleeing. We’ll use these variables added to the Monster class to decide what gets a monster from one state to another.

self.state = "move" self.bloodied = false self.seesHero = false self.inRange = false self.friends = false

At the start of the game, all the monsters start out in the “move” state and a call to updateState() goes through the logic of what gets them out of “move” and into one of the other states. For us, this will depend on whether they are bloodied or not, whether they can see and are in range of the hero and also if there are nearby monsters.

Other programs get way more sophisticated with state and I’m sure yours will to. For instance, instead of the three listed above, consider

"attack" "berserk" "move" "sleep" "flee" "guard" "quest"

Monster behavior can get quite sophisticated quite fast. So let’s look at a simple Monsters:updateState() function.

function Monsters:updateState(m, id, hero) --update the bloodied variable m.bloodied = (m.hp <= m.bloodiedHP) --update the friends variable for i = 1, #self.list do if id ~= i then local other = self.list[i] if math.abs(m.x - other.x) <= 6 or math.abs(m.y - other.y) <= 6 then m.friends = true end end end --update the seesHero variable m.seesHero = (math.abs(m.x - hero.x) <= 6 or math.abs(m.y - hero.y) <= 6) --if not, just move if not m.seesHero then m.state = "move" else --update based on their tactical role if m.tactics == "minion" then --minions flee if bloodied or alone if m.bloodied or not m.friends then m.state = "flee" else m.state = "move" end elseif m.tactics == "skirmisher" then --skirmishers flee if bloodied and alone if m.bloodied and not m.friends then m.state = "flee" else m.state = "move" end --skirmishers change to their melee weapon if the hero is close if (math.abs(m.x - hero.x) == 1 and math.abs(m.y - hero.y) == 1) then m.weapon = m.weapons[2] end elseif m.tactics == "soldier" and m.bloodied then --soldiers change to their berserk weapon if bloodied m.weapon = m.weapons[2] end --next check if the hero is in range m.inrange = (math.abs(m.x - hero.x) <= m.weapon.reach and math.abs(m.y - hero.y) <= m.weapon.reach) --so far the states have been changed to flee or move. --update to attack if in range. if m.inrange and not (m.state == 'flee') then m.state = 'attack' end end end

updateState() goes through the state variables and updates them one by one. If they can’t see the hero, state is set to “move”. It then takes into account the monster’s tactical role and uses all the information to change to “move” or “flee”. At the end, it switches to “attack” if they’re in range of the hero and their state isn’t “flee”.

**How to do “What to do?”**

After updating the state of each monster, we then go to Game:MonsterAI(). MonsterAI() checks and responds to each state separately. First, when state == “move”.

if monster.state == "move" then --dx, dy are the direction coordinates where the monster will move local dx, dy = 0, 0 if monster.seesHero then --move towards the hero dx, dy = self.world:whichWay(monster, self.hero.x, self.hero.y) else --move randomly in one of four directions local directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}} local successful = false local tries = 0 while not successful do local d = directions[math.random(1, 4)] dx, dy = d[1], d[2] if not self.world:blocked(monster.x + dx, monster.y + dy) then successful = true end tries = tries + 1 if tries == 4 then successful = true end end end self.world:moveMonster(monster, dx, dy)

If the monsters sees the hero, then call WorldMap:whichWay() to figure out which direction to step. More on whichWay() shortly. Meanwhile, if they don’t see the hero, they move randomly. This is accomplished by picking one of the four directions and seeing if the tile is blocked. At the end, call WorldMap:moveMonster() to actually make the move.

function WorldMap:moveMonster(m, dx, dy) --get the array and entry local array = self.mapArrays[LAYER_MONSTERS] --erase the monster in the array and TileMap array[index(m.x, m.y)] = 0 self.mapLayers[LAYER_MONSTERS]:clearTile(m.x, m.y) --place the monster at the new position array[index(m.x + dx, m.y + dy)] = m.entry self.mapLayers[LAYER_MONSTERS]:setTile(m.x + dx, m.y + dy, m.entry, 1) --remove and move the HPbar array = self.mapArrays[LAYER_HP] array[index(m.x, m.y)] = 0 self.mapLayers[LAYER_HP]:clearTile(m.x, m.y) array[index(m.x + dx, m.y + dy)] = m.HPbar self.mapLayers[LAYER_HP]:setTile(m.x + dx, m.y + dy, m.HPbar, 1) --update x and y m.x = m.x + dx m.y = m.y + dy end

moveMonster() ensures that all the variables that need to be updated are updated. This includes monster.x and monster.y, but also the affected arrays in mapArrays and the TileMap layers in mapLayers.

If the hero is in sight, then WorldMap:whichWay() results in a step towards the hero. Let’s create that function next.

**Walking with a purpose**

whichWay() requires some thought because something could be in a way of the desired target. If the monster m wants to get to the target t in the example below, the direction (dx, dy) = (-1, 0) will get them there but only if the wall isn’t in the way.

. . . . . . t W m . . . . . .

Here, the monster should try (0,-1) or (0,1) if (-1,0) is blocked. If both of those directions are also blocked, stepping backward with (1,0) should also be considered. A list of possible moves for each direction is needed then.

function WorldMap:whichWay(monster, tX, tY) local x, y = monster.x, monster.y -- the direction variables to return and the default values 0, 0 local dx, dy = 0, 0 -- the table of directions to try local moves = nil --one of these directions is true if x > tX and math.abs(y - tY) <= math.abs(x - tX) then -- forward is west moves = {{-1, 0}, {0, 1}, {0, -1}, {1, 0}} end

Most of whichWay() tries to figure out, based on direction, the list of moves to test. Once it decides on the list to use, it checks to see if each is blocked. Note that staying still with (dx, dy) = (0, 0) is the default move if none of the others work.

for i, movein ipairs(moves) do dx, dy = move[1], move[2] if not self:blocked(x + dx, y + dy) then break end end return dx, dy

And that’s how monsters decide which way to move. The same logic can be used to make them flee. In fact, from MonsterAI(), the call is whichWay() is the same as if the monster.state is “flee”.

```
elseif monster.state == "flee" then
--move away from the hero
dx, dy = self.world:whichWay(monster, self.hero.x, self.hero.y)
self.world:moveMonster(monster, dx, dy)
```

Inside whichWay(), from the set of moves to choose from, the fourth move was also retreat. So let’s reverse the table moves and use the new reserveMoves table to “flee” instead of “move”.

if monster.state == "flee" then local reverseMoves = {} for i,v in ipairs(moves) do reverseMoves[v] = i end moves = reverseMoves end

Getting back to MonsterAI(), we’ve decided what to do for the “move”and “flee” states. What remains is the “attack” logic.

elseif monster.state == "attack" then --attack the hero self:basicAttack(monster, self.hero) --check to see if the hero died if self.hero.hp < 1 then self.msg:add("you died", MSG_DEATH) end end

What MonsterAI() does is simply call a basicAttack() function. We write that next.

**Attacking, the one thing we can agree on**

Actually, we already wrote it. basicAttack() is attackMonster() except now the tables are turned. If we copy/paste attackMonster() and write it a little more generically, we’ll have a function we can use for both attackMonster and attackHero. attackMonster() was called with coordinates and decided to use the hero’s weapon to attack.

function Game:attackMonster(x, y) local weapon = self.hero.weapon

This can be easily changed to a basicAttack() call with attacker and defender variables.

function Game:basicAttack(attacker, defender) local weapon = attacker.weapon

The rest of the function is just as easy to rewrite. Doing so results in a basicAttack() function that’s good to go for our monsters. For the hero, attackMonster() can also be quickly rewritten to take advantage of our basicAttack() function.

function Game:attackMonster(x, y) --find the monster being attacked and their index in monsters.list local monster = nil local id = 0 for i = 1, #self.monsters.list do monster = self.monsters.list[i] id = i if monster.x == x and monster.y == y then break end end --call the attack function self:basicAttack(self.hero, monster)

If we run the program now, the program will journey through the chain of functions we just wrote, from the initial turnOver() to the shared basicAttack().

**Summary**

For this step, we introduced State and a State machine. We then created a series of move functions so our monsters can move towards the hero before attacking, including whichWay(), our pathfinding algorithm that decided how the monsters move. Admittedly, whichWay() is basic pathfinding 101 at best. A monster in the following situation will never get to their target.

. . W W . . t W m . . . W W .

Remember this simplicity when you design bigger and cooler dungeons and you see monsters getting stuck in such situations. If that happens, you’ll have to read up on the cooler algorithms such as A* or Dijkstra and implement them inside whichWay() instead.

As is, whichWay() works to get the monsters into position to attack the hero. Our game is now a series of

- Hero attacks monster. Monster takes damage.
- Monsters attack hero. Hero takes damage.
- Monsters die and are removed.
- Hero dies and isn’t removed.

So while our monsters now have a fighting chance, we still haven’t dealt with what happens when they actually defeat the hero.