New Textures
I made one new texture last night, on my iPad. So I think I’ll point the game to the recently-purchased set.
I have the new tile set on my Mac desktop, and my iPad can see those files via some kind of cloud magic. Using Procreate on the iPad, I clumsily merged the ES and WS tiles to make a new tile, EWS. Seen small enough it looks pretty good. Seen here … well, look for yourself:

Not bad for first try. I should make the two stone sides more different. Live and learn.
So my plan for now, is “simply” to change the path in the TextureFinder to point to that folder, which will work fine here at home if not in production, and then I’ll be able to create and modify tiles without a lot of moving back and forth.
One issue that I think will arise is that some of the names in the current folder won’t be found in the new one. Maybe we’ll do a verify method, just to see how to do that. Maybe we won’t.
In TextureFinder:
class TextureFinder:
def __init__(self):
self.path = '/Users/ron/Desktop/DungeonTiles/png/tiles/'
And two tests fail, The two that check correct application of the path name. They were checking for the old path. We’ll commit this much, but I suspect main may break. Commit: using new tile set from desktop.
I can’t resist trying main, even though the right thing is probably to verify the files. I’m sure some are wrong, because I renamed a bunch when working on the iPad dungeon years back. But I gotta see what happens. Sure enough:
FileNotFoundError: [Errno 2] No such file or directory:
'/Users/ron/Desktop/DungeonTiles/png/Tile (21).png'
After a bit of renaming, Everything seems to work. I have gone back to the original set’s names, basically ‘Tile (nn).png’, despite despising despicable spaces in file names. My new tiles have more mnemonic names, ‘F_ENW’ and ‘F_EWS’, with F_ for Floor and the rest representing the closed sides from ENWS, counterclockwise from East. I propose to use M_ for mud tiles and W_for water tiles, in all good time.
I think we’d be wise to verify the paths to the tiles we propose to use. I add these tests and code:
def test_verify_exists(self):
tf = TextureFinder()
assert tf.verify_path_exists('F_EWS.png')
assert not tf.verify_path_exists('NoWay.png')
def test_verify_name_found(self):
tf = TextureFinder()
assert tf.verify_name_found('EWS')
assert not tf.verify_name_found('NOPE')
The first test provides a method to verify a file path, the looked-up or otherwise known actual file name, while the second looks up the border string to get and verify the file associated with it. The latter is the one that we’ll use to verify the whole TextureFinder, soon.
class TextureFinder:
def __init__(self):
self.path = '/Users/ron/Desktop/DungeonTiles/png/tiles/'
self.names = {
'': 'Tile (21).png', # none
'E': 'Tile (29).png', # east
...
}
def short_name(self, borders):
try:
return self.names[borders]
except KeyError:
return 'NoNoNoNo'
def full_name(self, borders):
return self.add_path(self.short_name(borders))
def add_path(self, short_name):
return self.path + short_name
def verify_name_found(self, border_name):
return self.verify_path_exists(self.full_name(border_name))
def verify_path_exists(self, path_string):
path = Path(self.path) / path_string
return path.exists()
I added a try:except: to short_name. I think we actually need to provide a texture that is guaranteed to exist, but I am not sure how that should best be done.
Additionally, Since I have begun to use Path here, we should look to see whether we can return paths and use them to fetch the actual resources or whether we have to stick with strings. We’ll see.
Let’s TDD verify on the TextureFinder, to verify all the names in it.
def test_verify(self):
tf = TextureFinder()
assert tf.verify()
tf.add_texture('XXX', 'XXX')
assert not tf.verify()
class TextureFinder:
def add_texture(self, name, texture):
self.names[name] = texture
def verify(self):
return all([self.verify_name_found(name) for name in self.names])
Test passes. Commit: added verification to TextureFinder.
Also there are these extra references in RoomView:
class RoomView:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
self.texture_finder = TextureFinder()
self.textures = ['Tile (21).png', 'Tile (22).png', 'Tile (13).png', 'Tile (23).png']
self.weights = [25, 5, 1, 1]
Those can also be wrong, and in fact some were. That puts some pressure on enhancing TextureFinder to deal with multiple choice tiles with weighted choices. But we can at least verify these by adding them to the finder and verifying here. Let’s do that, see how it feels. I think we’ll want to change how they are accessed as well.
class RoomView:
def __init__(self, dungeon, room):
self.dungeon = dungeon
self.layout = dungeon.layout
self.room = room
self.texture_finder = TextureFinder()
F1 = 'Tile (21).png'
F2 = 'Tile (22).png'
G1 = 'Tile (13).png'
G2 = 'Tile (23).png'
self.texture_finder.add_texture('F1', F1)
self.texture_finder.add_texture('F2', F2)
self.texture_finder.add_texture('G1', G1)
self.texture_finder.add_texture('G2', G2)
if not self.texture_finder.verify():
raise Exception('Invalid texture finder')
self.textures = [F1, F2, G1, G2]
self.weights = [25, 5, 1, 1]
This does the job but leaves us with our floor group still handled separately. Let’s commit: verifying all textures in RoomView.
What would be better would be this, I think:
...
if not self.texture_finder.verify():
raise Exception('Invalid texture finder')
self.textures = ['F1', 'F2', 'G1', 'G2']
self.weights = [25, 5, 1, 1]
Now we have the short names in textures. We need to change the choose:
def choose_flooring_texture(self, cell):
borders: BorderList = self.layout.get_borders(cell)
border_type = borders.border_string()
if border_type == '':
short_name = random.choices(self.textures, self.weights)[0]
name = self.texture_finder.add_path(short_name)
else:
name = self.texture_finder.full_name(border_type)
return arcade.load_texture(name)
Now our choices call is returning the key name, not the file name, so we have, in essence, a border_type, which really needs a better name. We can do this:
def choose_flooring_texture(self, cell):
borders: BorderList = self.layout.get_borders(cell)
border_type = borders.border_string()
if border_type == '':
border_type = random.choices(self.textures, self.weights)[0]
name = self.texture_finder.full_name(border_type)
return arcade.load_texture(name)
Now all the tiles are being served the same way, from border_type (needs renaming) to full_name. A small but noticeable improvement. Commit: all RoomView tiles served by TextureFinder.
Summary
A bit more than I had planned to do, but everything seemed to flow from one thing to the next. We have a better TextureFinder, which can verify existence of all the files it references, and we have used it to save and map additional files as needed by RoomView. That may be a step on the way to having TextureFinder directly support weighted collections of tiles to be chosen randomly.
I have a vague notion of how we might do that. What if instead of just a file name string, the TextureFinder saved a small object containing the string, returning the string via an accessor? Then we could create another small object, containing several file name strings, and some weights, returning one randomly.
Or, perhaps the second object contains a list of keys (currently called border type or border string) into the TextureFinder, randomly selecting a key and then fetching the actual file name with another call back to the Texture Finder. The first way is simpler, but the second has some appeal. Might be too clever: I’ll have to think about it.
Anyway, we’ve made the game better, because now we have tiled ENW and EWS tiles, as you can see here:

And the code is actually a bit better at the same time. More capability, better code. See you next time!