Tiles! We have Tiles!
I found some of my favorite tiles and did an experiment yesterday. We have some interesting work ahead. We spike a border!
I found some of the tiles on my iPad, enough for the experiment I’ll report below. I couldn’t find the entirety, so bought another copy to use here. They’re not yet installed, we’ll report on that below as well.
The Experiment
I began by copying the tiles I found into a new folder in the project, ‘assets/images’, thinking that we’ll probably have other kinds of assets, like sounds or characters.
Then I bashed a few things in. I was also attending a boring on-line meeting, so I have almost no useful recollection either of the meeting or of what I did. According to PyCharm, I have changed DungeonView and RoomView, so let’s look and see what I did.
In DungeonView, I changed from a shape list, which was holding those rectangles, to a SpriteList, which can contain textures. I changed these methods:
class DungeonView:
def run(self):
self.setup()
self.window.show_view(self)
arcade.run()
def setup(self):
self.setup_scroller()
self.setup_cameras()
self.shape_list = arcade.SpriteList() # changed
self.create_rooms(self.shape_list)
def create_rooms(self, shape_list):
for room in self.dungeon.rooms:
RoomView(self.dungeon, room).create(shape_list)
We could and should rename shape_list to sprite_list but I was bashing, and only that one line is changed as of now.
RoomView changes are a bit more extensive. First, there’s this:
class RoomView:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
path = '/Users/ron/PycharmProjects/dungeon/assets/images/'
f21 = arcade.load_texture(path + 'Tile (21).png')
fgrate = arcade.load_texture(path + 'Floor_13_Grateb.png')
f22 = arcade.load_texture(path + 'Tile (22).png')
self.textures = [f21, f22, fgrate]
self.weights = [20, 5, 1]
Here we do the arcane incantation that loads a list of three textures and associate three weights with them, intending that we’ll have approximate rations of 20 f21s, to 5 f22s, to 1 grate. We’ll see that happening in a moment:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
texture = random.choices(self.textures, self.weights)[0]
sprite = arcade.Sprite(texture, scale=1/16)
dx = cell.x * cell_size + cell_size //2
dy = cell.y * cell_size + cell_size //2
sprite.position = (dx, dy)
shape_list.append(sprite)
For each cell in the room we pick a random texture, according to those weights, create a Sprite holding that texture scaled down by 16, because the originals are 256 pixels, position our chosen sprite at the display coordinates associated with its x and y, and append it to the shape list.
That is all. And here is what we get when we run:

That, rather surprisingly, is exactly what I had in mind. And it’s a great deal like what I want.
In fact, I think we’ll commit that: it’s that good a step in the direction I think we need to go. Commit: initial tiling.
So that was yesterday afternoon. The only false step was that I initially tried associating the texture with the individual calls, and changed their init to load a texture. That was bad. We create a lot of cells during testing and running, and loading a texture opens, reads, and closes a file. Things slowed down. A lot.
With that in mind, here are some things I think we need to do:
Assets object
Now the fact is, we are still loading those textures every time we create a RoomView, which is just once per room but still probably a dozen times more than we really need to.
At a guess, what we really ought to do is to have a thing called assets containing all our program’s assets, probably accessed something like self.assets['f22']. We might choose to provide better names. We could hardly do worse. The original tiles are mostly named things like ‘Tile (20).png’, which isn’t as useful as it may seem.
Use :resources: notation
There is some way to associate a path with the Arcade ‘:resources:’ notation, such as we see in our SecretKey:
class SecretKey(ReceivableContent):
def __init__(self, name='a secret key', resource=None):
resource = ':resources:images/items/keyYellow.png'
super().__init__(name, resource)
self.visible = False
So far, I have not decoded how to make that work. We’ll get there in due time, or live with the actual file path. It’s not like we’re going to put this game on the app store.
Borders
There’s one big issue. You may recall that our rooms used to look like this:

The colored lines represent the boundaries between rooms (green) or solid rock (red). We have lost that delineation with our new tiles, and we will want to do something about that. There are some tiles in the set that have stone borders, like this:

There are some examples with two adjacent sides bordered, although there does not seem to be one with a border on all three sides. (It is possible that I could make one.) So my plan is to somehow—a miracle occurs about here—identify that a cell is on a border and select a bordered tile, rotate if needed, and place it.
Mud rooms
There is a fairly complete set of tiles that look like mud. We might enjoy making a mud room.
Water
There are some water tiles. We could make a water hazard of some kind.
Triggers and Traps
There is a room tile that has two forms, a raised or depressed center, clearly some kind of cell that when you step on it, it triggers something. We need that for our current button that displays the secret key, and we need to be able to change its texture from one to the other form.
There is also a switch or lever kind of thing with four positions:

I think these items are more likely to be dealt with as Content rather than fixed flooring, but they’re part of our new set of tiles and we’ll want to use them.
Get to Work!
Let’s try an experiment. As we choose the tiles for room cells, let’s see if we can interrogate the borders and use a bordered tile. We could TDD this but let’s just spike and see what we can make happen.
From this:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
texture = random.choices(self.textures, self.weights)[0]
sprite = arcade.Sprite(texture, scale=1/16)
dx = cell.x * cell_size + cell_size //2
dy = cell.y * cell_size + cell_size //2
sprite.position = (dx, dy)
shape_list.append(sprite)
Extract:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.choose_flooring(cell, shape_list)
def choose_flooring(self, cell, shape_list):
texture = random.choices(self.textures, self.weights)[0]
sprite = arcade.Sprite(texture, scale=1 / 16)
dx = cell.x * cell_size + cell_size // 2
dy = cell.y * cell_size + cell_size // 2
sprite.position = (dx, dy)
shape_list.append(sprite)
Extract again:
def create(self, shape_list):
cells = set(self.room.cells)
for cell in cells:
self.choose_flooring(cell, shape_list)
def choose_flooring(self, cell, shape_list):
sprite = self.choose_normal_flooring(cell)
shape_list.append(sprite)
def choose_normal_flooring(self, cell):
texture = random.choices(self.textures, self.weights)[0]
sprite = arcade.Sprite(texture, scale=1 / 16)
dx = cell.x * cell_size + cell_size // 2
dy = cell.y * cell_size + cell_size // 2
sprite.position = (dx, dy)
return sprite
I’m guessing here, but my plan, you can probably see, is to have another method to choose bordered flooring.
We have this method left over from the line drawing version, and it gives us a clue where to look:
def create_border(self, cell, shape_list):
borders: BorderList = self.layout.get_borders(cell)
for border in borders:
start, finish = border.line_coordinates(cell.xy, cell_size)
color = border.select_boundary(self.boundary_colors)
shape = arcade.shape_list.create_line(*start, *finish, color)
shape_list.append(shape)
What does this get_borders do for us?
class DungeonLayout:
def get_borders(self, cell):
return self.border_map[cell]
def make_borders(self):
self.border_map = BorderMap(self)
That leads us to BorderMap … we follow our nose …
class BorderMap:
def __init__(self, layout):
self.borders = {}
for room in layout.rooms:
for cell in room:
borders = BorderList(layout, cell)
self.borders[cell] = borders
class BorderList:
def __init__(self, layout, cell):
self.borders = []
my_room = layout.get_room(cell)
for side in [(1,0), (-1,0), (0, -1), (0,1)]:
neighbor = layout.at_offset(cell.xy, side)
neighbor_room = layout.get_room(neighbor) if neighbor else None
border = my_room.border_type(neighbor_room)
self.borders.append(Border(side, border))
class Room:
def border_type(self, neighbor_room):
if neighbor_room is None:
return 'wall'
elif neighbor_room == self:
return 'open'
else:
return 'partition'
I want to handle just one case. If the north border is not open, I want to assign a special texture.
def choose_flooring(self, cell, shape_list):
if self.has_north_border(cell):
sprite = self.choose_north_border(cell)
else:
sprite = self.choose_normal_flooring(cell)
shape_list.append(sprite)
def has_north_border(self, cell):
return False
def choose_north_border(self, cell):
pass
This should run and display as before. I’ll check that. It does. Not going to commit, this is a spike.
Let’s fill in choose_north_border so we’ll be ready to test if we can figure out the north border thing. The one we want is ‘Tile (36)’, which I have determined by exhaustive search. I add that to the init:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
path = '/Users/ron/PycharmProjects/dungeon/assets/images/'
f21 = arcade.load_texture(path + 'Tile (21).png')
fgrate = arcade.load_texture(path + 'Floor_13_Grateb.png')
f22 = arcade.load_texture(path + 'Tile (22).png')
self.fnorth = arcade.load_texture(path + 'Tile (36).png')
self.textures = [f21, f22, fgrate]
self.weights = [20, 5, 1]
Hate those names. I quickly learn that my extracts weren’t quite right, but we’re spiking. We’re here to learn how to do this.
def choose_north_border(self, cell):
texture = self.fnorth
sprite = arcade.Sprite(texture, scale=1 / 16)
dx = cell.x * cell_size + cell_size // 2
dy = cell.y * cell_size + cell_size // 2
sprite.position = (dx, dy)
return sprite
Now for the big payoff, finding out if there is a northern border.
This is nasty. The BorderList isn’t much help so we have to search it:
def has_north_border(self, cell):
borders: BorderList = self.layout.get_borders(cell)
north = [border.boundary for border in borders if border.side == (0,1)][0]
return north != 'open'
However, it works! Here’s the picture, which needs perhaps a bit of explanation.

See those white edges on the cells surrounding the center? Those are north walls! As intended! There are more down near the bottom, delineating a room down there.
We are gratified, elated, pleased, and somewhat happy. In a few minutes, we have a simplistic idea, embodied in some truly atrocious code, that identifies a northern boundary on a cell and selects a nearly suitable tile to display that boundary.
I think of this situation as analogous to building a bridge across a river, a thing about which I know essentially nothing. In my imaginary bridge world, we begin by casting a thin line across the chasm we need to bridge, providing a basis for pulling stronger and stronger lines across, upon which we begin to build our bridge. As we build, what we have looks more and more like a bridge and less and less like a raggedy old rope spanning the gap.
I really have no clue how to build a bridge, but this image works for me. We’ve built a raggedy connection between our objects, causing them to behave pretty much as we want them to. We have moved from “how the hell can we ever do this” to “well, that really could be better”, the important first step in doing anything new.
Summary
We’ll stop here. I’ve been at it exactly two hours, and the essentials of what we need to do are becoming clear, something like:
-
Using similar but probably different border-finding logic, identify the border configurations of each cell.
-
Select a cell texture with zero, one, two (or three?) borders.
-
We’ll probably have to rotate the cell into position, as I don’t think we have all the cases. Either that or we’re in for some texture making, which is quite possible if it comes to it.
-
We’ll get borders on both sides of the mating cells between rooms. We may or may not like that.
-
We’ll need to figure out movement between rooms. Presently you can walk through the borders. Presumably that’s not good. Also we’ll need doors.
It’s premature even to plan that much, but it’ll probably go something like that.
For now, we got what we wanted, a northern border. Woot! See you next time!