Some planning, then something. Come find out with me. I have no plan for the morning.

I really have no plan for the morning, so let’s see how th old man plans. Let’s begin by reviewing the yellow sticky notes for things not yet done. I’ll transcribe them exactly as they are. There’s a point to that: in the context of a team working together to make something, we don’t need a lot of detail. We create the details as we need them.

Notes are added today, not on the sticky notes.

  • Poison
  • Don’t Die
  • Puzzles
  • Doors / keys
  • Levels
  • Level up
  • Traps
  • Write test for cloning
  • Test for acc damage
  • Pain when there are lots of tests. (Note: Partly solved.)
  • Separate projects??
  • Magic 1 in getRandomMonsters, make param. (Note: May be done.)
  • __newindex Wrong variable names
  • CombatRound appendText vs display is confusing. (Note: talking here about the names of methods.)
  • Make CombatRound easier to use in e.g. Spikes
  • Flickering torches
  • Darkness / Torches
  • Monster deterrent objects
  • Decor e.g. skels
  • Gold / Store
  • Health +1 every so often (Note: done)
  • Poison -2 every so often
  • Better test for monster behavior, called wrong method.
  • Global __index

I also have a request from UberGoober on the Codea forum, to include CodeaUnit with the package. It’s only one more tab, so it could be done with no problem. I had thought that if people wanted to play the game they wouldn’t want it, but upon reflection, if people reading these articles are taking the code, I want them to be able to run the tests. So let’s do that right now.

Including CodeaUnit

I just open the CodeaUnit project, copy the CodeaUnit tab, open D2, add blank tab named CodeaUnit, paste the code. After the usual two attempts to run, it works.

It reminds me that since I added the frogs to wander around the WayDown, some tests have broken. Let’s deal with that. But first commit: CodeaUnit bound in directly.

Now the tests that fail. I’ll see if I can just adjust some results, or if something more profound is called for.

The messages are all like this with different values:

2: monsters correctly initialized  -- Actual: 8, Expected: 6

The tests are:

        _:test("monsters correctly initialized", function()
            local gm = GameRunner()
            Runner = gm
            gm:createLevel(12)
            local mt = gm.monsters:rawTable()
            _:expect(#mt).is(6)
            for i,m in ipairs(mt) do
                _:expect(m.level).is(1)
            end
        end)
        
        _:test("monsters correctly initialized level 2", function()
            local gm = GameRunner()
            Runner=gm
            gm:createLevel(12)
            gm:createLevel(12)
            local mt = gm.monsters:rawTable()
            _:expect(#mt).is(6)
            for i,m in ipairs(mt) do
                _:expect(m.level).is(2)
            end
        end)

Here we have lovely examples of why tests at high levels are a problem. These tests are creating entire dungeon levels, and checking to see if there right number and type of monster have been created. Note that the second test creates and checks level 2, by calling createLevel twice!

I suspect this test predates the Monsters class entirely. What are we interested in testing here?

We might be interested in testing whether GameRunner knows to ask for level two monsters in the second test, i.e. whether it is tracking level numbers correctly. If we are interested in that, we should write a more direct test. In my view, however, we weren’t interested in that.

We had just created the monster level selecting code and we wanted to know whether it correctly selected from the right group.

Today, we can, and should, test that more directly. These tests need to be redone.

Again, this is a thing that happens frequently when we test large assemblies of objects, that is, when our tests are not what GeePaw Hill calls “microtests”. Maybe the opposite of microtest is macrotest. Be that as it may, macrotests often change for the wrong reasons. Microtests usually only change for the right reasons.

I’ve been feeling the lack of good testing lately, so let’s replace these tests with better ones going to the same question, whether we’re creating the right number of monsters at the right level, given the number and the level.

That’s done in the Monster class, here:

function Monster:getRandomMonsters(runner, number, level)
    local t = self:getMonstersAtLevel(level)
    local result = {}
    for i = 1,number do
        local mtEntry = t[math.random(#t)]
        local tile = runner:randomRoomTile(1)
        local monster = Monster(tile, runner, mtEntry)
        table.insert(result, monster)
    end
    return result
end

What’s to test here? We don’t really want to test whether we set them down, that’s not our concern. We just want to know whether we get N of them, if we doubt this code, and whether they are at the right level, which is done by this code:

function Monster:getMonstersAtLevel(level)
    if not MT then self:initMonsterTable() end
    local t = {}
    for i,m in ipairs(MT) do
        if m.level == level then
            table.insert(t,m)
        end
    end
    return t
end

Let’s test that, not that it could fail. I guess that ideally, we’d test two things. We’d test whether all the monsters in the output table have the right level. That’s easy. And we’d test whether we have the same number of monsters as exist at that level. That would require us to scan the monsters table.

Let’s do it all. That means I need an accessor to MT, which is local to the Monster class. That’s how I avoid contaminating the global space. Back in the tests, I’ll remove the other tests and replace:

        _:test("monsters level one correctly chosen", function()
            local mt = Monster:getMonstersAtLevel(1)
            local MT = Monster:getMT()
            local c = 0
            for i,m in ipairs(MT) do
                if m.level == 1 then c = c + 1 end
            end
            _:expect(#mt).is(c)
            for i,m in ipairs(mt) do
                _:expect(m.level).is(1)
            end
        end)

Now I need getMT:

function Monster:getMT()
    if not MT then self:initMonsterTable() end
    return MT
end

I decided to include the lazy init here, and I think I’ll refactor Monster to use the method. Once I have a method to access a member, I always feel that it’s better to use it everywhere.

I expect my new test to run. Naturally enough, it does. I remove the level two check, because this test convinces me that the function works as advertised.

Tests are green. Commit: improve monster level checking.

OK, that was fun. Now what?

Now What?

There was that sticky note about a random 1 in getRandomMonsters:

function Monster:getRandomMonsters(runner, number, level)
    local t = self:getMonstersAtLevel(level)
    local result = {}
    for i = 1,number do
        local mtEntry = t[math.random(#t)]
        local tile = runner:randomRoomTile(1)
        local monster = Monster(tile, runner, mtEntry)
        table.insert(result, monster)
    end
    return result
end

It’s the 1 there in the call to randomRoomTile. That function returns a random room tile not in the room whose number you provide:

function GameRunner:randomRoomTile(roomToAvoid)
    return self:getDungeon():randomRoomTile(roomToAvoid)
end

The getRandomMonsters method knows that we want to avoid Room 1, because it knows that we want to ignore the princess’s room and it knows that that’s room 1.

Ideally, we’d pass that down as a parameter, from GameRunner, who knows what he wants. That would be done here:

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel)
    self.monsters:createHangoutMonsters(self,2, self.wayDown.tile)
end

And here:

function Monsters:createRandomMonsters(runner, count, level)
    self.table = Monster:getRandomMonsters(runner, count, level)
end

Not too arduous I guess. GameRunner should express the idea better, however.

function GameRunner:init()
    self.tileSize = 64
    self.tileCountX = 85 -- if these change, zoomed-out scale 
    self.tileCountY = 64 -- may also need to be changed.
    self.cofloater = Floater(self, 50,25,4)
    self.musicPlayer = MonsterPlayer(self)
    self.dungeonLevel = 0
    self.requestNewLevel = false
    self.playerRoom = 1
end

And first, while we’re at it, we’ll pass that down those three levels:

function GameRunner:setupMonsters(n)
    self.monsters = Monsters()
    self.monsters:createRandomMonsters(self, 6, self.dungeonLevel, self.playerRoom)
    self.monsters:createHangoutMonsters(self,2, self.wayDown.tile)
end

function Monsters:createRandomMonsters(runner, count, level, roomToAvoid)
    self.table = Monster:getRandomMonsters(runner, count, level, roomToAvoid)
end

function Monster:getRandomMonsters(runner, number, level, roomToAvoid)
    local t = self:getMonstersAtLevel(level)
    local result = {}
    for i = 1,number do
        local mtEntry = t[math.random(#t)]
        local tile = runner:randomRoomTile(roomToAvoid)
        local monster = Monster(tile, runner, mtEntry)
        table.insert(result, monster)
    end
    return result
end

So that should be linked up just fine. But we also have some other 1s, in GameRunner:

function GameRunner:createLoots(n)
    local loots = {}
    for i =  1, n or 1 do
        local tile = self:randomRoomTile(1)
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        table.insert(loots, Loot(tile, tab[1], tab[2], tab[3]))
    end
    return loots
end

Becomes, of course:

function GameRunner:createLoots(n)
    local loots = {}
    for i =  1, n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        local tab = RandomLootInfo[math.random(1,#RandomLootInfo)]
        table.insert(loots, Loot(tile, tab[1], tab[2], tab[3]))
    end
    return loots
end

And also:

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(1)
        table.insert(things, aClass(tile,self))
    end
    return things
end

Becomes

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        table.insert(things, aClass(tile,self))
    end
    return things
end

That seems to be all of them. Test. All is well: Commit: removed magic number 1 (playerRoom) throughout.

And Now …

I noticed that I’m not using my new trial convention of putting an underbar on private member variables. I actually thought of doing it before I typed the first playerRoom and actively decided not to do it. I reckon that means that I don’t like the idea.

Arguably, I owe it to future versions of me to do it one way or the other. But clearly I am not going to go back and check each member var for whether it is private. Fact is, however, my preferred style would be that they are all private, and that no one should be grabbing them out of other objects. If they need the info at all, it should be via an accessor, and often, needing the info is a hint that some capability is in the wrong place

I’d do better to search the system for accesses to other people’s privates.

Yes. I’ll first search out the _ variables and remove the underbar.

Meh. I did a few of those. It’s not worth it. I’ll fix them as I encounter them, but there’s no big gain to doing them all now, and it’ll take a while. Commit: renamed _characterSheet.

And Now?

It would be nice to do a little something for the users, but since it’s Saturday and I’m on my own time, unlike Monday through Friday, where I’m on my own time, I feel that I can just clean up a bit of code, add tests, whatever makes sense.

One bit of semi-maintenance might be to move some of my more recent sprite finds into the game. The sticky notes mentioned “skels”, which is of course short for skeletons, and I do have some objects like that that I’ve found and downloaded. Let’s move some items in so that we can readily use them.

Here’s a pic of some of the items. I think I’ll move the skeletons and the barrels, the … heck let’s just move all of them.

If I recall the trick, I use files to move them into Codea’s dropbox and then use Codea to move them into the project.

move step 1

move selected

move target dropbox

items in place

OK, they’re moved. Let’s see about putting some of them down. We have a request for the skeletons.

We could make them instances of Loot, with no behavior. Let’s look at Loot class.

local LootIcons = {Strength=asset.builtin.Planet_Cute.Gem_Blue, Health=asset.builtin.Planet_Cute.Heart, Speed=asset.builtin.Planet_Cute.Star}

function Loot:init(tile, kind, min, max)
    self.tile = tile
    self.kind = kind
    self.icon = self:getIcon(self.kind)
    self.min = min
    self.max = max
    if tile then tile:addDeferredContents(self) end
end

We could certainly put them here. They’d be odd, giving no points to anything. Or … what if they scare you and take away a health point? That might be amusing.

Let’s look at the creation of Loots though.

function GameRunner:createLevel(count)
    self.dungeonLevel = self.dungeonLevel + 1
    if self.dungeonLevel > 4 then self.dungeonLevel = 4 end
    TileLock=false
    self:createTiles()
    self:clearLevel()
    self:createRandomRooms(count)
    self:connectRooms()
    self:convertEdgesToWalls()
    self:placePlayerInRoom1()
    self:placeWayDown()
    self:placeSpikes(5)
    self:setupMonsters(6)
    self.keys = self:createThings(Key,5)
    self:createThings(Chest,5)
    self:createLoots(10)
    self:createButtons()
    self.cofloater:runCrawl(self:initialCrawl(self.dungeonLevel))
    self:startTimers()
    self.playerCanMove = true
    TileLock = true
end

function GameRunner:createThings(aClass, n)
    local things = {}
    for i = 1,n or 1 do
        local tile = self:randomRoomTile(self.playerRoom)
        table.insert(things, aClass(tile,self))
    end
    return things
end

Things get created by instantiating a class. Do we really want to have a class for these things? Yes, I guess we’d better, but let’s just make one class, Decor, for now. It will be something like a Key, but not even that bright. Let’s check Key.

Key = class()

function Key:init(tile, runner)
    self.tile = tile
    self.tile:addDeferredContents(self)
    self.runner = runner
end

function Key:draw(tiny)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(asset.builtin.Planet_Cute.Key,g.x,g.y, 50,50)
    popStyle()
    popMatrix()
end

function Key:getTile()
    return self.tile
end

function Key:take()
    self.tile:removeContents(self)
end

OK, I think I’ll TDD a bit of decor, because I figure they’ll have random sprites.

New tab, Decor.

I figure I’ll let them randomly decide what to be, but you can choose if you wish, using a string to look up the sprite.

        _:test("Decor Creation", function()
            local tile = FakeTile()
            local runner = nil
            local decor = Decor(tile, runner, "Skeleton1")
            _:expect(decor.sprite).is(asset.Skeleton1)
        end)

With an empty class in place:

1: Decor Creation  -- Actual: nil, Expected: Asset Key: Skeleton1.png (path: "/private/var/mobile/Containers/Data/Application/483B2209-7216-46A0-8C24-5253968FCBE5/Documents/D2.codea/Skeleton1.png")

No big surprise there. Let’s code. I kind of went above and beyond. Here’s what I’ve got:

Decor = class()

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,

}

function Decor:init(tile,runner,kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    self.tile = tile
    self.tile:addDeferredContents(self)
    self.runner = runner
end

function Decor:draw()
end

function Decor:randomSprite()
    return asset.banner_1
end

Test runs. I want to test that complicated and or thing, so:

        _:test("Random Decor", function()
            local tile = FakeTile()
            local runner = nil
            local decor = Decor(tile,runner)
            _:expect(decor.sprite).is(asset.banner_1)
        end)

I expect this to run but not to last.

Test runs. Of course I want to select a random item here. Interesting question: how do we select a random item from a table of k,v pairs? And even worse, how do we test it.

Hell, I’m gonna write it.

function Decor:getDecorKeys()
    local keys = {}
    for k,v in pairs(DecorSprites) do
        table.insert(keys, k)
    end
    return keys
end

function Decor:randomSprite()
    local keys = self:getDecorKeys()
    local key = keys[math.random(1,#keys)]
    return DecorSprites[key]
end

And now I can only check for non-nil in the test:

        _:test("Random Decor", function()
            local tile = FakeTile()
            local runner = nil
            local decor = Decor(tile,runner)
            _:expect(decor.sprite).isnt(nil)
        end)

I’m satisfied that this works. Now they need to draw, and I can rip off that code from Key.

function Decor:draw(tiny)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    sprite(self.sprite,g.x,g.y, 50,50)
    popStyle()
    popMatrix()
end

Now to create some of these babies:

function GameRunner:createDecor(n)
    for i = 1,n or 10 do
        local tile = self:randomRoomTile(666)
        local decor = Decor(tile, self)
    end
end

I’ve called that with an n of 20. Note that I allow decor in the princess’s room. Let’s try this. I think it’s going to work.

Works fine:

room1 with barrels

room2 with skels

Right now we have a lot of barrels and not much else, because of this:

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,

}

We’ll have three barrels for every two skeletons. Honestly, I want more skeletons. Let’s review what else we put in the list though. There was a crate … and pots … I’ll save the full pot to be a loot, and that gives me this:

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,
Crate=asset.Crate, PotEmpty=asset.pot_empty
}

I want lots more skeletons. I’ll just add some in with names no one will have to know.

local DecorSprites = { Skeleton1=asset.Skeleton1, Skeleton2=asset.Skeleton2,
s11=asset.Skeleton1, s12=asset.Skeleton1, s13=asset.Skeleton1, s14=asset.Skeleton1,
s21=asset.Skeleton2, s22=asset.Skeleton2, s23=asset.Skeleton2, s24=asset.Skeleton2,
BarrelEmpty=asset.barrel_empty, BarrelClosed=asset.barrel_top, BarrelFull=asset.barrel_full,
Crate=asset.Crate, PotEmpty=asset.pot_empty
}

Now we should get more skeletons. Also I think 20 decor isn’t enough. I’m going to put down 30.

Here’s the starting room I get now:

lots of skeletons

The bones look too much the same. Let’s randomly flip all the sprites at creation time, so they’ll be reversed.

local ScaleX = {-1,1}

function Decor:init(tile,runner,kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    self.tile = tile
    self.tile:addDeferredContents(self)
    self.runner = runner
    self.scaleY = ScaleX[math.random(1,2)]
end

function Decor:draw(tiny)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    scale(self.scaleX, 1)
    sprite(self.sprite,g.x,g.y, 50,50)
    popStyle()
    popMatrix()
end

That should look a bit better. Most of the tiles are symmetrical but the skels will visibly flip.

After some fiddling, because I coped and pasted poorly, I get this fine result:

flipped bone images

The code is this:

local ScaleX = {-1,1}

function Decor:init(tile,runner,kind)
    self.sprite = kind and DecorSprites[kind] or Decor:randomSprite()
    self.tile = tile
    self.tile:addDeferredContents(self)
    self.runner = runner
    self.scaleX = ScaleX[math.random(1,2)]
end

function Decor:draw(tiny)
    if tiny then return end
    pushMatrix()
    pushStyle()
    spriteMode(CENTER)
    local g = self.tile:graphicCenter()
    translate(g.x, g.y)
    scale(self.scaleX, 1)
    sprite(self.sprite,0,0, 50,50)
    popStyle()
    popMatrix()
end

Let’s commit: Added 30 decor items to each level.

And let’s sum up. I’m taking the rest of the day off.

Summary

We have a nice start at decor, including what I consider a semi-elegant hack to change the proportions of items. There’s surely a better way to do that, but I can’t think of an easier or simpler one offhand. I’m sure the rules on Decor will change anyway, with level-by-level differences and so on.

It went in very nicely. The stumble with the draw method may hint that we need a more generic means of drawing tile contents. Arguably we should have just grabbed a TileDrawingFlipper object or something, and used it.

Decor items are little more than obstacles at this point. Like all room items, they are placed so that they can’t, in principle, block anything important, though I suppose randomness could position a bunch of items surrounding an important item. We might extend the decor to let you walk on it, or even make them dangerous. That’s something we’ll decide later.

Other than the nice decor, which was easier to do than I had feared, we improved a bit of code and some tests.

A nice, easy, Saturday morning. I hope yours was as well.

See you next time!


D2.zip