Maybe some wind for my sails. A need for better notes. An idea from Bryan’s experiment with Unity. A thought about apps without programming. Everything is hopeless. Not necessarily in that order.

Wind. For my sails.

Last night’s Zoom Ensemble1 gave me a bit more energy around working on the dungeon program. As much as I am OK with have a pandemic-style non-social life, and I am very much OK with it, I will freely say that a chat session with colleagues and pals can give me a jolt of energy for diving back in and doing this thing that I do.

Notes. For remembering and planning.

This morning, as I was thinking about the Idea From Bryan, I realized that there are some other almost done features of the Dungeon, that are falling through the cracks. One that came to mind was the “History” or “Repeat That” button that triggers a replay of the important things that have been in the crawl. Right now, the ?? button plays a dual role of repeating that material and inspecting objects near you.

I do have this stack of sticky notes, right here, well organized as you can see, containing all the notes that I’ve written down, except for the ones that I’ve thrown away or lost.

sticky

As you may suspect from looking at the pile, er file there, I don’t spend a lot of time organizing the stickies. When I plan, I often go through them and either just pick something to do, or discuss items with you. Often, I do things that are in the pile, not because they’re in there and I found them, but because I remember, or run across the code, and just do the thing. The sticky remains in the pile until I discover it, and remember that it’s done. More than once, I’ve picked one up and gone to do it only to discover that someone already had done it. That would be less weird if there was anyone but me working on this thing.

I like stickies when there are just a few: they don’t blow away when the cat goes by. But when there get to be a lot, cards might be easier to work with. I have cards, maybe I’ll try it.

Pretty sure I won’t start using Jira.

Apps. Without programming.

Bryan Beecham, of Zoom Ensemble and other fame, is experimenting with Unity for his dungeon program, so we looked at some of the code and talked about his experience. Mostly his experience upgrading something in Unity which then broke his entire program. The update is new and even Stack Overflow wasn’t much help.

We also talked about the tools and widgetry in Unity and how much of the game could be created, not by programming, but by configuring various items, selecting avatars and editing them graphically, drawing collision squares around things in the dungeon, attaching items to other items, typing in textual scripts for conversations with NPCs, and so on.

I don’t have many of those tools in Codea, but if I were trying to do a real game with many levels and activities, I would probably have to write or acquire tools like that, so that an army of artists could work creating the many levels and artifacts of the game. Building the game would turn from a programming exercise into a sort of building with Lego activity. And, in many ways that’s a good thing, even though it isn’t my thing.

Hopeless. Or is it?

Our conversation drifted, inevitably, to the plight of programming and programmers today. One of the Ensemble members is working with a team who have an application that uses a large number of distributed microservices, containers, and all that jazz. We think the entire customer base could run on a Fitbit. But the team has been pushed into, and to a degree willingly jumped into, today’s “way we do things”.

And, often, out in the wild, young programmers don’t know better, and they don’t know enough good programming to know what to do even when they feel the pain.

And we don’t know how to help the millions of programmers who need help.

We mused that people learn the things we know–and the things that our betters know and we still do not–one person, one thing at a time. Yes, if you have a group of people for a week or a month or two, several of them may go away with enough enlightenment that they’ll keep working and join us on our path up and around the mountain. But even those several were really reached one at a time over the course of your time together.

That means that it is a long and slow-growing process to spread the good news. However, it just might be non-linearly good. If I can reach enough people so that just two of them take ideas forward, take learning forward, and take telling others forward, I’ve just tripled my effectiveness while I live and doubled it even afterward.

So we keep trying, and we try to remember that our problem isn’t five or twenty-five programmers, it’s one. One at a time.

Idea. From Bryan.

Bryan told us of this cool thing that is part of Unity, or the 2d dungeon part of Unity, or, I don’t know, I’m not a Unity programmer, this cool thing. He put it this way:

You can attach a quest to objects. Like there are these apples, and you can attach the quest to them, and when you collect them, they accumulate in your inventory, and there can be some guy in the game and he asks you to get him some apples and when you do, he gives you something.

Well, Bryan was more clear than that. That’s my excited version of thinking about a Quest object.

I’ve not really done any work on quests yet, in part because I haven’t thought of one. (Don’t be surprised if the Princess needs to find, oh I don’t know, some peaches to give to the Jam Lady.) But I was also held back because while my objects can communicate with each other via the EventBus, my design mostly considers all the dungeon contents to be independent “active” entities. So it isn’t clear, in the current design, where you’d put something like the relationship between peaches in the dungeon, peaches in your inventory, and the Jam Lady.

The quest object! What a neat idea. Create a Quest, associate some Peach (Loot) with it, associate the Jam Lady with it, and give it a simple rule ability like if inventory > 5 then Jam Lady gives jam. I don’t know yet what the Quest object would be like, nor do I even want to. I think just the idea allows me to create the first quest, discover what I need, and evolve a Quest class, or a handful of quest-related classes, to serve this new need.

And that is part of why I’ve got wind back in my sails: an idea for something to build that will actually be interesting.

I think I’ll likely start on that on Monday. Today, let’s just do something small.

How about that History Button?

Yeah, how about that? How does that work now?

The ?? button’s working name is “query”. All the buttons work the same way: they send their name, as a message, to the player. We have:

function Player:query()
    self:inform(self.tile:nearestContents():query())
    Announcer:playCache()
end

What we need is two buttons, one for the inform and one for the Announcer:play. Then, the way things work now, we’ll need a method on Player to do the work. Arguably our buttons could to more work but that’s not for today.

Buttons are laid out like this:

buttons

Easy enough to make a new button. But the word History isn’t going to fit in there, is it. Let’s see:

function GameRunner:createButtons()
    self.buttons = {}
    table.insert(self.buttons, Button("left",100,200, 64,64, asset.builtin.UI.Blue_Slider_Left))
    table.insert(self.buttons, Button("up",200,250, 64,64, asset.builtin.UI.Blue_Slider_Up))
    table.insert(self.buttons, Button("right",300,200, 64,64, asset.builtin.UI.Blue_Slider_Right))
    table.insert(self.buttons, Button("down",200,150, 64,64, asset.builtin.UI.Blue_Slider_Down))
    self:createCombatButtons(self.buttons)
end

function GameRunner:createCombatButtons(buttons)
    table.insert(buttons, Button:textButton("??", 100,350))
    table.insert(buttons, Button:textButton("Fight",200,350))
    table.insert(buttons, Button:textButton("Learn", 300, 350))
end

Those aren’t really combat buttons any more. Also Fight does nothing. Let’s just use that one.

Trying the word History in there shows me that either the type is too large or the button is too small. For now, let’s change the type.

hist1

function Button:createTextImage(label, w, h)
    local img = image(w,h)
    setContext(img)
    fill(109, 206, 227)
    rectMode(CORNER)
    rect(0,0,64,64)
    fill(0)
    textMode(CENTER)
    fontSize(20)
    text(label, 32,32)
    setContext()
    return img
end

Size 18 works OK:

button18

Now for the method:

function Player:query()
    self:inform(self.tile:nearestContents():query())
end

function Player:history()
    Announcer:playCache()
end

This works exactly as anticipated. Commit: new history button replays announcer cache. ?? does not.

I noticed that the ?? button has no effect when you are near the Switch. Or Lever. Or whatever it’s called.

function GameRunner:placeLever()
    local r1 = self.rooms[1]
    local tile = r1:centerTile()
    local leverTile = tile:getNeighbor(vec2(0,2))
    local lever = Lever(leverTile, "Lever 1")
    local annTile = tile:getNeighbor(vec2(0,-2))
    local ann = Announcer({"This is the mysterious level called", "Soft Wind Over Beijing,",
    "where nothing bad will happen to you", "unless you happen to be a princess.", "Welcome!" })
    annTile:addDeferredContents(ann)
    annTile = tile:getNeighbor(vec2(1,-2))
    annTile:addDeferredContents(ann)
    annTile = tile:getNeighbor(vec2(-1,-2))
    annTile:addDeferredContents(ann)
end

There are three secret tiles, two below the princess’s starting position, that make that announcement. No real reason to retain it, but we’ll let it be for now. We’re here for the Lever.

I believe that if we give the Lever a query method that returns a string, it should just work.

function Lever:query()
    return "At first glance, this appears to be a lever. Do you think it does anything?"
end

lever

So, that works. Commit: Lever now has a query message.

That message is rather long and takes up a lot of screen space. Someone playing in portrait mode might now have that space, if we supported portrait mode. There are a couple of other long messages. We could put in a newline, but that looks funny on the screen: the spacing between newline lines and crawl lines is different.

Isn’t there a feature to split new-lined messages up? I vaguely recall that … well, how about that?

function Player:inform(message)
    if type(message) == "string" then
        message = splitStringToTable(message)
    end
    for i,msg in ipairs(message) do
        Bus:publish("addTextToCrawl", self, {text=msg})
    end
end

We can readily stuff a newline in there and be sure it’ll be handled suitably.

two lines

Commit: Lever has two-line message.

I’ve noticed that sometimes when I stand next to something and hit ??, I’m actually standing near more than one thing, and the message I get isn’t the one I’m expecting. We might want to display all the items in the immediate vicinity.

Let’s see how that works:

function Player:query()
    self:inform(self.tile:nearestContents():query())
end

function Tile:nearestContents()
    return self.runner:getDungeon():nearestContents(self)
end

function Dungeon:nearestContents(tile)
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then return obj end
        end
    end
    return NullQueryObject()
end

Well, we have just learned that inform will be happy to have an array of messages. But our player query method is assuming just one. The nearestContents in dungeon is odd, because it only returns queriable contents, and just the first one. Not the best name.

I’m pretty sure no one else is using this nearestContents method: it’s pretty specialized. That’s correct, though there is a test we’ll need to modify, depending how things go here.

Let’s rename the Dungeon method and make it return a table of query results:

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then
                local msg = obj:query()
                if type(msg) ~= "table" then msg = { msg} end
                for i,m in msg do
                    table.insert(msgs,m)
                end
            end
        end
    end
    return msgs
end

This now returns a table, possibly empty, of messages. Some of the messages may have newlines in them: we’d best not forget that.

Now change the Tile message to use this:

function Tile:nearestContentsQueries()
    return self.runner:getDungeon():nearestContentsQueries(self)
end

And Player to use it:

function Player:query()
    for i,m in self.tile:nearestContentsQueries() do
        self:inform(m)
    end
end

I have a test that will fail but I’m thinking the code works in game. Let’s find out why I’m wrong.

Dungeon:306: attempt to call a table value
stack traceback:
	Dungeon:306: in function <Dungeon:297>
	(...tail calls...)
	Player:233: in field '?'
	Button:59: in method 'performCommand'
	Button:54: in method 'touched'
	GameRunner:572: in method 'touched'
	Main:96: in function 'touched'

That’s not entirely informative, is it? What’s Dungeon:306?

                for i,m in msg do

Forgot some ipairs calls, innit? With those fixed, it works as programmed:

two queries

I think I’d like to have a blank line in between the separate messages.

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then
                local msg = obj:query()
                if type(msg) ~= "table" then msg = { msg} end
                for i,m in ipairs(msg) do
                    table.insert(msgs,m)
                end
                table.insert(msgs,"...")
            end
        end
    end
    return msgs
end

Not quite. Don’t want the last one. Let’s put the … ahead of the 2nd and following messages.

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            local q = obj.query
            if q then
                if #msgs > 0 then
                    table.insert(msgs,"...")
                end
                local msg = obj:query()
                if type(msg) ~= "table" then msg = { msg} end
                for i,m in ipairs(msg) do
                    table.insert(msgs,m)
                end
            end
        end
    end
    return msgs
end

queries ok

OK, that’s as intended. Commit: query reports on all adjacent tiles.

That method, though. Kind of busy. We can in-line one temp:

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            if obj.query then
                if #msgs > 0 then
                    table.insert(msgs,"...")
                end
                local msg = obj:query()
                if type(msg) ~= "table" then msg = { msg} end
                for i,m in ipairs(msg) do
                    table.insert(msgs,m)
                end
            end
        end
    end
    return msgs
end

I don’t have a lot of fancy table methods to help me, but we could do something like this:

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        for k,obj in pairs(tile:getContents()) do
            if obj.query then
                if #msgs > 0 then
                    table.insert(msgs,"...")
                end
                self:addQueryToTable(obj,tab)
            end
        end
    end
    return msgs
end

function Dungeon:addQueryToTable(obj, tab)
    local msg = obj:query()
    if type(msg) ~= "table" then msg = { msg} end
    for i,m in ipairs(msg) do
        table.insert(msgs,m)
    end
end

And extract again, and make sure the parameter names are correct (if not good).

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        self:addTileQueriesToTable(tile,msgs)
    end
    return msgs
end

function Dungeon:addTileQueriesToTable(tile,msgs)
    for k,obj in pairs(tile:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            self:addQueryToTable(obj,msgs)
        end
    end
end

function Dungeon:addQueryToTable(obj, msgs)
    local msg = obj:query()
    if type(msg) ~= "table" then msg = { msg} end
    for i,m in ipairs(msg) do
        table.insert(msgs,m)
    end
end

That looks a bit better. Now where do these methods actually belong? addTileQueriesToTable looks only to the tile and a provided table. So it belongs on Tile. The other method only looks to the tile contents and the table, so it belongs on tile contents items. However, if we put it there, we will have to push it to each of the objects that can be tile contents.

Those objects do all know query but that doesn’t make me want to add this method multiple times. If they all inherited from a gods save us concrete superclass, we could have the code where it belongs without duplication.

I’d like to have query be required always to provide a table, since a table is optional. No! I am mistaken. A dungeon object can only return a string to query. We can simplify this code.

function Dungeon:addQueryToTable(obj, msgs)
    table.insert(msgs,obj:query())
end

In-line that back.

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        self:addTileQueriesToTable(tile,msgs)
    end
    return msgs
end

function Dungeon:addTileQueriesToTable(tile,msgs)
    for k,obj in pairs(tile:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            table.insert(msgs,obj:query())
        end
    end
end

Now that second method can be moved to tile.

function Tile:addTileQueriesToTable(msgs)
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            table.insert(msgs,obj:query())
        end
    end
end

And it can be renamed:

function Dungeon:nearestContentsQueries(tile)
    local msgs = {}
    local neighbors = self:neighbors(tile)
    for i,tile in ipairs(neighbors) do
        tile:addQueriesToTable(msgs)
    end
    return msgs
end

function Tile:addQueriesToTable(msgs)
    for k,obj in pairs(self:getContents()) do
        if obj.query then
            if #msgs > 0 then
                table.insert(msgs,"...")
            end
            table.insert(msgs,obj:query())
        end
    end
end

Works perfectly. I’m not entirely satisfied but there’s a break in the refactoring rhythm here so commit: Refactor nearest content queries.

I’d really like to be able to forward Tile’s method on down to each object, but without them all inheriting from DungeonObject or something, there’s not much to do.

Lua doesn’t have the notion of an “interface”, so I can’t declare for the classes what methods they must have. It would be possible to do that, but if one were to forget to declare a dungeon object to use require the interface, no one would know.

This may be something to work on. I’ll make a sticky … ok, I’ll make a card for it.

Great, now I have a stack of stickies with one card in the stack. Much improved.

Returning to the Idea Inspired by Bryan …

I think Bryan said that all the dungeon objects in Unity share a lot of behavior and such in common. That makes it easy to connect dialog or behavior to just anything in the dungeon.

We don’t have quite that much commonality. So far, in the Dungeon, we have at least these separate objects:

Loot, Chest, Key, WayDown, Spikes, and Decor. Oh, and Lever.

Let’s do a little analysis here. Just eyeballing, all those classes have in common:

  • init
  • draw
  • getTile
  • query
  • setTile

Most have actionWith(player). Most have a way of having an effect on the player, including take, giveItem, or open.

That’s just scanning the dump of classes and methods. There’s clearly some room for consolidation of some kind. It could be a superclass. Or perhaps there’s a generic DungeonObject that holds certain details.

It seems to me that if we do that, then we’ll just again impose the requirement for each of those classes to implement whatever protocol we use to get their query or contents or whatever. Possibly that could be boiled down to one or two methods, which might not be too bad.

If we think of making things more data-driven, we might just have a single associated associative table held by the DungeonObject, in which it would look up, say “query”, and if it didn’t find it, perform the default action or display the default message or whatever.

It might be a worthy thing to try. I already know that I don’t mind the concreted methods in superclass pattern, even if some of my colleagues would drum me out of the corps for it. So doing it another way, namely with composition, would be a good learning experience for us all. Or at least for me.

That’s for another day. Today, we’ve done some good. Let’s back away slowly before we break something.

Oh, like that test. We have to make that work.

5: neighbors -- Dungeon:85: attempt to call a nil value (method 'nearestContents')
        _:test("neighbors", function()
            local tile = Tile:room(10,10,Runner)
            local t9 = dungeon:getTile(vec2(9,9))
            local decor = Decor(t9, "fake item")
            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)

We don’t have that method at all now. We have nearestContentsQueries, which returns an array of messages. We can use that:

Ah, no, wait? This method expects to find an item inside the decor using the neighbor logic. This is no longer applicable: we don’t use a nearest contents method to do that. I think we’ll drop the expects from the end of this.

        _:test("neighbors", function()
            local tile = Tile:room(10,10,Runner)
            local t9 = dungeon:getTile(vec2(9,9))
            local decor = Decor(t9, "fake item")
            dungeon = Runner:getDungeon()
            local neighbors = dungeon:neighbors(tile)
            _:expect(#neighbors).is(8)
            _:expect(neighbors, "same t9").has(t9)
        end)

The test wasn’t very robust to begin with, and now it is less so, but it does check the neighbors function a bit. We’ll let it live.

Commit: simplified test “neighbors”.

Let’s quickly sum up and get outa here.

Summary

We started with some general musings, on topics that I feel somewhat deeply about. Lessons, for me, may include:

  • Even as solitary as I am, comrades are comforting and helpful.
  • Energy in my work can come from ideas generated in random conversations.
  • As we try to change the world, we need to realize that change generally comes one person at a time. Try to help a few, and be satisfied if you manage that.
  • Small steps still work fine.
  • Refactoring leads to better design.
  • We need some peaches up in this thing.

See you next time!


D2.zip


  1. Boy’s Night Out on Zoom. Every Friday until it isn’t.