Dungeon 71
An interesting problem. Is there a clean solution, or do I need a bigger hammer?
Yesterday, “off line”, I renamed some more of the tiles and adjusted the tiling logic to use them. Nothing to see here.
But now that I have these fine tiles, I have a couple of interesting problems. One of them will be whether to use the “Face” tiles that provide the 3/4 view of walls on the north side of things. But before that, there’s another um opportunity.
The regular “Wall” tiles come in various styles, depending on their orientation in the layout. Here are some screenshots from the tile set’s page on graphicriver.net
A bit of thought tells us that the choice of a wall tile depends on what’s beside it. If it’s a right hand wall, with floor on its left only, it should be the tile with a row of white stones on the left, and dirt on the right:
But if it’s at the northeast corner, with only a floor tile to its southwest, it should be the one with just one stone in the lower right corner (I think):
And if it’s in an inside corner, with tiles to its right, southeast, and bottom, then it should (probably) be this one:
So the problem, stated vaguely, because I only understand it vaguely, is to come up with a scheme to select wall tiles based on how many floor tiles are adjacent to the tile, and where they are. I’ll draw a picture of the situation to get my head clear about the cases, but offhand I think they are:
- Top of room
- Bottom of room
- Left of room
- Right of room
- NW outside corner
- NE outside corner
- SW outside corner
- SE outside corner
- NW inside corner
- NE inside corner
- SW inside corner
- SE inside corner
And I think there are quite likely a few others, when two hallways are parallel and separated by only one row, so that the cases include
- Top of one room bottom of another
- Inner hallway end right
- Inner hallway end left
And the same for vertical hallways.
- Left of one room right of another
- Inner hallway end up
- Inner hallway end down.
And I think it’s even possible to leave a single square open, surrounded by floor tiles on all sides.
- room on all sides
That last one would probably use this tile:
Now it seems to me that the number of cases can’t possibly be 19, so there must be some possibilities that I’ve missed or duplicated. We’ll find that out soon enough, I imagine.
Prior Art
We do have some code that puts wall tiles around carved hallways. It looks like this:
function GameRunner:convertEdgesToWalls()
for i,row in ipairs(self.tiles) do
for j,tile in ipairs(row) do
tile:convertEdgeToWall()
end
end
end
function Tile:convertEdgeToWall()
if self.kind == TileEdge and self:hasRoomNeighbor() then
self.kind = TileWall
end
end
function Tile:hasRoomNeighbor()
local ck = { vec2(1,0),vec2(1,1),vec2(0,1),vec2(-1,1), vec2(-1,0),vec2(-1,-1),vec2(0,-1),vec2(1,-1)}
for i,p in ipairs(ck) do
local pos = self:pos() + p
local tile = self.runner:getTile(pos)
if tile:isRoom() then return true end
end
return false
end
This code relies on the fact that there is only one kind of wall tile, so we can use it anywhere.
There’s also wall building around rooms. After we find a place for a room, we paint it into the array of tiles:
function Room:paint()
-- leave wall on all four sides
for x = self.x1,self.x2 do
for y = self.y1,self.y2 do
self.runner:setTile(self:correctTile(x,y))
end
end
end
function Room:correctTile(x,y)
if x == self.x1 or x == self.x2 or y == self.y1 or y == self.y2 then
return Tile:wall(x,y, self.runner)
else
return Tile:room(x,y, self.runner)
end
end
This code sets the outer perimeter of the space allocated to wall. We could almost use it to choose left/right/cop/bottom/corners, with a more extensive nest of ifs.
The bugaboo, however, is hallways. After rooms are allocated, we create a hallway between each consecutive pair of rooms, varying whether we move first horizontally toward it or first vertically. This process ensures that all our rooms are connected, and tends to make interesting maps. But some of the “interesting” things get in the way of placing smart wall tiles. In principle, a hallway can touch any tile of any existing room. It can cut the entire top or side off the room, leaving a room with a hallway at the top.
I was going to fiddle the code to show the full map all the time, and then generate some maps showing odd things. It turns out that the map is barely visible, since it shows the floor tiles, which are not just barely not black. This will need improving. Meanwhile here are a few interesting layouts:
Enough Complication?
I think I’ve described enough about what’s going on to confuse us all with the problem. It seems to me that there is a core problem whose solution would help us:
Given a tile in the game, not a floor tile, return the sprite that tile should use, as a function of the 8 booleans representing whether each of the 8 tiles around it is a room tile.
Hm. Put this way, it’s “obvious” that there are 256 possible arrangements of a tile and its eight neighbors. Are they all possible given our layout process? And are they all unique? Do some override others? Are there patterns we can usefully apply, such as reflections and rotations?
I think we can answer some of these. Reflections and rotations will not be useful to us, because the tiles we want to use are not the same rotated or reflected. So the left side tile is not generally a reflection of the right side tile, even if we could reflect a tile. (We can do that: recall that monsters turn to face the player. But I don’t think the tiles we’re given work that way:
I’ve been looking at a tic-tac-toe diagram, imagining an “edge” tile in the middle, and trying to see what kind of wall pattern might be impossible around an edge tile. I’m starting to think that all combinations are possible. Yikes. I really don’t want to fill out a 256-element table.
Any “yikes” aside, here’s what I want to know:
- What data structure will allow me to easily specify the tile to be used for each unique case?
- What data structure will be useful to the program in selecting the tile needed given the case?
These two may not be the same. It’s easy, for example, to produce an integer from 0-255 based on the surrounding tiles of a given tile. That integer could be used to select the desired tile from our table of (yikes) 256 elements. But that table would be a veritable #@$%^@! to fill in.
I’m pretty sure that if I can devise a good way to input the data, I can devise a way to process that data to select the right tile. And we’re not very concerned about speed here, since the tiles are just computed once, when the dungeon is laid out.
But at this moment, I’m not seeing a good way to do this.
Maybe I need to create an app? It would be “easy” to display all the available tiles, and a tic-tac-toe thing showing the state we’re trying to fill in. Then you tap a tile, it displays, and if you like it, you tap OK and that value is recorded. 256 cycles later, you’re done.
I like that idea. Let’s try it, at least a spike to see if it makes sense.
New App
I’ll set up an app with CodeaUnit in it, just in case there’s TDD to be done.
We have 83 total tiles to be concerned about, though I think I have them classified into groups with useful names like Face_xx, Floor_xx, Pit_Column_xx, Wall_xx, and Water_xx. We only want the Pit and Wall ones in our array.
There turn out to be 36 tiles with those names. A bit of hackery and I have this:
-- CUBase
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
local all = asset.documents.Dropbox.all
used = {}
for i,a in ipairs(all) do
local n = a.name
local i = string.find(n,"^Pit")
local j = string.find(n,"^Wall")
if i or j then
table.insert(used,a)
end
end
print(#used)
end
function draw()
-- if CodeaUnit then showCodeaUnitTests() end
--background(128,128,128)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(128,128,128)
rect(590,190, 670,670)
for i,a in ipairs(used) do
local row = (i-1)%6
local col = (i-1)//6
pushMatrix()
translate(600,200)
translate(col*110, row*110)
sprite(a,0,0,100,100)
popMatrix()
end
popStyle()
end
With this result:
So that’s nice. I notice there are two tiles showing floor instead of the apparent mud on the others. That may turn out to be interesting: I suppose the creator of these tiles had a reason for those. (I’m also vaguely thinking about how I could use these tiles and Procreate to make more if I really must.)
Now we want to display a “tic-tac-toe showing floor tiles in some of the outer cells, for us to put wall tiles in the middle. If I go with the same size tiles, which makes sense, the rectangle will be 340 square (3*100 + 4 fenceposts).
No. We want no fenceposts here, so that we can see how the files fit together. Width and height will be 320.
function drawTicTacToe()
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(192,192,192)
rect(90,290,320,320)
for row = 0,2 do
for col = 0,2 do
pushMatrix()
translate(100,300)
translate(col*100,row*100)
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
popMatrix()
end
end
popStyle()
end
That gives this:
Now it starts getting tricky. We have 256 cases to consider, 0-255, where the bits in the number represent positions in the periphery. And there will need to be some kind of control over moving to the next setting, and selecting the tile to be displayed, and so on.
Inch by inch.
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
local all = asset.documents.Dropbox.all
used = {}
for i,a in ipairs(all) do
local n = a.name
local i = string.find(n,"^Pit")
local j = string.find(n,"^Wall")
if i or j then
table.insert(used,a)
end
end
byte = 0xFF
selected = used[17]
end
I’ve decided that byte
will be our value that increments. I’ve set it to 255 to give a full display to start with. And selected
will be the tile most recently touched.
Now to make this display. What’s a good way to display those bits? Let’s do a shift.
function drawTicTacToe(byte)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(192,192,192)
rect(90,290,320,320)
local bits = byte
for row = 0,2 do
for col = 0,2 do
pushMatrix()
translate(100,300)
translate(col*100,row*100)
if col ~= 1 or row ~= 1 then
if bits&1 == 1 then
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
bits = bits >> 1
end
end
popMatrix()
end
end
popStyle()
end
This is getting intricate but it works as intended:
Now I want to watch it count. It turns out, that code above doesn’t remotely work. Oh, maybe the shift is in the wrong place. I’ll try that, and failing that, back to the drawing board.
Yes that was it:
function drawTicTacToe(byte)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(192,192,192)
rect(90,290,320,320)
local bits = byte
for row = 0,2 do
for col = 0,2 do
pushMatrix()
translate(100,300)
translate(col*100,row*100)
if col ~= 1 or row ~= 1 then
if bits&1 == 1 then
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
end
bits = bits >> 1
end
popMatrix()
end
end
popStyle()
end
Here’s a movie of the thing counting. Looks just like you’d expect of a binary counter with a hole in the middle:
So that tells us we can display what we need to. Remove the hackery of the timer and see what’s next.
I think what we want to do with this thing is to iterate manually through all 256 possibilities (yikes), touch tiles in the big matrix, see how they look, then when we like them, save them in an array, indexed by byte
, in a text form that we can retain. This will take a few steps.
First, let’s see about displaying the chosen file in the middle.
function drawTicTacToe(byte)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(192,192,192)
rect(90,290,320,320)
local bits = byte
for row = 0,2 do
for col = 0,2 do
pushMatrix()
translate(100,300)
translate(col*100,row*100)
if col ~= 1 or row ~= 1 then
if bits&1 == 1 then
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
end
bits = bits >> 1
else
sprite(selected,0,0,100,100)
end
popMatrix()
end
end
popStyle()
end
You may be wondering …
Where is all that clean, expressive, small method code for which I am always the intrepid champion? Well, to tell the truth, in all this excitement I kind of lost track myself, but I do feel lucky. This is just a one-time tool that we (believe) we’ll never modify again. (Do we believe that? What if I get a different tile set, or there are more than 36 I want to keep track of?)
But this is a spike, and we allow them to get rough, until the roughness starts to hurt.
But I digress …
How are we going to decide which tile we’ve touched?
I guess we have to convert a touch in the big square back into row and column indices. And I think that the first element in the array is at the bottom left. I wonder if we should be displaying the names? Maybe not yet.
There are at least two ways to do this that come to mind. One is to detect the touch coordinates and just smash them with arithmetic until they confess which tile number they represent. The other is to build up a helper data structure, an array of screen rectangles, each telling us what we want to know when it’s touched.
The latter is better, not least because it can handle arrangements that change far more readily than the arithmetic hammering can.
Let’s do that. We have each rectangle in hand … if we can recognize it:
for i,a in ipairs(used) do
local row = (i-1)%6
local col = (i-1)//6
pushMatrix()
translate(600,200)
translate(col*110, row*110)
sprite(a,0,0,100,100)
popMatrix()
end
The rectangle corner will be (600+col110, 200+row110) and it’ll be 100x100. Let’s do a tiny object. I’m not sure quite what I want here. This is a guess:
TouchPoint = class()
function TouchPoint:init(x,y, tile)
self.x = x
self.y = y
self.tile = tile
end
function TouchPoint:touched(touch)
local pos = touch.pos
if pos.x > self.x and pos.x < self.x+100 and pos.y > self.y and pos.y < self.y+100 then
return self.tile
else
return nil
end
end
I observe that if I’ve got these guys I can make them display themselves, but that’s not the swamp I’m here to drain. Let’s create them and use them.
for i,a in ipairs(used) do
local row = (i-1)%6
local col = (i-1)//6
pushMatrix()
translate(600,200)
translate(col*110, row*110)
table.insert(tiles, TouchPoint(600+col*110,200+row*110,i))
sprite(a,0,0,100,100)
popMatrix()
end
This is getting weird. Let’s make it work tho, then we really must clean up the camp a bit.
function touched(touch)
for i,tp in ipairs(tiles) do
local t = tp:touched(touch)
if t then
selected = used[t]
return
end
end
end
And we get this:
Now let’s clean this up a bit. I don’t have it under version control. I may regret that but it’s a pain to set up.
function setup()
if CodeaUnit then
codeaTestsVisible(true)
runCodeaUnitTests()
end
used = allUsedTiles()
byte = 0x00
savedTime = ElapsedTime
selected = used[17]
tiles = {}
end
function allUsedTiles()
local all = asset.documents.Dropbox.all
local used = {}
for i,a in ipairs(all) do
local n = a.name
local i = string.find(n,"^Pit")
local j = string.find(n,"^Wall")
if i or j then
table.insert(used,a)
end
end
return used
end
Close enough for that.
function draw()
-- if CodeaUnit then showCodeaUnitTests() end
--background(128,128,128)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(128,128,128)
rect(590,190, 670,670)
drawTileArray()
popStyle()
drawTicTacToe(byte)
end
function drawTileArray()
for i,a in ipairs(used) do
local row = (i-1)%6
local col = (i-1)//6
pushMatrix()
translate(600,200)
translate(col*110, row*110)
table.insert(tiles, TouchPoint(600+col*110,200+row*110,i))
sprite(a,0,0,100,100)
popMatrix()
end
end
Now a bit more clarity in drawTileArray
:
function drawTileArray()
for i,a in ipairs(used) do
local row = (i-1)%6
local col = (i-1)//6
pushMatrix()
local tableOrigin = vec2(600,200)
local elementOrigin = vec2(col*110, row*110)
local pos = tableOrigin + elementOrigin
translate(pos.x, pos.y)
table.insert(tiles, TouchPoint(pos.x,pos.y,i))
sprite(a,0,0,100,100)
popMatrix()
end
end
Now for this mess:
function drawTicTacToe(byte)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(192,192,192)
rect(90,290,320,320)
local bits = byte
for row = 0,2 do
for col = 0,2 do
pushMatrix()
translate(100,300)
translate(col*100,row*100)
if col ~= 1 or row ~= 1 then
if bits&1 == 1 then
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
end
bits = bits >> 1
else
sprite(selected,0,0,100,100)
end
popMatrix()
end
end
popStyle()
end
That becomes this, which is somewhat better:
function drawTicTacToe(byte)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(32,32,32)
rect(90,290,320,320)
drawCells()
popStyle()
end
function drawCells()
local bits = byte
for row = 0,2 do
for col = 0,2 do
bits = drawCell(row,col, bits)
end
end
end
function drawCell(row,col,bits)
pushMatrix()
translate(100,300)
translate(col*100,row*100)
if col ~= 1 or row ~= 1 then
if bits&1 == 1 then
sprite(asset.documents.Dropbox.Floor_14, 0,0, 100,100)
end
bits = bits >> 1
else
sprite(selected,0,0,100,100)
end
popMatrix()
return bits
end
I’ve noticed something during testing. The answers are not obvious. Look at these examples, all with the same surrounding tiles:
This looks possible … but isn’t this more likely right, given what’s going to happen at the top?
I think it is, because there will be two vertical walls above, and a horizontal one at the top of the cell to our right, but not on the bottom. But we might want the corresponding upside down L shape with one cell in the corner.
This is going to be difficult to do correctly one at a time.
That tells me that either we’ll have to accept textual input as well as output, or we’ll be faced with a decision to do the task all over (error-prone and tedious) or to update manually after the first run-through.
Amusing.
Press on a bit. Let’s display status below the tic-tac-toe
function draw()
-- if CodeaUnit then showCodeaUnitTests() end
background(0)
pushStyle()
spriteMode(CORNER)
rectMode(CORNER)
fill(128,128,128)
rect(590,190, 670,670)
drawTileArray()
drawTicTacToe(byte)
drawStatus()
popStyle()
end
function drawStatus()
pushMatrix()
pushStyle()
textMode(CORNER)
fontSize(30)
s = string.format("[%X] = \"%s\"", byte, selected.name)
text(s,90,250)
popStyle()
popMatrix()
end
That gives us this behavior:
The text shows the floor combination currently in effect, and the tile chosen in the center.
It remains to do the ticking through, and saving of the information, both internally and in a text output form of some kind. I’m moderately sure we can make Codea create a text file of some kind. Moderately sure.
Anyway, I’m tired and hungry. This has been an interesting experiment. I’m still stuck with creating 256 entries, but now it might at least be possible.
See you next time!