Dungeon 98
Verbs. Then, who knows?
I amused myself yesterday adding verb synonyms for “attacks” to the Princess. Today, I think I’ll start out by giving the various monsters verbs of their own, appropriate to their style and demeanor.
The monsters presently compute their attack word thusly:
function Monster:attackVerb()
return self.mtEntry.attackVerb or "whacks"
end
I gave the princess a random list of words. The code above suggests that there’s only one verb per monster type, and I think a random list will be better. I’ll “just” add the list as part of a monster’s monster table entry. A typical entry looks like this:
m = {name="Serpent", level=3, health={8,14}, speed={8,15}, strength={8,12},
dead=asset.snake_dead, hit=asset.snake_hit,
moving={asset.snake, asset.snake_walk}}
I’ll extend them like this:
m = {name="Serpent", level=3, health={8,14}, speed={8,15}, strength={8,12},
attackVerbs={"bites", "poisons", "strikes at"},
dead=asset.snake_dead, hit=asset.snake_hit,
moving={asset.snake, asset.snake_walk}}
Then in init:
self.attackVerbs = self.mtEntry.attackVerbs or {"strikes at"}
And then, we have this in Player:
function Player:attackVerb()
return self.attackVerbs[math.random(1,#self.attackVerbs)]
end
And we can promote this up to Entity, and remove the method in Monster.
As we see above, the serpent now has a number of different words for its attack. Let’s enhance the other monsters. I’ll spare you the complete list, but here are some battles1.
I noticed two things in making the video above. First, it’s too easy to evade attacks. Second, monsters often do not attack when given a chance. We’ll look at those after we commit: added attack verbs to all monsters.
For the evasion, we have this code:
function CombatRound:attemptHit()
local msg
if self.attacker:speedRoll() >= self.defender:speedRoll() then
msg = string.format("%s %s %s!", self.attacker:name(), self.attacker:attackVerb(), self.defender:name())
self:append(self:display(msg) )
self:rollDamage()
else
msg = string.format("%s evades attack!", self.defender:name())
self:append(self:display(msg))
end
end
Let’s define a new method, attackerSpeedAdvantage
:
function CombatRound:attemptHit()
local msg
if self.attacker:attackerSpeedAdvantage() + self.attacker:speedRoll() >= self.defender:speedRoll() then
msg = string.format("%s %s %s!", self.attacker:name(), self.attacker:attackVerb(), self.defender:name())
self:append(self:display(msg) )
self:rollDamage()
else
msg = string.format("%s evades attack!", self.defender:name())
self:append(self:display(msg))
end
end
I’ll set this to 2, up in Entity, for now:
function Entity:attackerSpeedAdvantage()
return 2
end
We’ll play with that for a bit and see how it works. In due time, I imagine we’ll give different entities different values, much as we just did with the verbs.
I forgot to add the method to FakeEntity but with that in place evasion seems less common. Good for now.
Commit: added speed advantage of 2 for attackers.
Now let’s see what’s up with monster attacks. I find this:
function Monster:chooseMove()
if not self.alive then return end
if self:distanceFromPlayer() <= 10 then
self:moveTowardAvatar()
else
self:makeRandomMove()
end
end
That’s concerning, but let’s see what follows. Ah:
function Monster:moveTowardAvatar()
local dxdy = self.runner:playerDirection(self.tile)
if math.random() < 0.5 then
self.tile = self.tile:legalNeighbor(self,vec2(0,dxdy.y))
else
self.tile = self.tile:legalNeighbor(self,vec2(dxdy.x,0))
end
end
You can probably see the issue but let’s review playerDirection
just to be sure what’s going on.
function GameRunner:playerDirection(aTile)
return aTile:directionTo(self.player:getTile())
end
function Tile:directionTo(aTile)
local t = aTile:pos()
return vec2(sign(t.x-self:pos().x), sign(t.y-self:pos().y))
end
function sign(x)
return (x < 0 and -1) or (x == 0 and 0) or (x > 0 and 1)
end
So direction returns a vector that is -1, 0, or 1 in the x and y direction. This means that x will be zero if we are already on the player’s x coordinate. Since we don’t check the coordinates for zero in moveTowardAvatar
, we have a 50-50 chance of moving toward the princess when we’re on the same x or y coordinate. When we’re right next to her, we have only a 50-50 chance of attacking.
Let’s adjust this. I think I’ll take the naive approach of having the move be unconditional for now, but we’ll probably want to make more or less aggressive monsters in due time. For now, if either the x is zero, we want the other. If they’re both non-zero, we pick one at random. (Monsters can’t move diagonally. Hmm. Maybe they should.)
function Monster:moveTowardAvatar()
local dxdy = self.runner:playerDirection(self.tile)
if dxdy.x == 0 or dxdy.y == 0 then
self.tile = self.tile:legalNeighbor(self, dxdy)
elseif math.random() < 0.5 then
self.tile = self.tile:legalNeighbor(self,vec2(0,dxdy.y))
else
self.tile = self.tile:legalNeighbor(self,vec2(dxdy.x,0))
end
end
This should accept any move that’s horizontal or vertical, and otherwise choose randomly h or v.
This works nicely. Since the frog has to turn square corners, the princess is able to get one ahead of it. But once she lets it get adjacent, it attacks on every turn thereafter.
Commit: monsters now move reliably toward player when within range.
I think that’ll do for this morning. A few fun things. Let’s sum up.
Summary
Today things went as we’d like them to, with the changes we needed going in rather simply. This is a sign that the code we’re working on is reasonably well designed and structured. It’s not proof: we might just be really clever or something. But it’s a sign.
You’ll have noticed that I didn’t test anything with TDD. Now, I think you should sin your own sins, so I’ll not be one to tell you when to test and when not. Today, I chose not to write tests, but as you saw, the code was quite simple and hard to get wrong, and if it was wrong, the game would quickly show it.
There is a possible future concern. It’s possible that someone would come along and change things in today’s code such that the game wouldn’t work, perhaps only in rare cases. We have no tests that help us ensure that that doesn’t happen.
I’d rather have those tests … but not enough to write them.
I think today’s changes improve the game. Combat is more interesting, due to the inclusion of interesting attack words, and to monsters being more aggressive and attacks more difficult to evade.
Thinking of the verbs also made me want to add some more interesting features. We have at least three monsters that are poisonous, the frog, the spider, and the hornet. I think it would be interesting if the poison was persistent, that is, the player would slowly lose health points until being healed somehow. That might be fun.
I think I’d like to add music, at least two tunes, a normal one and a danger one.
It’s past time to work on having more than one level, with monsters getting tougher as one goes deeper into the dungeon.
I’d like to add some traps and puzzles. And doors.
Doors will be interesting, since the game doesn’t really even know where the hallways are, so it will be fun finding places to put them.
Maybe we need a boss room.
And more treasures. And inventory …
There’s lots to do. Imagine how much there’d be if this were a real game. There’s so much detail that goes into making a game interesting, playable, fun. The code is only a fraction of the real work.
Fortunately for me, I get to focus on the parts I find interesting.
See you next time!
-
Get your mind out of the gutter. “Lick” is a Pokemon ghost attack. ↩