Dungeon 272
Back to tiny steps, if I possibly can. And you know what? I bet I can.
Yesterday I spent a long time between commits. Things were always under control, with brief moments of concern, but had I run out of time, I’d have had to revert my work, or stash it, or one of those Git things, because I had intentionally broken the program and was ticking through the steps to fix it.
Hm, FTP to my site is failing with error 203. Help request sent. I hope that one day you may see this article.
Anyway, what we accomplished yesterday was to build a new object, BuilderView, that is a Façade for Dungeon, providing just the minimal functions that DungeonBuilder needs to access in Dungeon. My intention was, and is, to reduce the complexity of the system, making these builder changes easier.
Looking forward, I recall that there are a lot of building calls made back to GameRunner, and, in addition, there are a large number of calls made from things in the Dungeon back to Runner, which may suggest making additional views of GameRunner. For folks using languages with the Interface notion, these façades work as Interfaces, at the cost of an additional message dispatch, and with the possible advantage of a bit of transformation in the façade, in the manner of an Adapter pattern. I don’t think we’ve done that yet but it was a long time ago, yesterday, so I may have forgotten.
Anyway, let’s get back to moving things into Builder from Dungeon.
Moving Little Bits
The way I find things to move is simple. I look in DungeonBuilder for self.dungeon
When I find one of those, it’s an opportunity to move something.
function DungeonBuilder:buildLevel(roomCount)
self:dcPrepare()
self:defineDungeonLayout(roomCount)
self:placePlayer()
self:customizeContents()
self:dcFinalize()
return self.dungeon:asFullDungeon()
end
This one is legit. We put it in yesterday: when we return a constructed Dungeon, the caller wants the full interface. That call transforms our Façade back to a Dungeon. That occurs three times, for the three different dungeon builds we can perform.
function DungeonBuilder:clearLevel()
for key, tile in self.dungeon:map():pairs() do
tile:convertToEdge()
end
end
This is OK but not great. This method shouldn’t really need to know the map, and ideally Builder wouldn’t know about it at all. (We may not be able to stick with that entirely, because we do have to get the map built somehow. But here, we can do this:
function DungeonBuilder:clearLevel()
for key, tile in self.dungeon:tilePairs() do
tile:convertToEdge()
end
end
function BuilderView:tilePairs()
return self.dungeon.map:pairs()
end
That should work. It does. Can we remove BuilderView:map
? If we change this as well:
function DungeonBuilder:convertEdgesToWalls()
for key,tile in self.dungeon:tilePairs() do
tile:convertEdgeToWall()
end
end
Now we can remove the :map()
from BuilderView and test again. Test. Works. Commit: Replace calls to BuilderView:map with tilePairs. Remove map.
Continuing the search:
function DungeonBuilder:createRandomRooms(count)
local rooms = self:getRooms() -- <===
local r
while count > 0 do
count = count -1
local timeout = 100
local placed = false
while not placed do
timeout = timeout - 1
r = Room:random(self.dungeon:tileCountX(),self.dungeon:tileCountY(), 5,14, self.dungeon:asTileFinder())
if r:isAllowed() then -- <===
placed = true
table.insert(rooms,r) -- <===
r:paint() -- <===
elseif timeout <= 0 then
placed = true
end
end
end
end
This is righteous, we’re just fetching things we actually need. Here’s the next one, and it’s an opportunity:
function DungeonBuilder:createLearningRooms()
local w = 12
local h = 8
local a2 = { "In this room, there is a health power-up.",
"Please walk next to it (not onto it)",
"and press the ?? button.",
"You'll get an informative message about whatever you're next to." }
local t = {
{2,2, w,h},
{15,2, w,h, a2},
{28,2, w,h},
{41,2, w,h},
{2,11, w,h},
{15,11, w,h},
{28,11, w,h},
{41,11, w,h},
{2,20, w,h},
{15,20, w,h},
{28,20, w,h},
{41,20, w,h},
}
for i,r in ipairs(t) do
r[1] = r[1]+24
r[2] = r[2]+24
end
self.dungeon:createRoomsFromXYWHA(t, self.RUNNER)
local r2 = self:getRooms()[2]
local lootTile = r2:tileAt(2,2)
local loot = Loot(lootTile, "health", 5,5)
loot:setMessage("This is a Health Power-up of 5 points.\nStep onto it to add it to your inventory.")
end
The createRoomsFromXYWHA
is just the sort of thing we’re trying to move over. I’ll bring a copy over into Builder and see what it does. Moved but not edited, it looks like this:
function Dungeon:createRoomsFromXYWHA(roomTable, runner)
local makeRoom = function(result,roomDef)
return result + Room:fromXYWHA(roomDef, runner:tileFinder())
end
ar.reduce(roomTable, self.rooms, makeRoom)
end
The only thing I see there that needs help is the self.rooms
. We need to process the actual DungeonRooms table … that ar.reduce
is putting rooms into it. We’ll do this:
function DungeonBuilder:createRoomsFromXYWHA(roomTable, runner)
local makeRoom = function(result,roomDef)
return result + Room:fromXYWHA(roomDef, runner:tileFinder())
end
ar.reduce(roomTable, self.dungeon:rooms(), makeRoom)
end
And in the BuilderView … I find getRooms
already. I’ll use that instead. I have high hopes for this. Test to find out why I’m mistaken. The learning level builds. Remove the method from Dungeon and test again. All good. Commit: Move createRoomsFromXYWHA from Dungeon to Builder.
Find another self.dungeon:
. … There are a lot of righteous references to self.dungeon
now. They are irritating, because Codea’s “Find” capability is very last century. I’ll do a quick change. I’ll save two copies of my input BuilderView, one called view
and one dungeon
. I’ll change all the righteous references to view
and they’ll disappear from … well, view.
function DungeonBuilder:init(runner, dungeonView, levelNumber, playerRoomNumber, numberOfMonsters)
self.RUNNER = runner
self.view = dungeonView
self.dungeon = dungeonView
self.levelNumber = levelNumber
self.playerRoomNumber = playerRoomNumber
self.numberOfMonsters = numberOfMonsters
end
Immediately I don’t entirely like this idea, because there are three cases, not just two.
- Things in DungeonBuilder that point to code in dungeon that needs to be changed.
- Legitimate “final” references through the view, which should probably all point to pure data when we’re done.
- Things in BuilderView that themselves refer to things we may want to move over.
An example of the third case:
function BuilderView:randomRoomTile()
return self.dungeon:randomRoomTile()
end
Assuming that Dungeon doesn’t need randomRoomTile
during game play, that method should move. So I’ve been a bit careful with which ones I change to self.view
, trying to hit only things that I think are done-done.
Anyway, back to work. First test and commit Two copies of builderView, one called dungeon, used to mar things that change, one “view” for things that are done.
Now back to work. Let’s see about this:
function DungeonBuilder:dcPrepare()
TileLock = false
self.dungeon:createTiles()
self:clearLevel()
DungeonContents = DungeonContentsCollection()
MonsterPlayer(self.RUNNER)
Announcer:clearCache()
end
Ah. The createTiles
method might be tricky. What does it look like in situ?
function Dungeon:createTiles()
Maps:cartesian()
self.map = Maps:map(self.tileCountX+1,self.tileCountY+1, Tile,TileEdge, self:asTileFinder())
end
Yes. To make this work we’ll need to know more in Builder than we do now, namely, this intricate bit. And we’ll need a setter in Dungeon. So be it. Let’s try, we’re green and this shouldn’t take long to try.
function DungeonBuilder:createTiles()
Maps:cartesian()
local map = Maps:map(self.view:tileCountX()+1,self.view:tileCountY()+1, Tile,TileEdge, self.view:asTileFinder())
self.view:setMap(map)
end
I think this is the change. I need to change the call:
function DungeonBuilder:dcPrepare()
TileLock = false
self:createTiles()
self:clearLevel()
DungeonContents = DungeonContentsCollection()
MonsterPlayer(self.RUNNER)
Announcer:clearCache()
end
And I need setMap
:
function BuilderView:setMap(aMap)
self.dungeon:setMap(aMap)
end
function Dungeon:setMap(aMap)
self.map = aMap
end
I expect this to work. It does. Let’s look for other users of createTiles and see if we can do without our method in BuilderView and in Dungeon itself. There seem to be none. Delete the other createRoom
methods. Commit: Move createTiles to DungeonBuilder, remove scaffolding.
OK, who’s next?
function DungeonBuilder:setupMonsters()
local monsters = Monsters()
monsters:createRandomMonsters(self.RUNNER, self.numberOfMonsters, self.levelNumber, self.playerRoomNumber)
monsters:createHangoutMonsters(self.RUNNER, 2, self.wayDownTile)
self.dungeon:setMonsters(monsters)
end
This is righteous, change to refer to view
:
function DungeonBuilder:setupMonsters()
local monsters = Monsters()
monsters:createRandomMonsters(self.RUNNER, self.numberOfMonsters, self.levelNumber, self.playerRoomNumber)
monsters:createHangoutMonsters(self.RUNNER, 2, self.wayDownTile)
self.view:setMonsters(monsters)
end
What’s left? These …
function DungeonBuilder:placeSpikes(count)
for i = 1,count or 20 do
local tile = self.dungeon:randomHallwayTile()
Spikes(tile)
end
end
function DungeonBuilder:placeWayDown()
local r1 = self:getRooms()[1]
local target = r1:centerTile()
self.wayDownTile = self.dungeon:farawayOpenRoomTile(r1, target)
local wayDown = WayDown(self.wayDownTile)
self.RUNNER:setWayDown(wayDown)
end
function DungeonBuilder:randomRoomTile(roomNumberToAvoid)
local avoidedRoom = nil
if roomNumberToAvoid < #self:getRooms() then
avoidedRoom = self:getRooms()[roomNumberToAvoid]
end
return self.dungeon:randomRoomTile(avoidedRoom)
end
I expect these to be tricky. More important, I’m a bit under the weather today, having had a tooth ripped out yesterday, so I’m going to stop here and sum up.
Summary
First thing: I find that when I’m not physically near my best, I tend to make more mistakes and notice them less. So before doing anything complex, I think it’s best to find someone to work with, or to wait until I’m closer to nominal. YMMV, but I know how I’d bet.
Second, we have seven commits in less than an hour, so that’s pretty decent. We moved a few building methods out of Dungeon and into Builder, which is what we’re here for.
I think the BuilderView is helping a bit, keeping my concerns limited. If we do accidentally try to do anything unauthorized to the Dungeon, we’ll get an error. Perfect.
I’m outa here. When my FTP comes back, I’ll push this article up for my reader.
See you next time, reader!