Now that we can slice images, let’s see what it takes to plug them in. I’m sure we’ll need to adjust how things work.

Yes. We’re surely going to have to change some code in order to accommodate sliced images. One alternative would be to slice them outside the program and give each slice a unique name in our assets folder. That makes managing the assets a bit harder, but there are already a zillion of them. In a more mature system, we’d probably have file folders to help us organize them. If we really cared, we could organize them by name prefixes or something.

Another alternative, and the one I’ve been assuming we’d use, is to extend the definitions of Monsters and Loots and whatnot, to be able to refer either to assets or slices of assets.

(It comes to me suddenly that when we use an entire asset at once, it is just a rather complete slice of that asset. That might be a useful observation.)

Be that as it may, if we do use the slice notion in the Dung program, we’ll need to extend code like this:

    local mtEntry = {name="Poison Frog", level=3, health={4,8}, speed = {2,6}, strength={8,11},
    attackVerbs={"leaps at", "smears poison on", "poisons", "bites"},
    dead=asset.frog_dead, hit=asset.frog_hit,
    moving={asset.frog, asset.frog_leap}}

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

Homily

I want to do a little homily here. Some readers might be thinking something like

If we had planned this project properly, we’d have known all along that we would need slices of sprite sheets. Everyone in gaming knows about sprite sheets. The need to change this is a failure of planning.

They’d probably go on to remember a time in the distant past where they had said “We’re gonna be in big trouble, folks, if we don’t put in code for sprite sheets right at the beginning.” They might even say “I told you so!”

Yes, well, don’t imagine for a minute that I didn’t know about sprite sheets. I did. I’ve been programming since 19-bloody-61 and I know about sprite sheets. I just don’t put sprite sheets into every program I write. I put them in when I need them.

We are in the business of changing code. Not writing perfect one-off code, changing code. The best way we know to write software is incrementally, most important bits first, always shippable, always improving. Change is our daily bread. How do we avoid getting cornered, finding ourselves in some situation where the next change is impossible, because of some decision we made months ago. If only we had known! And Eddie, Eddie told us, didn’t he?

We avoid getting cornered by writing code with what I like to call “good design”, encompassing all kinds of notions, certainly including low coupling, and high cohesion. What that means is that we try to write objects that take on a single area of responsibility, and that are connected together loosely.

In this program, we have mostly accomplished that kind of design. It’s “good”, though certainly not great, and usually things aren’t so interconnected as to be a problem.

Usually. We do have issues setting up tests because of the connections between some core classes, notably GameRunner and everyone else. We’ve done some work to untangle those connections, like adding the Dungeon class, and we’ve done some work to learn how better to test in that environment, using Fake test double objects. We may, in future, do something more about that.

But we’ve always been able to plug in a new capability readily. I expect this new challenge to be no different

We are in the business of changing code. That’s not easy, but it’s easier than predicting the future, and much more effective than trying to build the last version first, by piling in every capability we can imagine may some day be needed.

We are in the business of changing code.1

But Slices?

Thinking, as one does, I recognize that our Slice idea has two separate aspects:

First, there is the dimensioning of the slice logic for a particular asset. This requires us to specify how many images there are in an asset, and to specify whether there are lines of pixels to skip over. This is the information in our SpriteSheet creation method:

function SS:init(sheet, numberX, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)

Second, we actually use these images one at a time. We slice a dozen staffs out of the staffs image, but we use them one at a time. In the SpriteSheet class, we select them by number:

function SS:sliceFromImage(img,sliceNumber)

Now, you’ve got to ask yourself whether you really want to use a number. We could imagine defining a monster using a numerical extension on the asset:

dead=asset.frog[3]

That’s going to be easy to get wrong, and it’s not easy to test, since we have no current facility for creating a monster and watching it go through its antics. Perhaps we should have. There could be monsters in there right now that have mistakes in their definitions.

Even if we accept the use of the number, we don’t have enough information in that single number. Remember the number of slices and pixel-skipping info.

Now I don’t want to make a huge deal about the number vs name thing. It’s almost as easy to get frog[dead] wrong as it is to get frog[3] wrong. But symbolic names are better, surely.

OK, imagine that instead of saying asset.frog when we want the whole asset, we said slice.frog_dead. And imagine that we had a standard object that takes a string like asset.frog or slice.frog_dead and if it’s an asset, returns it to the caller, and if it’s a slice, looks it up in an internal table of all slices and returns that image.

It would be essentially a table with key of slice name and value pointing (somehow) to a SpriteSheet, and providing the index of the slice desired.

We would have an initializing method, to be run at game startup, that would populate this table. We could populate it from a literal table, or from a file of values, or whatever seemed useful.

If we go as I’m describing now, we have to give each slice a unique name. That’s not an incredible burden, we already have each existing asset with a unique (file) name.

Let’s go down that path.

Story

Modify asset access so that in monsters, loots, and everywhere, the system can accept slice.name in addition to asset.name, and look up the image in a table of slices.

We’ll have to deal, among other things, with every call to sprite, to plug in the translation. Or at least, in any object that we want to give access to sprite sheets.

Let’s return to my spike from yesterday, and write some more tests, building this new facility.

I think I’ll call it Images. Or maybe Sprites. We’ll see.

I begin my test … and decide that the thing is to name the sprites, and use the number of names to get the number of sprites to cut. That is, if there are 12, we provide 12 names.

        _:test("Sprites lookup", function()
            local names = {"crossed", "green_staff", "feather_staff", 
            "purple_staff", "snake_staff", "cudgel_staff", "skull_staff",
            "bird_staff", "jewel_staff", "knob_staff", "crystal_staff", "twist_staff"}
            _:expect(#names).is(12)
        end)

So far so good. Now what? Remember that we want a new object that we can ask for one of these names, and, somehow, it fetches the appropriate image. Let’s see if we can write a test for this object. Its name is Sprites, for now.

        _:test("Sprites lookup", function()
            local testss = SS(sheet, 12, 1,0, 0,1)
            local names = {"crossed", "green_staff", "feather_staff", 
            "purple_staff", "snake_staff", "cudgel_staff", "skull_staff",
            "bird_staff", "jewel_staff", "knob_staff", "crystal_staff", "twist_staff"}
            _:expect(#names).is(12)
            local sp = Sprites()
            sp:add(names, sheet, 1,0, 0,1)
            _:expect(sp:sprite("green_staff")).is(testss.slices[2])
        end)

I grab an SS to use in my check, then posit a class Sprites and two methods, add and sprite. This should fail enough to drive out my code.

4: Sprites lookup -- Tests:41: attempt to call a nil value (global 'Sprites')

I go this far:

Sprites = class()

function Sprites:init()
    self.sprites = {}
end

function Sprites:add(names, sheet, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
    
end

Note that I am not doing the pure “TDD ritual”. It’s good to know that ritual, never writing a line of code without a test that requires it, step by step, inch by inch, but when I have code in my head, I type it in, even if a smaller step might be possible.

Often, that works. Sometimes it doesn’t. When it doesn’t, I go to smaller steps. It’s a continuing adjustment.

Test should run and fail with a nil.

4: Sprites lookup -- Tests:43: attempt to call a nil value (method 'sprite')

Not the one I expected, but not really surprising.

function Sprites:sprite(name)
    
end

Now the framework is in, but no guts.

Oddly, the test runs. I think I have a missing something. But what?

For the second expect to run, testss.slices[2] must have returned nil. What did I do wrong? I don’t know, let’s do put in an assert on it.

Well, I did that, but the bug is that you have to either call sliceAll or slice(n) before a slice exists in the SS. So:

        _:test("Sprites lookup", function()
            local testss = SS(sheet, 12, 1,0, 0,1)
            local names = {"crossed", "green_staff", "feather_staff", 
            "purple_staff", "snake_staff", "cudgel_staff", "skull_staff",
            "bird_staff", "jewel_staff", "knob_staff", "crystal_staff", "twist_staff"}
            _:expect(#names).is(12)
            local sp = Sprites()
            sp:add(names, sheet, 1,0, 0,1)
            _:expect(sp:sprite("green_staff")).is(testss:slice(2))
        end)

Now I get the error I wanted:

4: Sprites lookup  -- Actual: nil, Expected: Image: width = 47, height = 63 (raw_width = 47, raw_height = 63)

Time to do some work. Here’s my cut:

function Sprites:add(names, sheet, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
    local ss = SpriteSheet(sheet, #names, xSkipLow, xSkipHigh, ySkipLog, ySkipHigh)
    for i,name in ipairs(names) do
        if name ~= "" then
            self.sprites[name] = ss:slice(i)
        end
    end
end

function Sprites:sprite(name)
    return self.sprites[name]
end

I decided that you can give a thing an empty string as a name and we don’t read it. I note in passing that we will read and create all the images at the time of adding things to the Sprites object. That seems fine.

I kind of think this test is going to run. As so often happens, my think is wrong:

4: Sprites lookup -- Tests:100: attempt to perform arithmetic on a nil value (field 'ySkipLow')

That’s probably a typo somewhere. Yes. I said “Log” not “Low”.

function Sprites:add(names, sheet, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
    local ss = SpriteSheet(sheet, #names, xSkipLow, xSkipHigh, ySkipLow, ySkipHigh)
    for i,name in ipairs(names) do
        if name ~= "" then
            self.sprites[name] = ss:slice(i)
        end
    end
end

Now I get a surprising message:

4: Sprites lookup  -- Actual: Image: width = 47, height = 63 (raw_width = 47, raw_height = 63), Expected: Image: width = 47, height = 63 (raw_width = 47, raw_height = 63)

I have a sprite but they’re not equal. Did I count incorrectly in the test?

I wonder whether the two images are not being compared byte by byte, but for identity. They are userdata, and will not be identical. I could, I suppose, compare them using the raw set and get methods on images.

Fact is, I’m sure this is working. (I could be wrong, of course: I often am. But I’m sure.)

I’m going to use the new object in Main.

function setup()
    if CodeaUnit then 
        codeaTestsVisible(true)
        runCodeaUnitTests() 
    end
    local img = readImage(asset.documents.Dropbox.gear_staffs_2)
    local n = 3
    slice = img:copy(n*48+2,1,47,63)
    ss = SpriteSheet(asset.documents.Dropbox.gear_staffs_2, 12, 1,0, 0,1)
    ss:sliceAll()
    
    local names = {"crossed", "green_staff", "feather_staff",
    "purple_staff", "snake_staff", "cudgel_staff", "skull_staff",
    "bird_staff", "jewel_staff", "knob_staff", "crystal_staff", "twist_staff"}
    sprites = Sprites()
    sprites:add(names, asset.documents.Dropbox.gear_staffs_2, 1,0, 0,1)
end

function draw()
    if CodeaUnit then showCodeaUnitTests() end
    background(256)
    sprite(asset.documents.Dropbox.gear_staffs_2,800,200)
    sprite(slice, 800, 400, 48,64)
    for i = 1,12 do
        sprite(ss:slice(i), 500 + i%4*100, 400 + 100*(i//4), 96,128)
    end
    fill(100,100,100)
    ellipse(500,900,200)
    local img = sprites:sprite("green_staff")
    sprite(img, 500, 900, 96, 128)
end

This produces this lovely picture:

green staff in grey circle

So that’s working as expected. What about the test? I’ve lost interest in comparing two images for equality, but maybe it’s not too hard. What the heck, in for a penny.

        _:test("Sprites lookup", function()
            local testss = SS(sheet, 12, 1,0, 0,1)
            local names = {"crossed", "green_staff", "feather_staff", 
            "purple_staff", "snake_staff", "cudgel_staff", "skull_staff",
            "bird_staff", "jewel_staff", "knob_staff", "crystal_staff", "twist_staff"}
            _:expect(#names).is(12)
            local sp = Sprites()
            sp:add(names, sheet, 1,0, 0,1)
            local eq = areImagesEqual(sp:sprite("green_staff"), testss:slice(2))
            _:expect(eq).is(true)
        end)

Now to write the thing. Wait! A little research tells me that you can ask an image for data and you get a string that is the bytes of the image. Can we do this?

No. What comes back isn’t the actual image, it’s that string description. OK:

function areImagesEqual(i1,i2)
    local h = i1.height
    local w = i1.width
    if h ~= i2.height or w ~= i2.width then return false end
    for row = 1,h do
        for col = 1,w do
            local p1 = i1:get(col,row)
            local p2 = i2:get(col,row)
            if p1 ~= p2 then return false end
        end
    end
    return true
end

That works (and fails if I use slice(3) instead of 2). So we have a decent test of our new sprites object.

I could test further, giving it two files and so on. It might be valuable to make it refuse to define the same name more than once. I’m not inclined to do those things. Two files will “obviously” work, and if you give it the same name again, it will take it. If you want to check against that mistake, modify the object then, or protect yourself some other way.

What happens if we call it with an asset? Note that in our existing monsters, we say

hit=asset.frog_hit

Not

hit="asset.frog_hit"

But what will we say in the case of a sprite? Now that they all have unique names, we could just put in the name, as a string:

staff="green_staff"

We may want our sprite code to pass assets right on through. But I think we need to wait until we know that for sure. If I provide that capability now, it’ll match my test, and what’s in my head, but it may not match the actual code we’ll write when we use this thing. We’ll wait.

However, I’ll integrate this code into Dung now, by pasting the test and code page in as yet another tab. I’ll call the tab Sprites, I guess, though it has two classes in it.

Done, tests still run in the Dung program.

I’ve been working for about two hours, so let’s wrap this up.

Wrapping Up

I envision using the Sprites class in an initSprites function, where I’ll do as many add calls as needed to parse out the sprite sheets I need. The SpriteSheet class is essentially internal to Sprites, though it is public at present. I’ve not used the practice of making local classes, but it’s possible and easy.

It’s clear that when we call Sprites:sprite(name), we’ll get the image we want, if it’s in there. We haven’t resolved whose job it is to detect asset type things and not look them up. (I suppose we could give them names as single-cut sprites. That would have the advantage of putting all the graphical assets “together”. It would also entail editing a lot of monster tables and such. We’ll see.)

All in all, it’s going nicely. Tomorrow we’ll create something from a slice. Maybe a nice staff, or a potion or something. I’ve got a lot of art files now.

Commit: Sprites object, SpriteSheet object.

See you next time!


D2.zip

  1. I thank GeePaw Hill for this clear statement of what we do.