Dungeon 50
I think we need to work on treasures today.
Before we do, however, I want to note an idea from Bruce Onder:
@RonJeffries I think it would be great to flesh out combat and its support systems.
Damage system Healing potions Shield (absorbs 1 point of damage per attack) Sword (adds to P’s attack power) Experience Leveling Up
Great series so far!
He went on to suggest some more detailed notions, which are worth considering. But certainly we need to have valuable things for our player princess to find. I had in mind some +n weapons, probably swords because of our limited graphics, and some kind of shielding thing. Amulets might be good.
Anyway, we need to do something about keys being needed for chests, and about getting the contents.
The TileArbiter
manages things like the player trying to step onto a tile containing a chest. The relevant control lines now are these:
t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
t[Key][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithKey}
function Player:startActionWithKey(aKey)
self.keys = self.keys + 1
sound(asset.downloaded.A_Hero_s_Quest.Defensive_Cast_1)
aKey:take()
end
function Player:startActionWithChest(aChest)
aChest:open()
end
function Chest:open()
self.pic = self.openPic
end
I guess that a chest needs to contain a treasure, and that the treasure should display when the chest is open. If we add the treasure to the tile, it will display on top of the chest. If it’s an active object, as is the chest, it can be taken, as are keys.
In fact, we could create the treasure randomly when we open the chest, no need to actually lug it around. Or we could just have a simple member variable in the chest that tells it what to create.
Let’s try adding a Health treasure to the cell when a chest is opened. We’ll ignore the key issue for now, though it should be trivial.
I’m inclined to make the treasures each their own class. They’ll be small, and there will be duplication, but otherwise there’d likely be lots of conditionals if we create a single treasure class.
Health = class()
function Health:init(aTile,runner)
self.tile = aTile
self.runner = runner
end
function Health:draw()
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y, 50,50)
popStyle()
end
Now on Chest:open
:
function Chest:open()
self.pic = self.openPic
self.tile:addContents(Health(self.tile, self.runner))
end
That looks nearly good. We should boost it upward a few pixels, perhaps. Ten up:
function Health:draw()
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 50,50)
popStyle()
end
The original heart size is 101x171. To keep it proportional, a better size would be 35 by 60.
function Health:draw()
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
popStyle()
end
That looks good:
Now let’s give it to the player when they next step on the tile, and clear the tile while we’re at it.
t[Heart] = {}
t[Heart][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
And …
function Player:startActionWithHealth(aHealth)
self.healthPoints = self.healthPoints + aHealth:points()
end
And …
function Health:init(aTile,runner, points)
self.tile = aTile
self.runner = runner
self.points = points or 1
end
function Health:points()
return self.points
end
This is all pretty hard wired but it should do the job. Of course, we have no display of player health as yet. Let’s do a quick and very dirty:
function Player:draw(tiny)
local dx = -2
local dy = -3
pushMatrix()
pushStyle()
spriteMode(CORNER)
local center = self:graphicCorner()
if not self.alive then tint(0) end
if tiny then
tint(255,0,0)
sx,sy = 180,272
else
sx,sy = 80,136
end
sprite(asset.builtin.Planet_Cute.Character_Princess_Girl,center.x+dx,center.y+dy, sx,sy)
text(self.healthPoints, center.x+dx, center.y+dy+ 100)
popStyle()
popMatrix()
end
This displays a small number near the princess’s head. Let’s make it larger:
fontSize(fontSize()*2)
text(self.healthPoints, center.x+dx, center.y+dy+ 100)
OK, yes, well, I meant to say Health
, not Heart
:
t[Health] = {}
t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
And I need this:
function Health:healthPoints()
return self.points
end
I’m rushing and it’s causing me to make silly mistakes. I could do some tests here but it seems unnecessary.
I noticed that the player can move onto the chest tile. I thought that wasn’t supposed to happen. I don’t see why it might be allowed, maybe it was because of the crash.
I’ve gotta make a movie of this. In the movie below, every time the number goes up, I tried to move the player into the chest:
So that’s interesting. I suspect that two things are happening. One of them is that even though the chest is already open, trying to open it again will cause it to add another health to the contents:
function Chest:open()
self.pic = self.openPic
self.tile:addContents(Health(self.tile, self.runner))
end
This should be conditioned:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
self.tile:addContents(Health(self.tile, self.runner))
end
But the other issue is that the tile is in the process of looping over its contents, and adding an item to it will cause that item to immediately be discovered and we’ll interact with it. We need to buffer the addContents
somehow, I think.
OK, that was fiddly. I’ve changed this to remove the health item after use:
function Health:giveHealthPoints()
self.tile:removeContents(self)
return self.points
end
And modified addContents
and draw
in Tile
:
function Tile:addContents(anEntity)
self.newlyAdded[anEntity] = anEntity
end
function Tile:draw(tiny)
pushMatrix()
pushStyle()
spriteMode(CORNER)
tint(self:getTint(tiny))
local sprites = self:getSprites(self:pos())
local center = self:graphicCorner()
for i,sp in ipairs(sprites) do
sprite(sp,center.x,center.y,self.runner.tileSize)
end
for k,c in pairs(self.newlyAdded) do
self.contents[k] = c
end
self.newlyAdded = {}
for k,c in pairs(self.contents) do
c:draw()
end
popStyle()
popMatrix()
end
And now I expect it to work:
That looks right. Commit: chests contain Health+1. Temporary health display over player.
Let’s Reflect
That was a bit fast and loose. I didn’t make any “serious” mistakes, but I made a lot of silly careless kinds of mistakes. I was sort of in a mode of just banging in what was needed, and I even tweaked a couple of things that really didn’t need it.
TDDing the feature would have slowed me down, and that would have been good in this case, except that this feature really doesn’t lend itself very well at all to TDD, as it has no real logic to speak of.
The problem with adding contents on the fly has bitten me in the past. Formally the rule is that you can assign a new value to an existing key, or remove it, but addition of keys is not OK. It seems that the new key may or may not appear during the current loop. So, we buffer additions. That seems to work. However this is now messy again:
function Tile:draw(tiny)
pushMatrix()
pushStyle()
spriteMode(CORNER)
tint(self:getTint(tiny))
local sprites = self:getSprites(self:pos())
local center = self:graphicCorner()
for i,sp in ipairs(sprites) do
sprite(sp,center.x,center.y,self.runner.tileSize)
end
for k,c in pairs(self.newlyAdded) do
self.contents[k] = c
end
self.newlyAdded = {}
for k,c in pairs(self.contents) do
c:draw()
end
popStyle()
popMatrix()
end
Let’s refactor a bit:
function Tile:draw(tiny)
pushMatrix()
pushStyle()
spriteMode(CORNER)
self:updateContents()
tint(self:getTint(tiny))
self:drawSprites()
self:drawContents()
popStyle()
popMatrix()
end
function Tile:drawContents()
for k,c in pairs(self.contents) do
c:draw()
end
end
function Tile:drawSprites()
local sprites = self:getSprites(self:pos())
local center = self:graphicCorner()
for i,sp in ipairs(sprites) do
sprite(sp,center.x,center.y,self.runner.tileSize)
end
end
function Tile:updateContents()
for k,c in pairs(self.newlyAdded) do
self.contents[k] = c
end
self.newlyAdded = {}
end
Much nicer, by my standards.
In other news, I have failing tests. Since they don’t display in those big letters on the screen now, I tend to miss that they didn’t run. It may be necessary to improve the tool. Anyway let’s see what’s borked.
19: TileArbiter: player can step on key and receives it -- Actual: table: 0x2940d4b40, Expected: table: 0x2940d4bc0
I rather fear that some of these may have been failing for a while. If so, an argument could be made that I’m a bad person.
Anyway this test is:
_:test("TileArbiter: player can step on key and receives it", function()
local runner = GameRunner()
local room = Room(1,1,20,20, runner)
local pt = Tile:room(11,10,runner)
local player = Player(pt,runner)
runner.player = player
local kt = Tile:room(10,10, runner)
local key = Key(kt,runner)
_:expect(player.keys).is(0)
_:expect(kt.contents).has(key)
local arb = TileArbiter(key,player)
local moveTo = arb:moveTo()
_:expect(moveTo).is(kt)
_:expect(player.keys).is(1)
_:expect(kt.contents).hasnt(key)
end)
There are three OKs after the fail, and one before, so it appears that the tile contents does not include the key. I’d better put some commentary in those expectations, to be sure. Oh. I know what it is, it’s the new add contents. Key does this:
function Key:init(tile, runner)
self.tile = tile
self.tile:addContents(self)
self.runner = runner
end
And we don’t update contents until inside draw. So our tests aren’t seeing updates to contents.
One “easy” fix will be to change all the related tests to update. We just added a function to do it. I think a better way will be to set the original add contents back the way it was, and add a new function, addDeferredContents
for the special case of a chest update.
function Tile:addContents(anEntity)
self.contents[anEntity] = anEntity
end
function Tile:addDeferredContents(anEntity)
self.newlyAdded[anEntity] = anEntity
end
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
This seems to work just fine. Commit: added addDeferredContents
for live additions to tiles.
In addition, all the tests are now running save only one:
13: monster can enter player tile -- Actual: Tile[10][10]: room, Expected: Tile[11][10]: room
I think I changed that ages ago and never changed the test. Here is the TA entry:
t[Player][Monster] = {moveTo=TileArbiter.refuseMoveIfResidentAlive, action=Monster.startActionWithPlayer}
So that test needs improvement:
_:test("monster can enter player tile", function()
local runner = GameRunner()
local monsterTile = Tile:room(10,10,runner)
local monster = Monster(monsterTile, runner)
local playerTile = Tile:room(11,10,runner)
local player = Player(playerTile,runner)
runner.player = player -- needed because of monster decisions
local chosenTile = monsterTile:validateMoveTo(monster,playerTile)
_:expect(chosenTile).is(playerTile)
end)
Let’s test both cases:
_:test("monster can enter player tile only if player is dead", function()
local chosenTile
local runner = GameRunner()
local monsterTile = Tile:room(10,10,runner)
local monster = Monster(monsterTile, runner)
local playerTile = Tile:room(11,10,runner)
local player = Player(playerTile,runner)
runner.player = player -- needed because of monster decisions
chosenTile = monsterTile:validateMoveTo(monster,playerTile)
_:expect(chosenTile).is(monsterTile)
player:die()
chosenTile = monsterTile:validateMoveTo(monster,playerTile)
_:expect(chosenTile).is(playerTile)
end)
All tests run. Commit: modified test for current monster-player interaction.
I’m hungry. Let’s sum up and grab something to eat.
Summary
I was trying to go too fast, for no particular reason. I had done a few other things this morning, so maybe I wasn’t in as contemplative a mood as when I start first thing. It would have been wise to have noticed sooner and perhaps taken a break, or written a bit about the feeling. I think that working too fast is a bit like running downhill. You can get going too fast for your feet and start tumbling. The good news is that you rarely break a bone when coding too fast. The bad news is that you quite frequently break code.
Well, I do. I don’t know that you do, but I know how I’d bet.
Nonetheless, we have a decent but rudimentary implementation of chest contents. We’ll probably want to have GameRunner
make the determination of what kinds of treasures should be provided, so that we can adjust the contents of levels according to some notion of difficulty. More healing hearts if there are lots of powerful monsters, and so on.
But that should be straightforward to do in the next phase.
Does it seem odd to you to do such a thin version of the treasures contained in chests? There’s no control over how many points the Health
is worth, nor provision for anything other than Health
. There’s hardly anything there.
To me, that’s the point. We have a big story in mind:
Chests can contain all kinds of treasures. Most of them will be good. Some may be bad. Treasures might include gold, health potions, gems of power, weapons, charming evening outfits, or pets. Provision should be made for artificial intelligence and teleportation.
To me, one of the most important tools in software development is to slice a big story down to a small essential first story that actually delivers visible progress toward the big story. Then we keep improving that capability as our product owners, managers, or whims may direct.
Thin is good. This is the way.
See you next time!