Dungeon 169
We need puzzles. How do we address a big problem like ‘puzzles’? Same way you eat an elephant.
OK, let me be clear, I do not recommend eating an elephant. They’re large, rare, intelligent creatures, and we should be preserving them, not eating them. Not that anyone does eat them as far as I know. Lions, maybe.
It’s a saying, OK, just a saying. “How do you eat an elephant?” “One bite at a time.”
No, really, don’t eat elephants. But do solve problems one bite at a time.
In the Zoom Ensemble last night, we were talking about getting the right handle on a problem, about taking small steps, and about the raging desire to get something done, do you hear me done!
When programming, everyone I know sometimes takes too big a bite and can’t swallow it. The wisest of us revert the bite (can’t do that with an elephant) and take a smaller bite. The less wise (raises hand) will sometimes work for hours, or days, on something that’s just too big. Sometimes after hours or days, we make it work. Sometimes we finally give up and revert. Sometimes we ship something that doesn’t quite work, and never really will.
When we’re in the first part of too big a bite, it can seem like we’re really flying along. Wow, nearly done already. Then we bog down, trying to connect those two ends that won’t quite reach, and we’re in the second 90 percent of the time to do the thing, and we go slower and slower in terms of percent done, but we’re so close that we keep on keeping on.
In the end, if we make it work at all, we’ll be far slower. It will have taken far longer than we thought. But, we tell ourselves, it was a hard problem. Only a powerful wizard programmer like myself could done it at all. I powered through, and now, here it is, almost completely working. Yay, me. There’s just this one bug …
What’s Wrong With a Big Bite?
Well, the bottom line what’s wrong is that when it doesn’t work, we suddenly spend three or five times as long as we expected, and we often only get to 90% or less of what we thought we’d get, and later, we have code that is hard to change, and hard to work with.
But why does that happen? Is there some general rule, some law of nature, that makes it turn out that way?
A few days ago, on Twitter someone asked that people solve a simple arithmetic problem and send them a tweet saying what they thought as they solved it. The problem was, if I recall,
27 + 48
A lot of folks answered something like “7 plus 8 is 15, carry the 1, 2+4 is 6, plus 1 is 7, answer is 75”.
I answered “move 2 from 27 to 48, get 25 + 50: 75”.
Now there’s a dirty secret behind why I did that: I’m terrible at arithmetic. Yes, I know I have two degrees in math and one in computer science. I’m terrible at arithmetic, not math or programming. I hate rote practice, II never practiced arithmetic, and so I know lots of tricks to make problems easier to solve. I am also a bloody genius at calculating on my fingers.
However, there’s more of a moral to the story than “learn your times tables”.
In the example above, there are two ways the problem is solved. One is the official, here’s how you do it, add the columns from left to right, carrying anything past the first digit to the next column. That always works, if you can add.
The second solution doesn’t always work. It depends on the numbers. This problem happened to yield to moving two from one side to the other, resulting in
25
+50
And even I can solve that one.
What does this have to do with an elephant? Just this:
If we have a big problem, its solution will involve some number of interacting ideas, or objects or functions–whatever the solution is made of. The complexity of solutions increases dramatically, the more elements there are to it. One element, easy. Two, still easy, there’s only two says two things can go together, AB or BA. Three? Uh oh, there’s ABC ACB BAC BCA CAB CBA. Six? There are 720 orders for 6 letters. And code isn’t just pasting letters together.
But, pushing the analogy just a bit further, if we can peel off a part of out six-element problem and do two elements, we wind up with a problem of difficulty 2, and a problem of difficulty 24. Peel another two off the 24 side and we have problems of difficulty 2, 2, and 2.
I’m nearly done with this reasoning …
So, let’s try to bring it home. If we can break a large problem down into smaller problems that are (mostly) independent, we can reduce the total complexity of our solution to the big problem dramatically.
Dramatically. Really.
Suppose we have a problem that we can break down into five pieces of size 2. The total work will be of difficulty 10. If we try to solve the thing all at once, will it still be difficulty 10? No! I would argue that it’ll be much larger. Maybe 2 times 5 factorial, or 240. Maybe even 10 factorial, or 3,628,800. Unlikely? I don’t know, I have in my life tried to write things and utterly failed. Maybe they were difficulty 3 million. That makes me feel a bit better.
Alan Kay said “Point of view is worth 80 IQ points”. Same point as we have here, just a different angle.
If we can partition our problem into small pieces, it simplifies all our work. There is no silver bullet, but breaking big problems down into small problems is unquestionably a silver-plated bullet.
How Does This Apply to Puzzles?
Puzzles? Not just puzzles. It applies to everything I try to do. If I can’t make something simple enough to tear off a little piece and do it, I’m almost certain to fail.
So I have in mind a vague idea of a complicated sort of puzzle in the dungeon. I don’t know the details, just an idea like this:
There are some tiles in our tile set that are like a floor panel with a button that can be up or down. They could serve as difficult-to-find switches. And we have some items that actually look like switches, with a few different positions.
Use these tiles and switches to create puzzles. Perhaps there is a gap full of water that has to be crossed, and the right combination of button tiles in another room, causes the gap to be covered. Perhaps there is a monster cage, and if you throw the switch while the monster is in the cage, you can trap it, protecting yourself. Perhaps there is even a way to lure him in there.
In my free time, maybe I’ll draw up some diagrams of puzzles like these. It seems to me that they’re going to involve some intricate logic, especially since they aren’t even well-defined. But today, I propose to figure out a simple way to begin on puzzles. And even though it’s 0925, and I started at 0825, I propose to finish the first puzzle feature today, or know the reason why I couldn’t.
And I really don’t know what the puzzle is, or how to build it. I do have one idea to start with.
Shouting
Puzzles will involve objects in the dungeon interacting at a distance. The cage or water trap is in one room, the buttons or switches are in another room, or even several rooms. Yet they must interact. How?
The buttons and switches will shout their status!
We have the relatively new EventBus object. Any object can publish events to the bus, and any other object can subscribe to whatever messages interest it. So if our buttons and switches have some kind of identifier, like a name “button 16”, and our water traps and cages listen for “button 16 up” and “button 16 down”, or something like that, we can make anything happen.
So the slice of the puzzle problem for the day is:
- Create a switch that announces its name and position on the EventBus.
- Use that switch to make something interesting happen. Extra credit if it reminds you of a puzzle.
One initial question is whether the switch is an instance of Decor, or an object of its own. I think it should be an object of its own, because it has state and behavior that’s far different from decor. If it overlaps with some other object, we can merge them as we see fit.
So let’s create a switch class:
-- Switch
-- RJ 20210508
Switch = class()
function Switch:init()
end
function Switch:draw()
end
function Switch:touched(touch)
-- Codea does not automatically call this method
end
I left the touched
function in, because it occurs to me that it might be interesting if the player could touch the switch to change it. I don’t think we’ll do that but I left it as a reminder of the possibility.
A switch is like a Chest or a Loot or a Decor. How do those get created? Here’s Loot:
Loot = class()
local LootIcons = {Strength="blue_pack", Health="red_vial", Speed="green_flask",
Pathfinder="blue_jar", Antidote="red_vase"}
function Loot:init(tile, kind, min, max)
self.tile = nil
self.kind = kind
self.icon = self:getIcon(self.kind)
self.min = min
self.max = max
if tile then tile:moveObject(self) end
end
The LootIcons there at the top give me an idea for how we’ll deal with the switch’s positions and images, something like the Loot does, a little table. That sends me on a mission to get the images. I’m not sure if I’ve moved them in or not.
Turns out that I haven’t. Here’s my process:
Type a sprite(xxx) command into Switch:draw
. That lets me open the asset finding window. I am disappointed to find that I’ve not moved in those items from the tile set in Files. Do that:
Find them in Files, select them, tap move, move to Codea’s “Dropbox.assets”, the intermediate folder we can reach.
Then in Codea’s asset window, refresh(!) and move the items into our project.
I notice that the pictures are called “Lever”, and I’m not hung up on the name, so I decide to rename the tab and Class to Lever. (I do that by pasting it over to Sublime and using Sublime’s marvelous multi-cursor select to type Lever once into five locations.)
OK, cool. Now the sprites, and II get to this:
-- Lever
-- RJ 20210508
Lever = class()
local LeverSprites = {asset.lever_1, asset.lever_2, asset.lever_3, asset.lever_4 }
function Lever:init(tile,name)
self.name = name
self.tile = nil
tile:moveObject(self)
end
function Lever:draw()
sprite(LeverSprites[1])
end
function Lever:touched(touch)
-- Codea does not automatically call this method
end
I think this might be enough to draw one, if I had one. Let’s put one in with the princess in Room 1. I think we have a way to do that.
A bit of programming gives me this:
function Lever:draw(tiny, center)
pushStyle()
spriteMode(CENTER)
sprite(Sprites:sprite(LeverSprites[1]),center.x,center.y+10)
popStyle()
end
And a forced placement of the lever in GameRunner:
function GameRunner:placeLever()
local r1 = self.rooms[1]
local tile = r1:centerTile()
local leverTile = tile:getNeighbor(vec2(3,2))
local lever = Lever(leverTile, "Lever 1")
end
And a somewhat surprising result:
I have to admit, that made me laugh. Yes, well, it is 414x290, a bit large for our taste. A bit of code reading and I’m reminded of the AdjustedSprite object, that knows how to draw itself at a smaller scale.
Let’s try that:
local sc = vec2(64/256,64/356)
local LeverSprites = {AdjustedSprite(asset.lever_1,sc),
AdjustedSprite(asset.lever_2,sc),
AdjustedSprite(asset.lever_3,sc),
AdjustedSprite(asset.lever_4,sc) }
This will be almost two cells wide, but it should be a decent start.
I have to change the draw to use the AdjustedSprite. They aren’t well integrated with my other sprite logic, I note. Make a note.
function Lever:draw(tiny, center)
pushStyle()
spriteMode(CENTER)
translate(center.x,center.y)
LeverSprites[1]:draw()
popStyle()
end
Now to allow interaction. Let’s begin by giving our Lever some state:
function Lever:init(tile,name)
print(tile,name)
self.name = name
self.tile = nil
tile:moveObject(self)
self.position = 1
end
function Lever:draw(tiny, center)
pushStyle()
spriteMode(CENTER)
translate(center.x,center.y)
LeverSprites[self.position]:draw()
popStyle()
end
And a way to change it:
function Lever:nextPosition()
self.position = self.position + 1
if self.position > 4 then self.position = 1 end
end
I’d rather do this with mod, but we’re on a mission right now.
Now we need a TileArbiter entry for lever.
t[Lever] = {}
t[Lever][Player] = {action=Player.startActionWithLever,
moveTo=TileArbiter.refuseMove}
And in player:
function Player:startActionWithLever(aLever)
aLever:nextPosition()
end
The switch moves!
Let’s make it report where it is. We could conceivably TDD this but it’s trivial so I’m going to risk it.
function Lever:nextPosition()
self.position = self.position + 1
if self.position > 4 then self.position = 1 end
Bus:publish("Lever", self, {name=self.name, position=self.position})
end
I’v decided that the message should just be “Lever” and the name and position passed in the optional table.
Now we need something in the dungeon to respond to our lever. What would be easy?
Let’s change the spike oscillation frequency depending on lever position. In Spikes:
function Spikes:init(tile, tweenDelay)
self.delay = tweenDelay or tween.delay
tile:moveObject(self)
self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}
self.assetTable = { down=asset.trap_down, up=asset.trap_up }
self.verbs = {down="jab ", up="impale " }
self:up()
self.stayDown = false
self:toggleUpDown()
Bus:subscribe(self, leverHandler, "Lever")
end
function Spikes:leverHandler(event, sender, info)
local pos = info.position or 1
if pos == 4 then
self.stayDown = true
end
end
function Spikes:toggleUpDown()
self.state = ({up="down", down="up"})[self.state]
if self.stayDown then self.state = "down" end
if self.state == "up" then
local player = self.tile:getPlayerIfPresent()
if player then self:actionWith(player) end
end
self.delay(2, self.toggleUpDown, self)
end
This is moderately grotesque hackery but it should mean that if I set the lever position to 4, all the spikes will stay in the lowered position.
After some delay …
I hope I said up there somewhere that I’d get this done or “know the reason why”. Somehow, I’m not getting the “Lever” event in the Spikes objects. I’ve printed them subscribing, but then when the event is published, which I can also see, there are no Spikes in the event table any more. Weird.
Ah. I finally find this typo:
function Spikes:init(tile, tweenDelay)
self.delay = tweenDelay or tween.delay
tile:moveObject(self)
self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}
self.assetTable = { down=asset.trap_down, up=asset.trap_up }
self.verbs = {down="jab ", up="impale " }
self:up()
self.stayDown = false
self:toggleUpDown()
Bus:subscribe(self, leverHandler, "Lever")
Bus:dump()
end
Should be:
function Spikes:init(tile, tweenDelay)
self.delay = tweenDelay or tween.delay
tile:moveObject(self)
self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}
self.assetTable = { down=asset.trap_down, up=asset.trap_up }
self.verbs = {down="jab ", up="impale " }
self:up()
self.stayDown = false
self:toggleUpDown()
Bus:subscribe(self, self.leverHandler, "Lever")
Bus:dump()
end
This error would have been caught by a run-time check for creation of a new global. I really want to enable that feature as soon as I’m sure Codea won’t crash because of it.
Anyway, now the spikes stay down with the switch in position 4:
We have a feature. We also have a test failing. Let’s see why.
1: Spikes -- Spikes:57: attempt to index a nil value (global 'Bus')
Ah. That’ll be us, trying to catch a bus when they aren’t running.
function Spikes:init(tile, tweenDelay)
self.delay = tweenDelay or tween.delay
tile:moveObject(self)
self.damageTable = { down={lo=1,hi=1}, up={lo=4,hi=7}}
self.assetTable = { down=asset.trap_down, up=asset.trap_up }
self.verbs = {down="jab ", up="impale " }
self:up()
self.stayDown = false
self:toggleUpDown()
if Bus then
Bus:subscribe(self, self.leverHandler, "Lever")
end
end
Also this:
13: monster can't enter chest tile -- AnimatorAnimation:127: need hit animation
That test is in TiledGame:
_:test("TileArbiter: player and monster cannot step on chest", function()
local runner = Runner
local room = Room(1,1,20,20, runner)
local pt = Tile:room(11,10,runner)
local player = Player(pt,runner)
runner.player = player
local ct = Tile:room(10,10, runner)
local chest = Chest(ct,runner)
local arb = TileArbiter(chest, player)
local moveTo = arb:moveTo()
_:expect(moveTo).is(pt)
local mt = Tile:room(9,10,runner)
local monster = Monster(mt,runner)
arb = TileArbiter(chest,monster)
moveTo = arb:moveTo()
_:expect(moveTo).is(mt)
end)
I feel this must have been failing for a long time, and that I’ve mistaken it for an intermittent failure. Where does “need hit animation” come from, and why?
function Animator:init(mtEntry)
self.animations = {}
self.animations.dead = mtEntry.dead or error("need dead animation")
self.animations.hit = mtEntry.hit or error("need hit animation")
self.animations.moving = mtEntry.moving or error("need moving animation")
self.animations.attack = mtEntry.attack or mtEntry.moving
self.animations.hide = mtEntry.hide or mtEntry.moving
self.animations.idle = mtEntry.idle or mtEntry.moving
self.animationTimes = mtEntry.animationTimes or { 0.23, 0.27 }
self.animation = Animation()
end
Is there an mtEntry without hit? There is one, the PathFinder. Perhaps the PathFinder somehow came up in this test? I don’t know. Giving PathFinder a hit animation makes the test green. This adds a tick mark to my yellow sticky note about validating the monster table.
Commit: switch in room 1, in position 4, sets all spikes to downward position.
Let’s sum up. I’m not an entirely happy camper.
Imperfection is My Metier
It’s 1208. I started at 0825. The topic for the day was picking tiny slices of our larger problems, and getting them done.
Yet it has taken me almost four hours to get a switch into the system and make it adjust how spikes work.
I did fall into some rat holes and I did shave a couple of yaks.
- I had to hunt down and move some art.
- It took me three tries to get the art displaying at a reasonable size.
- AdjustedSprite seems not well integrated with the other sprite code, or I don’t know how to use it.
- It took me a while to think of a decent way to place a switch in the player’s initial room, not because it wasn’t easy, but because I thought there was a more general way to do it so I spent time rummaging. And the level creation code is a jungle.
- Wiring up the Spikes was easy, except that I passed in a nil method, and therefore wasn’t subscribing to anything. I looked in all the wrong places to find that problem until I finally realized the method could be nil. This could have been detected by a check for surprise global references.
- A weird test failure had me looking in the wrong place for a while. I’m still not sure what caused it, but the change I made fixed it. The problem was in an area that I know needs work, monster table validation.
There are at least four lessons here.
Split, slash, slice, cut
The first one, I think, is that spitting things down to incredibly simple is really important. I can probably think of something even simpler than what I did this morning, but this morning’s story was pretty simple and it still took twice the time I thought it would.
Clean up your mess
The second lesson is that there are continuing issues that rise up to bite me in the tail, including the lack of validation on complex tables, and spurious access to undefined globals. These are both capable of being addressed, but it always feels useless to work on them. And then they cost me a half hour or hour.
Slow tests
The third lesson is that my tests run too long, so I had them turned off a long time, so they have not evolved to keep up, and they still run too long.
Focused tests
The fourth lesson is that the tests run too long because they are not micro-tests at all, but integration kinds of tests. Look at the one from today:
_:test("TileArbiter: player and monster cannot step on chest", function()
local runner = Runner
local room = Room(1,1,20,20, runner)
local pt = Tile:room(11,10,runner)
local player = Player(pt,runner)
runner.player = player
local ct = Tile:room(10,10, runner)
local chest = Chest(ct,runner)
local arb = TileArbiter(chest, player)
local moveTo = arb:moveTo()
_:expect(moveTo).is(pt)
local mt = Tile:room(9,10,runner)
local monster = Monster(mt,runner)
arb = TileArbiter(chest,monster)
moveTo = arb:moveTo()
_:expect(moveTo).is(mt)
end)
Particularly curious is that we have other tests addressing this same question:
_:test("monster can't enter chest tile", function()
local runner = Runner
local playerTile = Tile:room(15,15,runner)
local player = Player(playerTile,runner)
runner.player = player -- needed because of monster decisions
local chestTile = Tile:room(10,10,runner)
local chest = Chest(chestTile, runner)
local monsterTile = Tile:room(11,10,runner)
local monster = Monster(monsterTile,runner)
local chosenTile = monsterTile:validateMoveTo(monster, chestTile)
_:expect(chosenTile).is(monsterTile)
end)
_:test("player can't enter chest tile", function()
local runner = Runner
local chestTile = Tile:room(10,10,runner)
local chest = Chest(chestTile, runner)
local playerTile = Tile:room(11,10,runner)
local player = Player(playerTile,runner)
_:expect(chest:isOpen()).is(false)
local chosenTile = playerTile:validateMoveTo(player, chestTile)
_:expect(chosenTile).is(playerTile)
_:expect(chest:isOpen()).is(true)
end)
These tests seem to be checking the same thing. And they are costly to run because this set of tests sets up a GameRunner:
_:before(function()
_TileLock = TileLock
TileLock = false
_tweenDelay = tween.delay
tween.delay = fakeTweenDelay
_Runner = Runner
_bus = Bus
Bus = EventBus()
Runner = GameRunner()
Runner:createTiles()
end)
What we see here is tests that have not been maintained, and that were too large to begin with, and that show us that our objects are still too tightly tied together.
I’ve been leaving dirty dishes in the sink, and they are slowing down production of dinner.
The code written today? Less than 100 lines of code, I’d estimate, and mostly easy lines. But I was slowed down by things almost all of which could be avoided, or limited, by being a bit more careful about my work.
The lesson for me is to keep on trying, and to dedicate a bit of time to keeping things clean, and cleaning up things that aren’t clean enough.
And in that light, we have left a bit of a mess in the Spikes, but we’ll come up with a better story now that we have them responding to our Lever.
So. A decent morning, but not a great morning.
See you next time!