A couple of little changes from yesterday. Mostly just speculation, vague design thinking. I create an abstract class and draw an up-button.

I had a few idle minutes in the afternoon yesterday, so I made some modifications to allow this setup for the TextureFinder:

class TextureFinder:
    def __init__(self):
        self.path = '/Users/ron/Desktop/DungeonTiles/png/tiles/'
        floor_names = ['F1', 'F2', 'F16', 'F17', 'F18', 'F19', 'F20', 'G1', 'G2']
        floor_weights = [25,25,25,25,25,25,25, 5,5]
        flooring = RandomName(floor_names, floor_weights)
        raw_names = {
            '': RandomName(floor_names, floor_weights), # none
            'E': 'F_E.png', # east
            'N1': 'F_N.png', # north
            'N2': 'F_N2.png',
            'N': RandomName(['N1', 'N2']),
            'EN': 'F_EN.png',
            'W': 'F_W.png', # west
            'EW': 'F_EW.png',
            'NW': 'F_NW.png',
            'ENW': 'F_ENW.png',
            'S1': 'F_S.png', # south
            'S2': 'F_S2.png',
            'S': RandomName(['S1', 'S2']),
            'ES': 'F_ES.png',
            'NS': 'F_NS.png',
            'ENS': 'F_ENS.png',
            'WS': 'F_WS.png',
            'EWS': 'F_EWS.png',
            'NWS': 'F_NWS.png',
            'ENWS': 'Tile (57).png',
            'F16': 'Tile (16).png',
            'F17': 'Tile (17).png',
            'F18': 'Tile (18).png',
            'F19': 'Tile (19).png',
            'F20': 'Tile (20).png',
            'F1': 'Tile (21).png',
            'F2': 'Tile (22).png',
            'G1': 'Tile (13).png',
            'G2': 'Tile (23).png',
        }
        self.names = {key: self.wrapped(value) for key, value in raw_names.items()}
        self.add_complex_texture('', flooring)

The big change there is that we can directly add a RandomName instance in the init. That required a slight change: note the call to wrapped as we convert the raw_names into the names instance variable.

    def wrapped(self, value):
        if isinstance(value, str):
            return SimpleName(value)
        else:
            return value

This is, of course, a hack. If we’re passed a string, we wrap it in a SimpleName, otherwise we assume that it is already properly wrapped. I don’t love that, since checking the type of an object is a pretty sour code smell, but the ability to put our multi-texture random choice object right in line seemed good enough to try, so try it I did.

That’s about all I did. Now let’s look at the concerns I listed yesterday, and see if there are any new ones.

One concern was the need to import RandomName into RoomView, because RoomView was creating the random list items. That’s no longer the case … but we’re still faced with the need to create entirely different kinds of TextureFinder, for rooms made of mud or water or grass or something. I think we’ll defer that until creating a different room look seems like a priority.

I was concerned that RandomName is too clever, with its recursive call to allow it to use symbolic file names. We could have a RandomFile object instead, with a list of file names instead of TextureFinder keys. That would avoid the recursive call, which isn’t terribly important, but there is a hidden issue.

It would be easy to build a RandomName instance that referred to a name that was not in the keys. There is currently no check for that and if the texture in question were sufficiently low probability, we could be left with a run-time error in production. To deal with this, it might suffice to provide a HarmlessDefaultTexture that would be provided when the key is not found. Presently we return ‘NoNoNoNo’ as a file name, but we could certainly deal with that readily.

We do have a verify method for the TextureFinder:

    def verify(self):
        return all([self.verify_name_found(name) for name in self.names])

    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()

What does this do in the case of a RandomName instance? Well, it calls full_name, which calls short_name, which calls get, which will choose a random key and look it up. So only one of the possible files in a RandomName instance is checked.

We would do better, perhaps, to have a verify method on the ...Name instances. Note made, might be interesting.

I mentioned the possibility that we should make an AbstractBaseClass for these objects. Now is the right time:L there are two subclasses, and we have a solid reason to expect at least one more, a RandomFiles object, although once we have that we might remove RandomNames … except that there is a very nice thing that we could do with RandomNames. What if we had a RandomNames instance that pointed to two other RandomNames instances? That might allow a kind or reuse, and it might allow an easier way to handle the weights between common tiles and rare ones.

OK, I’ve convinced me. Let’s do the ABC right now.

class AbstractFileName(ABC):
    @abstractmethod
    def get(self, finder):
        pass

class SimpleName(AbstractFileName):
    ...

class RandomName(AbstractFileName):
    ...

Nothing to it, since the subclasses already had their get method. A perfect time to have done that. Yummy. Commit: provided abstract base class.

I think our biggest issue with the TextureFinder as it stands is the fact that there can be only one. We can deal with that by converting the current __init__ to a constructor method, and adding other ones.

A more interesting problem might be to come up with a more convenient way of specifying a TextureFinder than writing Python code. Maybe some textual form or something. Maybe dungeon layouts have a defined file format and a room’s whole tile set could be stored in a file folder.

Hmm, some kind of meta-dungeon. That could turn into fun, or horror, more or less unpredictably. We’ll keep that in mind but not too near the top of the list.

!! IDEA !!

Closer to the texture problem, there is the issue of representing content in the dungeon. Hmm. That could be very interesting. How is that done now? Let’s look at Content: it would be nice to have more things in the dungeon, we have only just begun with that, and things need pictures.

Ewww, this is definitely something needing attention. It’s done in main, and it is Ad Hoc with capitals, italics, and bold face:

def main:
    ...
    dungeon = Dungeon(layout)
    dungeon.populate()
    # options = layout.furthest_cells(target)
    # target = random.choice(options)
    target = Cell(32, 28)
    dungeon.place_player_at(target)
    path=':resources:images/items/'
    item_1 = ReceivableContent("a mysterious key", path + 'keyRed.png')
    item_2 = ReceivableContent("a precious sapphire", path + 'gemBlue.png')
    item_3 = ReceivableContent("another sapphire", path + 'garbled.png')
    dungeon.place_content_at(Cell(29, 28), item_1)
    dungeon.place_content_at(Cell(29, 29), item_2)
    dungeon.place_content_at(Cell(29, 30), item_3)
    flag = Button('flag')
    dungeon.place_content_at(Cell(34, 28), flag)
    key = SecretKey()
    dungeon.place_content_at(Cell(36, 28), key)
    screen_width = cell_size*dungeon.max_x
    screen_height = cell_size*dungeon.max_y
    arcade.Window(screen_width, screen_height, 'Caveat Emptor')
    DungeonView(dungeon).run()

That path. Is that a legitimate file path, or some magical thing that only Arcade understands? The latter. So those paths might work in a TextureFinder but would not verify with the current verification logic. Ah, but a search tells me that arcade.resources.resolve() will convert a resource path to a file path, so we can use that. And presumably resolving a path that is already a file path will just work. Tested. Yes. OK we can stop worrying about :resources: type paths now.

I decide to take a flyer. I change the Button object’s texture path:

class Button(Content):
    error_texture = ':resources:images/items/star.png'
    def __init__(self, name, resource=None):
        resource = '/Users/ron/Desktop/DungeonTiles/png/objects/floor button/Button (1).png'
        self.name = name
        try:
            self.shape = arcade.load_texture(resource)
        except:
            self.name = 'invalid resource: ' + name
            self.shape = arcade.load_texture(self.error_texture)

That arcane path points to the “up” version of a button tile that is in the set I recently bought. And, I am pleased to report, it actually works as intended:

map with button on floor to right of the red dot

Wild Idea

So this little spike tells me that we could store dungeon contents textures in TextureFinders, which would allow that resource line to be more like this:

    resource = self.texture_finder.full_name('button_up')

However. The current TextureFinder instance(s) are in RoomView instances. (This means we are creating many of them, by the way.) For overall dungeon content, we’ll want a similar capability at the DungeonView level. This has given me a glimmer of a wild idea. Suppose that a TextureFinder had an instance variable backup, which, if present, was another TextureFinder. Then we could put default textures, or commonly-used textures, into one Finder, and then, say, in a room, create a local Finder that only defined the specialized textures needed in that Room, and deferred other references up to the backup or maybe parent Finder.

That might be quite useful.

And what about a kind of TextureFinder object that didn’t just position a tile at a given cell? What if we had a ContentName object with some key, ‘Chest’, that instead of returning a standard floor tile, returned a Chest tile, and added it to the dungeon’s content at that point automatically?

Too speculative, too soon. We should work out how we want to define random and non-random content, and then see what it has in common with random flooring, and work from there.

It is important—and difficult for me, apparently—to remember that the real basis for the current TextureFinder is to provide correctly formatted tiles to place on the borders. It was devised to support border-describing strings like EWS and return a tile that shows a border on east, west, and south sides. We can deviate from that, and perhaps we should, but while TextureFinder as it stands might be a part of the dungeon drawing and content capability, it doesn’t have to do everything.

If I were to try to figure out everything and put it into TextureFinder, I would have no good reason to suppose that what I did would be suitable for those other purposes. I find that I do better to add generality when I need it, not before. Quite often, that generality can be added, not by adding code, but by removing restrictions on existing code.

For me that works better. And for now, I plan to stop.

Again, we’ll work on some of the other areas