Dungeon 144
I guess we’re going to have to plan a bit today. Maybe a bit more fun with the new more intelligent Decor?
When this article is done, it will be the dozen-dozenth article about this little program. Honestly, I don’t think we have wrung the juice out of the topic yet. In a way, Dung is like a real product, changing and growing over a long time.
We often like to think of software development as projects that begin on some day, go for a while, and then end. But more often they are products, with work continuing on them for long periods of time, often years. And all that time, we are not mostly adding new code, we are changing old code.
My friend GeePaw Hill keeps this point central in his thinking and teaching: we are in the business of changing code. When we grok the truth of this, it makes us aware that the code we write today will likely be read many times, probably modified many times, over the course of the life of the product.
You’ll have seen in these articles that I begin every new feature by reading the code. I usually have a good idea where the feature should go in, and a fair idea of how it should go in, and yet I read the code. Not just the code around where I think the feature should go. I read the code for other similar features. I read the code for things that use code that’s similar to the code I think I’ll be writing.
Most notably, I often wind up changing different code from what I anticipated, and putting it in a different place, done differently from what I imagined before I read the code.
I try always to write clear, expressive code. I write it in the style I like to read. That may not be your preferred style, and if you were here working with me, we would develop a shared style and we’d both try to write clear code in that style. We would do it because we’re going to be reading it, often, and we’d like it to be as easy to understand, and as enjoyable to work with, as we’re able to make it.
Now, in these articles, sometimes I’ll just take some time and polish some code. I usually remind myself, and you, that in a real product situation, I don’t recommend just randomly polishing code. I recommend cleaning up code only when we have a feature to add that passes through that code. The reason is that I fear not delivering what the business needs. I’ve had products cancelled. I’ve been fired. I’ve seen companies folded because we didn’t deliver what was needed when it was needed.
Kent Beck once said that all methodologies are based on fear, whatever frightens the creator most. I fear not delivering working software. So I recommend writing the code as well as we can when we write it, and cleaning it up judiciously when we pass through it. I point out that if we’re not reading it, it’s not bothering anyone.
But here, I have the luxury of showing some code that is as good as I could make it on the day I last worked with it, and showing how, today, I may be able to make it a bit better. If we have to read it today, we’ll probably be reading it again, so if we can make it a bit more clear, that’s a good thing.
Some folks would add comments, and that’s not a terrible thing to do. But Kent Beck (again) taught us that “a comment is the code’s way of asking to be made better”. So where we can, it’s likely better to improve the code than to comment it.
But, always, we’re doing our best. We decide among conflicting goals, we do the work, and we come back tomorrow and do it again.
It’s Tomorrow Now …
Actually it’s still today, but as of yesterday today was tomorrow, so what shall we do today? Options include:
- More ?? Button
- We could add information messages to more items, although everything now has some kind of message.
- Active Decor
- Decor items were originally intended (by me) just to lie around creating atmosphere. However, now that they are there, it seems to make sense to let them do things. They can poison you–it’s dirty down in a dungeon–and maybe you should be able to find things if you rummage through them. (It just occurred to me that you might find a map, that would illuminate part or all of the small dungeon map.)
- Improve Runner-Tile-Dungeon
- The relationship between those objects seems more complicated than it needs to be. Improving it might make development go faster. Or it might just be an attractive nuisance.
- Inventory Focus
- I think I’d like to move almost all found objects to inventory rather than have them act as immediate power ups.
- Undefined Variables
- It might be possible to do a deep in the bag mod to Lua so that undefined variables would produce an error message. Of course, they usually do anyway, so maybe this is moot.
- Puzzles
- I’ve been promising myself puzzles for a long time, and there still aren’t any.
- Point of the Game
- The game has no point. You don’t even have a score to try to beat. Maybe there is some fundamental point, like find and rescue the frog, or return a book to the library. And maybe there is a running score of some kind, points that accumulate when you find things, or use them properly.
- Something Completely Different
- Maybe we should shelve this product and work on something else. If you know of something you’d rather see me working on, drop me a tweet or email or something.
A Random Idea Has Appeared
Let’s do this. You need a key in order to go down the WayDown. Keys are just lying about in the dungeon now. Let’s change that. Let’s hide keys in Decor items.
Decor can be poisonous. We need to ensure that the player has a chance, so perhaps some decor items contain antidotes, some contain keys, some are poisonous, some have nothing in them.
Things like Decor and Loot are random, so it is possible, in principle, that the game would never provide an antidote, or a key. (We do keys separately right now, so there are guaranteed to be some of those.)
We place Decor and Loot randomly. Loot never goes in Room 1, but Decor can. What if the game had a list of items that must appear in the dungeon? Since Loots and Decor are laid in randomly, we could just select an item, with removal, from the list, create the Loot or Decor with that item, repeat. If the list is consumed, and we have enough things created, we stop.
Oh I like that. Let’s see how we could do it. It’s code-reading time.
Loots are created here:
local RandomLootInfo = {
{"Strength", 4,9},
{"Health", 4,10},
{"Speed", 2,5 },
{"Pathfinder", 0,0},
{"Antidote", 0,0}
}
function GameRunner:createLoots(n)
for i = 1, n or 1 do
local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
local tile = self:randomRoomTile(self.playerRoom)
Loot(tile, tab[1], tab[2], tab[3])
end
end
And Decor here:
function GameRunner:createDecor(n)
for i = 1,n or 10 do
local tile = self:randomRoomTile(666)
local decor = Decor(tile, self)
end
end
We also create Chests as special items, and they always contain health. We should probably fold those into whatever we’re doing here. Same with Keys.
Let’s more toward there being no free-standing Loot. You must always search decor to find valuable items. It’s risky in the dungeon. Hmm … I wonder if this is going to get rid of the loot class entirely, since we also want to move to there being no instant power ups. There might be only inventory items.
Let’s read Inventory and InventoryItem.
InventoryItem = class()
function InventoryItem:init(icon, name, object, message)
self.icon = icon
self.name = name or icon
self.object = object or self
self.message = message or "print"
end
function InventoryItem:touched(aTouch, pos)
if aTouch.state == ENDED and manhattan(aTouch.pos,pos) < ItemWidth//2 then
Inventory:remove(self)
self.object[self.message](self.object)
end
end
When we create an InventoryItem we tell it its message and the object that should receive the message. There are just two cases for now:
function Loot:addPathfinderToInventory(aPlayer)
local item = InventoryItem("blue_jar", "Magic Jar", aPlayer, "spawnPathfinder")
Inventory:add(item)
end
function Loot:addAntidoteToInventory(aPlayer)
local item = InventoryItem("red_vase", "Poison Antodote", aPlayer, "curePoison")
Inventory:add(item)
end
Both of these are currently done in Loot: when you step onto those Loots, they put the item in your inventory, and then if you touch the item later, it cures your poison or spawns a pathfinder.
I see at least two ways we could go. We could turn all our Decor into Loots, with various add methods to add things to Inventory, while using Decor-style images in the Tile. That would eliminate the need for the Decor class. Or we could do it all with Decor items, eliminating Loot from the equation.
Loot and Decor classes are approximately the same size. Loot has this bit of code:
function Loot:actionWith(aPlayer)
self.tile:removeContents(self)
if self.kind == "Pathfinder" then
self:addPathfinderToInventory(aPlayer)
elseif self.kind == "Antidote" then
self:addAntidoteToInventory(aPlayer)
else
aPlayer:addPoints(self.kind, math.random(self.min, self.max))
end
end
That’s trying to turn into something more sensible, but what it is hasn’t shown up yet. If we move to never adding points immediately, that last bit would be removed.
Let’s review how the things are added. We’ll have to do something like that either way:
function Loot:addPathfinderToInventory(aPlayer)
local item = InventoryItem("blue_jar", "Magic Jar", aPlayer, "spawnPathfinder")
Inventory:add(item)
end
function Loot:addAntidoteToInventory(aPlayer)
local item = InventoryItem("red_vase", "Poison Antodote", aPlayer, "curePoison")
Inventory:add(item)
end
It’s pretty clear that those could be table driven. Which reminds me of something I wanted to talk about.
Data
We see above some very explicit code, procedurally adding specific inventory items, specifically examining the kind of Loot we have, and so on. Often we would do better to represent that information with data rather than code.
I suppose that all code can be seen as manipulating some data structure. If we represent two vectors by vec1x
, vec1y
, vec2x,
vec2y`, that’s a rather poor data structure, and if we want to add those two vectors and put the result in a third, we write code like this:
vec3x = vec1x + vec2x
vec3y = vec1y + vec2y
That’s truly horrible and quite error-prone. We might store them in arrays:
vec1 = {10,20}
vec2 = {1,2}
vec3 = { vec1[1]+vec2[1], vec1[2]+vec2[2]
This would probably be better. We might then write a function:
function vecAdd(v1,v2)
return { v1[1]+v2[1], v1[2]+v2[2] }
end
This would be better still. And if we were to implement a vector class, we could get to this:
vec1 = vector(10,20)
vec2 = vector(1,2)
vec3 = vec1 + vec2
And that would be nearly good.
The general point is that we can often make our code smaller, simpler, and more communicative by using a different data structure that’s more appropriate. What do I mean by more appropriate? Well, just that “when we use this structure our code becomes smaller, simpler, and more communicative”. Kind of recursive, but that’s the point.
We can adjust the quality of our code by adjusting the structure of the data.
I find, as a rule, that I often see how to write something out longhand before I see what a preferable data structure might be. That’s where we are with the Loot items that to into inventory. They’re new, and when I did them, the pattern wasn’t clear yet in my mind, and, accordingly, it’s not as clear in the code as it might be.
Oh, it’s not bad. We can read it. But we can also see that if there were going to be 15 different kinds of inventory items, we’d have 15 if statements and 15 add statements, and that would be bad. Somewhere between 2 and 15 we need to do better.
And we’ll do better by recording what we want as involving more data and less code. This is often the case.
Thanks for reminding me that I wanted to talk about that. Let’s get back to it.
Decor and Loot
Here’s what’s in my head on this topic. It’s not well-formed, but it is coming into view through the fog.
- Have just one kind of thing that appears on the floor as a loot-decor item.
- Any floor display icon can in principle contain any inventory item.
- We may wish to have some control over which floor display contains which item.
- We have some kind of list of items which must be available in the level.
- This list might be different by level.
- The floor item’s ?? messages might vary depending on what they hold.
- We may want to ensure that certain items are available in room one, and that others definitely are not.
- We probably don’t need both Decor and Loot classes.
I rather like the phrase “floor item”, although “decor” is pretty nice as well.
I think the choice between Decor and Loot classes is pretty arbitrary. I think I’ll work to keep Decor and remove Loot. Decor seems a bit simpler to me, and it’s newer, which may make it better. But I grant that I’m just making this choice on gut feel.
I’ll begin by putting our pathfinder and poison cure items into Decor, and then we’ll see what happens. If it doesn’t go well, we’ll go some other direction.
Again, here’s how we create Decor:
function GameRunner:createDecor(n)
for i = 1,n or 10 do
local tile = self:randomRoomTile(666)
local decor = Decor(tile, self)
end
end
Maybe I should do some TDD here. Yes, I should.
TDD For New Decor
I want to TDD the new creation loop. I’ll try to write a test to express what I want to say:
_:test("Decor with Inventory Items", function()
local someList = {}
local items = Decor:createRequiredItems(someList)
_:expect(#items).is(#someList)
end)
Not a lot of detail yet, but the idea is that I’ll give a control list to Decor, and it will return a list of items. What will the items be? Most likely they should be instances of Decor. If they are, then then should already have a Tile and a GameRunner. Now I prefer the Complete Creation Method pattern, where when you create an object it’s good to go, not needing injection later of things like runners or tiles. So that suggests that I need either to let this guy create the Tiles, and pass it the runner … or something.
But creating tiles is a hassle to test, because I have to pass in a live GameRunner or maybe a Dungeon, and next thing you know I’ve got an integration tests on my hands. So let’s instead pass in a bunch of tiles, enough for each one of the items in someList
, and we’ll pass in a runner, and the create will just zip the lists together.
_:test("Decor with Inventory Items", function()
local someList = {"abc"}
local rooms = map(someList, function(x) return FakeFile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item).is("abc")
end)
I’m going to let the items in someList
be anything. The Decor, for now, will just store them.
Now I can sort of write this method. I want to write the loop, so I’m going to enhance the test before making it work. This is a slightly larger reach but I feel strong.
_:test("Decor with Inventory Items", function()
local someList = {"abc", "def"}
local rooms = map(someList, function(x) return FakeFile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item).is("abc")
_:expect(items[2].item).is("def")
end)
The code:
function Decor:createRequiredItems(items, tiles, runner)
local result = {}
for i = 1, #items do
local item = items[i]
local tile = tiles[i]
table.insert(result, Decor(tile, item, runner))
end
return result
end
function Decor:init(tile, item, runner, kind)
self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
self.item = item
tile:moveObject(self)
self.runner = runner
self.scaleX = ScaleX[math.random(1,2)]
self.dangerous = math.random() > 0.5
end
I changed the init calling sequence, and the other two Decor tests to match. The test runs so far, so we know that we are creating a Decor for each item we pass in. We could pass in a bunch of nils to have empty ones, or a fake item.
Now let’s look back at InventoryItem and see what we might have to say, so that we can define our control table.
function InventoryItem:init(icon, name, object, message)
self.icon = icon
self.name = name or icon
self.object = object or self
self.message = message or "print"
end
function Loot:addPathfinderToInventory(aPlayer)
local item = InventoryItem("blue_jar", "Magic Jar", aPlayer, "spawnPathfinder")
Inventory:add(item)
end
function Loot:addAntidoteToInventory(aPlayer)
local item = InventoryItem("red_vase", "Poison Antodote", aPlayer, "curePoison")
Inventory:add(item)
end
Let’s assume for now that we always want to send the item’s message to the player. Looks like we’ll have to pass that in, or we can rely on the runner. Let’s defer that concern. Everything else can be in our table. I change the test:
_:test("Decor with Inventory Items", function()
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = { icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList = {i1,i2}
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].name).is("Poison Antidote")
_:expect(items[2].name).is("Magic Jar")
end)
All these things are just strings, which is a bit troubling but we’ll revisit that. For now we code:
… No. We think …
If I leave the test as it stands, the Decor will have to create InventoryItem instances. That’s not its job. Its job is to store the items until it’s called upon to put them into inventory. So I need to give it the items. I’ll do this:
_:test("Decor with Inventory Items", function()
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = { icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList = map({i1,i2}, function(t)
return InventoryItem(t.icon, t.name, nil, t.method)
end)
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].name).is("Poison Antidote")
_:expect(items[2].name).is("Magic Jar")
end)
You remember the map
function, right? It produces a new list by iterating the input list, calling the provided function on each element. In this case it’ll create InventoryItems. At least that’s my plan.
3: Decor with Inventory Items -- Actual: nil, Expected: Poison Antidote
3: Decor with Inventory Items -- Actual: nil, Expected: Magic Jar
I suspect I’d better test the map call.
_:test("Decor with Inventory Items", function()
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = { icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList = map({i1,i2}, function(t)
return InventoryItem(t.icon, t.name, nil, t.method)
end)
_:expect(someList[2].name).is("Magic Jar")
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].name).is("Poison Antidote")
_:expect(items[2].name).is("Magic Jar")
end)
That new expect does work. Oh. I need to grab the item:
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item.name).is("Poison Antidote")
_:expect(items[2].item.name).is("Magic Jar")
Test runs. Test was wrong, code was right.
Now, we’re using TDD to drive out a protocol that we like. So do we like this?
If we were building a real product, we would imagine a team of Decor makers and InventoryItem makers, and they’d be churning out decor and items madly. To plug their stuff into the system, they’d provide us with assets for the icons, they’d request new methods to perform actions, and they’d probably want to give us a file of textual InventoryItem descriptions, and we’d use that file to drive our creation.
Here, we don’t have that situation, and I think creating this list with map is arguably wasteful. However, I do rather like the notation:
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
I’ve kind of finessed the provision of the target of the message curePoison
or whatever, and that’s not entirely laziness.
What I’m liking about this little scheme so far is that the Decor and InventoryItem are both rather independent of the rest of the system. There is a requirement to be handed a tile, or something that acts like a tile, and to be handed an InventoryItem, or something that acts like one, but we haven’t had to link up a whole dungeon just to test this setup.
That’s a very good thing, and I rather wish I had done it elsewhere.
But there is the possibility of errors. If I mystipe icon as Icon or method as mentos, something’s going to blow up much later on. That’s not so good. Let’s do this:
_:test("Decor with Inventory Items", function()
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = { icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList = map({i1,i2}, function(t)
return InventoryItem:fromTable(t) -- < ---
end)
_:expect(someList[2].name).is("Magic Jar")
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item.name).is("Poison Antidote")
_:expect(items[2].item.name).is("Magic Jar")
end)
I’m positing a new method, fromTable
. Hm. There is an alternative. Lua provides a second way to create an object.
If we type Someclass(x,y,z)
, the init
will be called with the usual argument list, by position x,y,z. But if we said:
Someclass{x=31, z=19, y=77}
That’s the same as if we had said:
Someclass( {x=31, z=19, y=77} )
And the creation method can then look into the table by name and pull out its arguments. Let’s change InventoryItem to work that way:
function InventoryItem:init(aTable)
self.icon = aTable.icon
self.name = aTable.name or self.icon
self.object = aTable.object or self
self.message = aTable.method or "print"
end
Now we get to fix all the calls:
function Loot:addPathfinderToInventory(aPlayer)
local item = InventoryItem{icon="blue_jar", name="Magic Jar", object=aPlayer, method="spawnPathfinder"}
Inventory:add(item)
end
function Loot:addAntidoteToInventory(aPlayer)
local item = InventoryItem{icon="red_vase", name="Poison Antodote", object=aPlayer, method="curePoison"}
Inventory:add(item)
end
_:test("add items to inventory", function()
local i1 = InventoryItem{icon="green_staff"}
local i2 = InventoryItem{icon="snake_staff"}
_:expect(Inventory:count()).is(0)
Inventory:add(i1)
_:expect(Inventory:count()).is(1)
Inventory:add(i2)
_:expect(Inventory:count()).is(2)
end)
_:test("Inventory item always has target and message", function()
local item = InventoryItem{icon="foo"}
_:expect(item.name).is("foo")
_:expect(item.object).is(item)
_:expect(item.message).is("print")
end)
Let’s see if I did this right. Almost. This error:
6: neighbors -- Actual: Decor, Expected: Decor
Here’s the failing test:
_:test("neighbors", function()
local tile = Tile:room(10,10,Runner)
local t9 = dungeon:getTile(vec2(9,9))
local decor = Decor{t9, runner}
dungeon = Runner:getDungeon()
local neighbors = dungeon:neighbors(tile)
_:expect(#neighbors).is(8)
_:expect(neighbors, "same t9").has(t9)
local queryable = dungeon:nearestContents(tile)
_:expect(queryable).is(decor)
end)
That one needs improvement. I wonder what went wrong. We got some decor back but not the one we expected. Ah, it’s because Decor expects tile, item, runner, kind now. We need to provide an item.
_:test("neighbors", function()
local tile = Tile:room(10,10,Runner)
local t9 = dungeon:getTile(vec2(9,9))
local decor = Decor(t9, "fake item", Runner)
dungeon = Runner:getDungeon()
local neighbors = dungeon:neighbors(tile)
_:expect(#neighbors).is(8)
_:expect(neighbors, "same t9").has(t9)
local queryable = dungeon:nearestContents(tile)
_:expect(queryable).is(decor)
end)
The tests all run. Let’s think about what we’re creating here:
_:test("Decor with Inventory Items", function()
local i1 = { icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = { icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList = map({i1,i2}, function(t)
return InventoryItem(t)
end)
_:expect(someList[2].name).is("Magic Jar")
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item.name).is("Poison Antidote")
_:expect(items[2].item.name).is("Magic Jar")
end)
The question I’m asking myself is whether this would be better:
_:test("Decor with Inventory Items", function()
local i1 = InventoryItem{ icon="red_vase", name="Poison Antidote", method="curePoison" }
local i2 = InventoryItem{ icon="blue_jar", name="Magic Jar", method="spawnPathfinder" }
local someList= {i1,i2}
local rooms = map(someList, function(x) return FakeTile() end)
local items = Decor:createRequiredItems(someList, rooms, runner)
_:expect(items[1].item.name).is("Poison Antidote")
_:expect(items[2].item.name).is("Magic Jar")
end)
It binds a bit earlier, but the code is simpler. I think that’s a better protocol. But we’re not there yet.
What about the poison aspect? Maybe that should be independent? Some Decor is poisonous, take your chances? Or maybe we’ll decide that none of it is quite that dangerous, and limit poison to venomous monsters. Let’s not build it in.
What about the decor messages? Right now they’re just random:
function Decor:query()
local answers = {"I am junk.", "I am debris", "Oh, just stuff.", "Well, mostly detritus, some trash ..."}
return answers[math.random(1,#answers)]
end
That’s not our concern here. Our concern here is creating decor that contains the items we provide. And then, when they are stepped upon, they are supposed to give the item.
We’d better get down to it. Let’s go back to where we create the real Decor:
function GameRunner:createDecor(n)
for i = 1,n or 10 do
local tile = self:randomRoomTile(666)
local decor = Decor(tile, self)
end
end
We need n
InventoryItems for the decor to provide, given our new scheme. And I note that we don’t even hold on to the decor. Nice.
So …
Uh oh. If I create the tiles before setting down the individual decor, there is a chance that two decor will wind up in the same tile. Let’s deal with that later but not forget. I’ll make a sticky note. Carrying on …
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", method="spawnPathfinder" },
nil,nil,nil
}
items = {}
tiles = {}
for i = 1,n or 10 do
table.insert(tiles,self:randomRoomTile(666))
table.insert(items, sourceItems[1 + i%#sourceItems])
end
Decor:createRequiredItems(items,tiles)
end
I think this should create as many Decor as are required, and 2/5 of them will have items and 3/5 will not. I think I’ll make them non-poisonous for now:
function Decor:actionWith(aPlayer)
--if self.dangerous then aPlayer:poison() end
if self.item then Inventory:add(self.item) end
end
I rather expect this to work, and I suddenly realize I’ve not committed any code this morning. Wow, bad Ronald1.
Running, I notice these things:
- Magic Jar has no message about receiving one.
- I’m not sure if anyone gets non items … oh. Nils at the end of a table don’t count in #.
We’ll need to do something else to signify no item. Right now, everyone’s going to get something.
What about that received message?
function Inventory:add(item)
table.insert(inventory, item)
item:informObjectAdded()
end
function InventoryItem:informObjectAdded()
self:informObject("You have received a "..self.name.."!")
end
function InventoryItem:informObject(msg)
local implementsInform = self.object["inform"]
if implementsInform then self.object:inform(msg) end
end
function Player:inform(message)
self.runner:addTextToCrawl(message)
end
That looks OK. Did I create them poorly?
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", method="spawnPathfinder" },
nil,nil,nil
}
Sure did. Forgot the object
in the second one. And the nils don’t work.
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder" },
}
items = {}
tiles = {}
for i = 1,n or 10 do
table.insert(tiles,self:randomRoomTile(666))
table.insert(items, sourceItems[1 + i%#sourceItems])
end
Decor:createRequiredItems(items,tiles)
end
Let’s put in some blank ones.
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder" },
InventoryItem:null(),
InventoryItem:null(),
InventoryItem:null(),
}
I want to have a null item. To do that I first change this:
function Decor:actionWith(aPlayer)
--if self.dangerous then aPlayer:poison() end
if self.item then item:addToInventory() end
end
Now we’ll tell the item to add itself. Then:
function InventoryItem:addToInventory()
if self.icon then Inventory:add(self)
end
We won’t add an item without an icon, and …
function InventoryItem:null()
return InventoryItem{}
end
However, I’d like a message. Darn. OK …
function InventoryItem:null(object)
return InventoryItem{object=player}
end
function InventoryItem:addToInventory()
if self.icon then Inventory:add(self) end
if self.object then self:informObject("You received nothing.") end
end
This feels a bit messy. Back to the create, though:
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder" },
InventoryItem{object=player},
InventoryItem{object=player},
InventoryItem{object=player},
}
Now I don’t need null
after all. Delete it. It wouldn’t have worked anyway, referring to player like that.
Run. A crash:
Decor:77: attempt to index a nil value (global 'item')
stack traceback:
Decor:77: in method 'actionWith'
Player:237: in local 'action'
TileArbiter:27: in method 'moveTo'
Tile:110: in method 'attemptedEntranceBy'
Tile:390: in function <Tile:388>
(...tail calls...)
Player:192: in method 'moveBy'
Player:140: in method 'executeKey'
Player:186: in method 'keyPress'
GameRunner:332: in method 'keyPress'
Main:38: in function 'keyboard'
Oops. Going too fast. Steps way too big. Need to settle down.
function Decor:actionWith(aPlayer)
--if self.dangerous then aPlayer:poison() end
if self.item then item:addToInventory() end
end
Forgot self.
function Decor:actionWith(aPlayer)
--if self.dangerous then aPlayer:poison() end
if self.item then self.item:addToInventory() end
end
Says I received a magic jar, and nothing. Bad Ronald.
function InventoryItem:addToInventory()
if self.icon then
Inventory:add(self)
elseif self.object then
self:informObject("You received nothing.")
end
end
With this code in place, the empty decor don’t say you received nothing. Otherwise all seems well. Let’s start at the top:
Arrgh:
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder" },
InventoryItem{object=player},
InventoryItem{object=player},
InventoryItem{object=player},
}
These all need to say self.player
. Dave, my mind is going. I can feel it.
function GameRunner:createDecor(n)
local sourceItems = {
InventoryItem{ icon="red_vase", name="Poison Antidote", object=self.player, method="curePoison" },
InventoryItem{ icon="blue_jar", name="Magic Jar", object=self.player, method="spawnPathfinder" },
InventoryItem{object=self.player},
InventoryItem{object=self.player},
InventoryItem{object=self.player},
}
items = {}
tiles = {}
for i = 1,n or 10 do
table.insert(tiles,self:randomRoomTile(666))
table.insert(items, sourceItems[1 + i%#sourceItems])
end
Decor:createRequiredItems(items,tiles)
end
This should suffice.
And it does. Whew. Commit: Decor now provide magic jars, poison antidotes, or nothing, and say so.
OK it’s 1124 and I started before 0800. Only one commit all morning. Let’s reflect and then get this baby published.
Reflection
When all is said and done, this is nearly OK. We’ve modified Decor to hold InventoryItems, which they will provide when stepped on. Looking forward, we should be able to convert all our current Loot to Decor, and maybe even the Chest. We can move Keys to be inventory items.
We’ve already recognized the need to display a count of items instead of duplicates, so that was no surprise.
There is some questionably fancy stuff in there.
We have our only keyword creation method in InventoryItem, which accepts a table containing keys icon, name, object, and method. We should at least make it check its input for validity. And the method may be weird enough that we’ll go back to the other, but I rather like it.
In the olden days, with Smalltalk, arguments were always done in keyword fashion, so I rather like the style, but here in Lua it’s new and unlike most of the system. We’ll have to decide whether we like it. For now, I do like it.
The handling of Decor without items to give is a bit odd. We might want to toughen that up a bit. Validating items would help.
We went to that interesting creation method where we pass in a collection of rooms to match the collection of items. I like that that means that the Decor doesn’t know how to create a tile, and that the tile can be anything, because it makes testing easier. But it might be obscure. We’ll see the next few times we have to read the code. I expect it’ll be just fine, and that we’ll want to find similar ways to remove dependencies.
We actually did TDD the bulk of the changes, but should have committed before starting to plug the new capability in. That was an oversight, not an explicit decision, but in the end it turned out OK. Had we run into more trouble, there would have been a big revert to deal with.
You didn’t see this happen, but the output from the tests is still too much, even when I turn off details. I would like to change CodeaUnit so that with a flag setting it only displays the tests that fail and no other boilerplate at all. That may be a tiny bit tricky, but only a tiny bit. (He said …)
All in all, a good morning, and a nice new feature. We have cleaning to do, but it’s fit to release.
See you next time!
-
They always use your full name when scolding. Call me Ron, please. ↩