Dungeon 76
Let’s use the new Loot object for Health and Keys. That should test the concept and drive out some nice clean generality. (N.B. Plan changes one page in.) Worse yet: hours of confusion!
My guess is that doing both Health and Keys will drive out some useful behavior and changes to Loot. We’ll have to provide different graphics for each item, and the Keys probably suggest some kind of simple inventory of items whose number is important, such as keys and coins.
Narrator (v.o.): This display of even fake hubris could not go unpunished.
With luck, this will go fine. Well, and of course with the great yet unassuming skill that I bring to this work. Well, and luck.
Let’s review how Loot works. That was yesterday, and I don’t remember.
function Loot:init(tile, kind, min, max)
self.tile = tile
self.kind = kind
self.min = min
self.max = max
if tile then tile:addContents(self) end
end
function Loot:getTile()
return self.tile
end
function Loot:actionWith(aPlayer)
aPlayer:addPoints(self.kind, math.random(self.min, self.max))
self.tile:removeContents(self)
end
function Loot:draw()
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Gem_Blue,g.x,g.y+10,35,60)
popStyle()
end
Loot is called like this:
function GameRunner:createLoots(n)
local loots = {}
for i = 1, n or 1 do
local tile = self:randomRoomTile()
table.insert(loots, Loot(tile, "Strength", 4, 9))
end
return loots
end
The present design is that a Loot can only add points to some points dimension. One possibility is that it should stay that way. Maybe I’m wrong to want to jam Key into this structure. We could wait until there’s another inventory type object, generalize that concept, and then see whether Loot deals with it.
See how quickly plans change? We haven’t written a line of code and the plan has changed already. Reality impacts plans: that’s the way of it.
Anyway, let’s fold in Health, which looks like this:
function Health:init(aTile,runner)
self.tile = aTile
self.runner = runner
end
function Health:draw(tiny)
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
popStyle()
end
function Health:giveHealthPoints()
self.tile:removeContents(self)
return self:randomPoints()
end
function Health:randomPoints()
return math.random(4,10)
end
So that’s much the same, isn’t it? Let’s just start doing Health with Loot, and scavenge stuff from Health class, with the intention of getting rid of it.
We presently create Healths here:
self:createThings(Chest,5)
Because all the Healths are inside the chest. Just for fun, let’s change what the Chest contains. When a Chest opens:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
Hm. Let’s just create a Loot, instead:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
self.tile:addDeferredContents(Loot(self.tile, "Health", 4,10))
end
This should work, I think, except that the Health will have a big gem as its artwork. Well, the Gem shows up OK, although I’m allowed to move onto the square, unlike the usual effect of opening the chest but not jumping onto it. And the Gem disappears, but I don’t get a message. Why not? Let’s look at what goes on after Loot runs:
function Loot:actionWith(aPlayer)
aPlayer:addPoints(self.kind, math.random(self.min, self.max))
self.tile:removeContents(self)
end
function Player:addPoints(kind, amount)
local attr = self:pointsTable(kind)
if attr then
local current = self[attr]
self[attr] = math.min(20,current + amount)
self:doCrawl(kind, amount)
end
end
function Player:pointsTable(kind)
local t = {Strength="strengthPoints"}
return t[kind]
end
Yes, well, if the table has no expectations about Health, nothing’s gonna happen.
function Player:pointsTable(kind)
local t = {Strength="strengthPoints", Health="healthPoints", Speed="speedPoints"}
return t[kind]
end
A bit of speculation there, that there will be a way of adding to Speed. Seems a good bet, because my product owner (me) just decided we’ll do it.
Now I expect a health benefit. Still need to see why we can move into the tile when we couldn’t before.
Did you see what happened there? I got +6 Health directly upon moving in, and then again when I stepped out and back in. That’s different from the old behavior, I’m sure. I’m going to put Chests back the way they were and test again to be sure.
It’s definitely different. And I know what it is. The Loot lets you step into the square and gives you the stuff. The Health does not. Let’s check the tables:
t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
t[Loot][Player] = {moveTo=TileArbiter.acceptMove, action=Player.startActionWithLoot}
Loots let you move onto them. And the current rule is that if any object in the tile allows the move, you can move. So even though the Chest refuses the move, the Loot allows it. Let’s just change the Loot to refuse also. That’ll be OK. When you interact with a thing you try to step on it. It would make sense that you don’t move, you just interact. We should think about Keys in this light as well.
But for now, we’re working on Loot.
Making this change may break a Loot test, depending on whether we checked the tile. If so, we’re changing behavior so a behavior-checking test needs to change.
The change to refuse move does prevent me from moving into the tile, but somehow the Health gem triggers twice. The Health heart does not. Curious.
Narrator: A long period of confusion starts about here. Be prepared to skip ahead.
I’m not sure what’s happening. The Chest uses addDeferredContents
which is intended to make certain that the loops over contents don’t see the new things added during this attempted entry. But the deferred contents are added into the real contents by draw
. Could that be too soon, before the current interaction completes? I don’t see how: I’m sure Codea doesn’t interrupt the running program, I’m sure it waits until it returns from whatever event started our code.
And why didn’t the Gem remove itself, even if it was going to give us the Health too soon?
Here’s an interesting difference:
function Health:giveHealthPoints()
self.tile:removeContents(self)
return self:randomPoints()
end
function Loot:actionWith(aPlayer)
aPlayer:addPoints(self.kind, math.random(self.min, self.max))
self.tile:removeContents(self)
end
Health removes itself, then gives. Loot gives, then removes itself. Well, let’s reverse Loot’s behavior to match Health.
That makes no difference at all.
I am definitely confused, and don’t mind admitting it. It’s tempting to revert, but I’m sure that these tiny changes aren’t wrong. There’s something different in the flow between Loot and Health and we need to understand what it is.
Narrator: He isn’t lying, he was definitely confused.
Let’s dig deeper. My first question is why does the Loot get called on the first attempted entry, and the Health not?
Can I get this thing under TDD? That would be the thing to try, wouldn’t it?
Let’s see those tests. First I have to make the test run again:
1: Create Health Loot -- TileArbiter:33: attempt to call a nil value (method 'getTile')
We’re refusing the move now, so we are calling getTile on our FakePlayer. If she will return something, we can check for it.
function FakePlayer:getTile()
return "tile"
end
_:test("Create Health Loot", function()
local tile = FakeTile()
loot = Loot(tile, "Health", 5,5)
local player = FakePlayer()
local arb = TileArbiter(loot, player)
local m = arb:moveTo()
_:expect(m).is("tile")
_:expect(removedEntity).is(loot)
end)
So that works. But I think we’re deep enough in that we should revert this code or commit it. Let’s change Chest back to create Healths, and then commit. We have some work in progress but it is harmless. Commit: WIP for moving Health to Loot.d
Narrator: Committing here was probably wise. Confusion only grows.
Now let’s read some code, to see what we need to find out.
First, let’s see what the Chest-Health flow is like.
TileArbiter will ask whether the player can move onto the chest. The Chest table entry is:
t[Chest][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithChest}
So Player will do:
function Player:startActionWithChest(aChest)
aChest:open()
end
And Chest:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
--self.tile:addDeferredContents(Loot(self.tile, "Health", 4,10))
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
Commented out there is the line we’ve been using for Loot. No change so far. Now I feel quite sure that the Health will not interact with us until the next time we try to enter, because it won’t get drawn until after we’re done with this attempt, and even if it did, the same is true for Loot. So what happens when we try to step on a Health?
t[Health][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithHealth}
Now, I think we know that the Loot does behave properly when stepped on the second time. The question is why does it trigger the first time? I don’t see where it even gets a message to run.
The table entry for Loot is:
t[Loot][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithLoot}
Narrator: Had he thought “was it already added somehow”, Ron might have found the problem sooner. He didn’t think of that.
But why did it even get called already? Something funky is going on here. I’m going to put an assert into that method and see what I can find. I must be getting close … mustn’t I?
function Player:startActionWithLoot(aLoot)
assert(false, "startActionWithLoot")
aLoot:actionWith(self)
end
Player:240: startActionWithLoot
stack traceback:
[C]: in function 'assert'
Player:240: in local 'action'
TileArbiter:27: in method 'moveTo'
Tile:88: in method 'attemptedEntranceBy'
Tile:348: in function <Tile:346>
(...tail calls...)
Player:150: in method 'moveBy'
Player:143: in method 'keyPress'
GameRunner:213: in method 'keyPress'
Main:34: in function 'keyboard'
I do get entry on the first try moving in. Let’s put a corresponding assert in for Health and see if it happens there.
function Player:startActionWithHealth(aHealth)
assert(false,"startActionWithHealth")
self:addHealthPoints(aHealth:giveHealthPoints())
end
Narrator: When Ron says “bizarre”, you can be pretty sure he’s in the weeds. Consider speed-reading.
Bizarre. I do not get that assert until I try to move in a second time. It is as if the Loot is there, hiding, when I first move in. But we see that it’s created only on Chest:open():
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
--self.tile:addDeferredContents(Loot(self.tile, "Health", 4,10))
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
Whichever way we do it, it’s deferred. Yet we clearly see that Loot got arbitrated on our first attempted entry. How can that be?
I’m concerned about where the deferred objects are applied. Possibly that should be moved to another place, but why would it always happen one way with Health and the other with Loot.
This is one of those days when I wish I hadn’t started an article. I could just wallow through this and then tomorrow look really smart. But my point is to show that real programmers–if I am one–do get confused, even in the best code they know how to write, kept as clean as they reasonably can.
Narrator: You have to give him credit for not deleting all this confusion. He wants you to know it’s OK to be confused sometimes.
The only theory I have at present is that the deferred objects are somehow being applied too soon. I guess I should eliminate that concern, by changing where they are applied.
The deferred addition (and I think the thing in the Chest is the only one) is triggered by a TileArbiter. So in our code that creates and uses TAs, we can put the unwind at the end of that loop.
function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
local ta
local acceptedTile
local accepted = false
local residents = 0
for k,residentEntity in pairs(self.contents) do
residents = residents + 1
ta = TileArbiter(residentEntity,enteringEntity)
acceptedTile = ta:moveTo()
accepted = accepted or acceptedTile==self
end
if residents == 0 or accepted then
return self
else
return oldRoom
end
end
I think we can be certain that only the current tile gets the newContents. I was a bit worried about whether we’re on the right tile. Anyway, let’s try this:
function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
local ta
local acceptedTile
local accepted = false
local residents = 0
for k,residentEntity in pairs(self.contents) do
residents = residents + 1
ta = TileArbiter(residentEntity,enteringEntity)
acceptedTile = ta:moveTo()
accepted = accepted or acceptedTile==self
end
self:updateContents()
if residents == 0 or accepted then
return self
else
return oldRoom
end
end
Now first to test whether Health still works. It does, the assert only fires upon second entry.
Now Loot. It still happens!! It triggers on first entry.
Narrator: When you have eliminated your only idea, it’s time for new ideas.
Well, the good news is that that eliminates my only idea, the possibility that deferred update is implicated. The bad news is, I’m running short of ideas.
I think I’ll put a trap in that attemptedEntrance loop, to see whether we hit a Chest and a Health or Loot in the same iteration.
function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
local ta
local sawChest = false
local sawHealth = false
local sawLoot = false
local acceptedTile
local accepted = false
local residents = 0
for k,residentEntity in pairs(self.contents) do
residents = residents + 1
if getmetatable(residentEntry) == Chest then sawChest = true end
if getmetatable(residentEntry) == Health then sawHealth = true end
if getmetatable(residentEntry) == Loot then sawLoot = true end
ta = TileArbiter(residentEntity,enteringEntity)
acceptedTile = ta:moveTo()
accepted = accepted or acceptedTile==self
end
if residents > 0 then
if sawChest and sawLoot then
assert(false, "chest and loot together")
elseif sawChest and sawHealth then
assert(false,"chest and health together")
end
end
self:updateContents()
if residents == 0 or accepted then
return self
else
return oldRoom
end
end
That’s nasty but I think it’ll do the job.
With that in place, the startActionWithLoot
assert triggers, and the assert for sawChest and sawLoot does not trigger. I have doubly convinced myself that it’s not that they’re being checked on the same cycle. But then how is it happening at all?
Narrator: Here begins a long debugging cycle, putting in various prints and trying to see what the internal flow is. Probably necessary. Could have been done more effectively, in retrospect.
I’m now questioning whether the checks on the metatable work. I’m starting to question whether arithmetic works, or my brain works.
I’ll remove the asserts in startAction for now.
No, I put a print in the loop and none of the flags ever goes true. But then why does this work:
function TileArbiter:tableEntry(resident,mover)
local rclass = getmetatable(resident)
local mclass = getmetatable(mover)
local innerTable = TA_table[rclass] or {}
local entry = innerTable[mclass] or self:defaultEntry()
return entry
end
The metatable is used to look up in the table with entries like this:
t[Loot][Player] = {moveTo=TileArbiter.refuseMove, action=Player.startActionWithLoot}
t[Loot][FakePlayer] = {moveTo=TileArbiter.refuseMove, action=FakePlayer.startActionWithLoot}
OK back to basics. I’ll start testing addition in a minute.
OK, this prints true
:
local meta = getmetatable(Runner)
print("class?", meta==GameRunner)
So then why does this never print anything but false false false?
function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
local ta
local sawChest = false
local sawHealth = false
local sawLoot = false
local acceptedTile
local accepted = false
local residents = 0
for k,residentEntity in pairs(self.contents) do
residents = residents + 1
if getmetatable(residentEntry) == Chest then sawChest = true end
if getmetatable(residentEntry) == Health then sawHealth = true end
if getmetatable(residentEntry) == Loot then sawLoot = true end
ta = TileArbiter(residentEntity,enteringEntity)
acceptedTile = ta:moveTo()
accepted = accepted or acceptedTile==self
end
if residents > 0 then
print(sawChest, sawHealth, sawLoot)
end
self:updateContents()
if residents == 0 or accepted then
return self
else
return oldRoom
end
end
Narrator: Even confused, he’s doing the right thing: collect more information, and use it to think of new information to collect.
Bizarre.
Oh. I said “Entry” not “Entity”. Brilliant. What a simple mistake to burn at least an hour, probably more on.
Back to the instruments.
When I step into the tile with a chest containing a loot, I get true false true from the print: it saw both. On the first entry that should not be the case.
But monsters are moving and confusing the issue. Comment them out. Yes, still true, false, true, it sees the Loot.
Does it see a Health? It prints true, false, false. It is as if the Loot is present. Does order matter? How could it? I’m going to put in more tracing.
function Tile:attemptedEntranceBy(enteringEntity, oldRoom)
local ta
local arb = false
local sawChest = false
local sawHealth = false
local sawLoot = false
local acceptedTile
local accepted = false
local residents = 0
for k,residentEntity in pairs(self.contents) do
residents = residents + 1
if not arb then
print("beginning arbitration")
arb = true
end
--print("resident", residentEntry)
if getmetatable(residentEntity) == Chest then print("Chest"); sawChest = true end
if getmetatable(residentEntity) == Health then print("Health"); sawHealth = true end
if getmetatable(residentEntity) == Loot then print("Loot"); sawLoot = true end
ta = TileArbiter(residentEntity,enteringEntity)
acceptedTile = ta:moveTo()
accepted = accepted or acceptedTile==self
end
if residents > 0 then
print("ending arbitration", sawChest, sawHealth, sawLoot)
end
self:updateContents()
if residents == 0 or accepted then
return self
else
return oldRoom
end
end
When using Loot, I get this sequence:
beginning arbitration
Chest
chest adding deferred contents
Loot
ending arbitration true false true
The loot is clearly being found in the same loop. When I run it with Health, it doesn't get found in the loop, the loop ends, and it's there next time I try to enter.
Something wrong with addDeferredContents???
~~~lua
function Tile:addDeferredContents(anEntity)
self.newlyAdded[anEntity] = anEntity
end
Here’s the update:
function Tile:updateContents()
for k,c in pairs(self.newlyAdded) do
self.contents[k] = c
end
self.newlyAdded = {}
end
And the only call to updateContents is the one in the attempted entry code above. What if I don’t do the update at all?
Narrator: Here the dawn breaks! The effect shows up even though the object isn’t shown. Somehow this puts Ron on the right track.
Fascinating! The Gem never appears, but I get the message anyway.
Oh. At last. I’ll bet that the Loot adds itself to the tile right away, and the health does not.
function Health:init(aTile,runner)
self.tile = aTile
self.runner = runner
end
function Loot:init(tile, kind, min, max)
self.tile = tile
self.kind = kind
self.min = min
self.max = max
if tile then tile:addContents(self) end
end
Back On Track
Narrator: You can stop skipping now, he’s back on track.
There you go. It’s @#$^^!! 1143, and I’ve been working on this since about 0900. Fascinating.
Now, If I had tried leaving Health hearts lying around outside Chests, it would not have worked because they didn’t add themselves at all.
We need to normalize this behavior. What should the rule be? How about this: When an entity is created, it is passed a tile, and (if it gets a tile) it should add itself deferred. We’ll put the updateContents
back into draw
so the object will show up next time the draw cycles, and it will not magically appear in the same arbiter loop if it happens to be created by entry. But it will always add itself deferred, so that the Loot need not do that addition, just rez the guy. Make sense? I hope so.
Let’s revert, see where we are, and go from there.
Again, for the very ~last~ next time.
Narrator: From here on, things go as you’d hope, ticking through the necessary changes. You have to admire Ron’s stubbornness if not his wisdom.
updateContents
is back in Tile:draw
. We want to find everyone who does addContents and see about deferring them. This should just be a matter of simple search and destroy. Inch by inch, step by step, slowly we turn this thing into working code.
function Monster:init(tile, runner, mtEntry)
if not MT then self:initMonsterTable() end
self.alive = true
self.tile = tile
self.tile:addDeferredContents(self)
self.runner = runner
function Loot:init(tile, kind, min, max)
self.tile = tile
self.kind = kind
self.min = min
self.max = max
if tile then tile:addDeferredContents(self) end
end
function Player:init(tile, runner)
self.alive = true
self.tile = tile
self.tile:illuminate()
self.tile:addDeferredContents(self)
self.runner = runner
function Key:init(tile, runner)
self.tile = tile
self.tile:addDeferredContents(self)
self.runner = runner
end
function Chest:init(tile, runner)
self.tile = tile
self.tile:addDeferredContents(self)
self.runner = runner
function FakeTile:addDeferredContents(ignored)
end
What about this reference?
function Tile:moveEntrant(anEntity, newTile)
self:removeContents(anEntity)
newTile:addContents(anEntity)
end
I think we’d better do it here too.
function Tile:moveEntrant(anEntity, newTile)
self:removeContents(anEntity)
newTile:addDeferredContents(anEntity)
end
And finally:
function Tile:updateContents()
for k,c in pairs(self.newlyAdded) do
self.contents[k] = c
end
self.newlyAdded = {}
end
This guy doesn’t even use addContents
. Nice. Let’s remove the method, then no one will be tempted to use it.
Also removed from the FakeTile.
Now what about the open logic in Chest?
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
--self.tile:addDeferredContents(Loot(self.tile, "Health", 4,10))
self.tile:addDeferredContents(Health(self.tile, self.runner))
end
This no longer needs to add to tile. It can just create the object and move on. This should work as before Loot:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
--Loot(self.tile, "Health", 4,10)
Health(self.tile, self.runner)
end
Of course the whole system may explode. This tiny change is a big one, deferring all those updates.
What happens is that the Health heart does not appear. Oh. It doesn’t add itself deferred. That’s what we originally discovered.
function Health:init(aTile,runner)
self.tile = aTile
self.runner = runner
if self.tile then self.tile:addDeferredContents(self) end
end
Try again. Works as intended. Now convert to Loot:
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
Loot(self.tile, "Health", 4,10)
--Health(self.tile, self.runner)
end
Works. I see a blue Gem in the chest, and when I try to enter, I get some health.
Narrator: A while. It took a while. Yes, Ron, it took a while. So did the creation of the Alps.
Wow that took a while. I thought we’d just plug that in and then deal with the image.
I don’t feel whipped yet, so let’s press on.
Giving Loot an Image
Narrator: Perhaps interestingly, this doesn’t work first time either, but upon discovering it, Ron manages to recover quickly. I guess he’s not all bad.
Now we need to put images into our various Loots. This may take more than one pass, as we first figure out how to do it at all, and then figure out how to do it in our creation loop. Loots are created and drawn as follows:
function Loot:init(tile, kind, min, max)
self.tile = tile
self.kind = kind
self.min = min
self.max = max
if tile then tile:addDeferredContents(self) end
end
function Loot:draw()
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Gem_Blue, g.x,g.y+10,35,60)
popStyle()
end
It seems we need to provide a sprite. The Health object, soon to be defunct, does this:
function Health:draw(tiny)
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(asset.builtin.Planet_Cute.Heart,g.x,g.y+10, 35,60)
popStyle()
end
We could generalize this code to deal with different scaled sprites, and maybe use and even enhance AdjustedSprite, but at this point there is no call for that. There is only the need to provide the sprite.
Loots are created directly as well as in chests:
function GameRunner:createLoots(n)
local loots = {}
for i = 1, n or 1 do
local tile = self:randomRoomTile()
table.insert(loots, Loot(tile, "Strength", 4, 9))
end
return loots
end
I think we need to allow this bit to be the main source of Loots, and to deal with Chests separately. So I’d like this loop to create n random loots, either Health, or Strength, or Speed.
Let’s build a simple table:
local StrengthIcon = asset.builtin.Planet_Cute.Gem_Blue
local HealthIcon = asset.builtin.Planet_Cute.Heart
local SpeedIcon = asset.builtin.Planet_Cute.Star
local RandomLootInfo = {
{"Strength", StrengthIcon, 4,9},
{"Health", HealthIcon, 4,10},
{"Speed", SpeedIcon, 2,5 }
}
And use it:
function GameRunner:createLoots(n)
local loots = {}
for i = 1, n or 1 do
local tile = self:randomRoomTile()
local tab = RandomLootTable[math.random(1,#RandomLootTable)]
table.insert(loots, Loot(tile, tab[1], tab[2], tab[3], tab[4]))
end
return loots
end
And rig Loot to expect the new parm:
function Loot:init(tile, kind, icon, min, max)
self.tile = tile
self.kind = kind
self.icon = icon
self.min = min
self.max = max
if tile then tile:addDeferredContents(self) end
end
function Loot:draw()
if tiny then return end
pushStyle()
spriteMode(CENTER)
local g = self.tile:graphicCenter()
sprite(self.icon,g.x,g.y+10,35,60)
popStyle()
end
And bring Chest up to date:
Ah …
function Chest:open()
if self:isOpen() then return end
self.pic = self.openPic
Loot(self.tile, "Health", 4,10)
--Health(self.tile, self.runner)
end
How can it know the icon? It can’t. Therefore Loot has to know the icons. Back to the drawing board, change the table and its use:
local RandomLootInfo = {
{"Strength", 4,9},
{"Health", 4,10},
{"Speed", 2,5 }
}
function GameRunner:createLoots(n)
local loots = {}
for i = 1, n or 1 do
local tile = self:randomRoomTile()
local tab = RandomLootTable[math.random(1,#RandomLootTable)]
table.insert(loots, Loot(tile, tab[1], tab[2], tab[3]))
end
return loots
end
And teach Loot to know the icons:
local LootIcons = {Strength=asset.builtin.Planet_Cute.Gem_Blue, Health=asset.builtin.Planet_Cute.Heart, Speed=asset.builtin.Planet_Cute.Star}
function Loot:getIcon(kind)
return LootIcons[kind]
end
Nearly worked. Yes, well, name things the same in the use as the declaration:
function GameRunner:createLoots(n)
local loots = {}
for i = 1, n or 1 do
local tile = self:randomRoomTile()
local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
table.insert(loots, Loot(tile, tab[1], tab[2], tab[3]))
end
return loots
end
Everything looks good, I found Chests of health, and separate health, strength and speed Loots. We can commit: all three Loots now working.
OK, that last bit seemed like a scramble and we did update 9 files, but we changed a couple of major protocols. Nonetheless it’s clear that all of Loot, Monster, Player, and Chest and Health have some common duplicated elements. We may wish to root those out in the future.
I also noticed that the icons in the AttributeSheet don’t match the ones used in the various powerups. I think that’s OK, the powerup icons are arbitrary and will probably turn into potions and other random stuff as time goes on.
For now, let’s wrap up.
Wrapping Up
I had a couple of hours of rank confusion, all caused because of two interlacing issues:
First, some entities were adding themselves to the tiles they were on, and some (Health) were not. I can’t say I didn’t know that: I did write the code, but I wasn’t aware of it, and the effect was that anything other than a Health that came out of a Chest would be added to the tile immediately.
The second issue was that objects adding themselves to tiles did so immediately, instead of going through the addDeferredContent
method. This was in fact the larger defect, and one that I knew about and “should” have avoided.
The rules of Lua are that if you are looping over a keyed table and remove an item, nothing will go wrong. If you’ve already hit that item, no harm done, and you’re guaranteed not to hit it or get in other trouble after you delete it.
With adding things to tables, all bets are off. Behavior is undefined and you’re not supposed to add to tables you’re looping over. I knew that, having done the deferred addition trick in other Codea/Lua programs, and I even used it here … I just didn’t go back and use it everywhere.
So that was a serious although simple mistake, and it took me the longest time to recognize it. I could see the deferred addition happening, and it never occurred to me that an undeferred addition was also happening.
Well, folks, that’s really what happens sometimes. Even with clean code, and that code is pretty darn clean, sometimes you miss a detail and the bear bites you.
I’ll go back and put warnings in the text so you don’t have to read every line if you don’t want to. I hope, one way or another, you got here.
The hard part is this: What lesson can we learn from this experience? Be smarter? Not likely. Be more careful? Again, not great advice. I honestly don’t know. If you have ideas, tweet me up, and I’ll try to remember to ask the Zoom Ensemble guys for ideas.
For now, all’s well that ends well, and I think we’re pretty solid with our new Loots.
Can I remove the Health class now? I think so. Let’s check. I see no references to it. Let’s rip it out and see how things go.
Ah, has to come out of TileArbiter too. And now everything is fine. Commit: remove Health class as unused.
I reckon that’s a wrap. See you next time!
P.S. I’d fire that Narrator but he sounded a lot like God to me.