I think we’ll take a cut at converting from pygame to Arcade today. I have a tentative idea, and I’m prepared to fail spectacularly.

We’ll look at our calls to pygame shortly. I think there are few of them. Based on what we see, we’ll decide how to do this conversion. But I have a tentative plan in mind, which, if all goes as it usually does, may last little longer than it takes me to type it in here. It goes like this:

  1. Move all the pygame drawing code to one class, which will just have a few methods, as we really only draw rectangles and lines.
  2. Create a new file with a main program in it, into which we’ll paste in and edit the Arcade boilerplate.
  3. Probably create a arcade.View subclass, inside the new main file.
  4. Either create a similar drawing class to the one in #1 above, or just edit that one to use Arcade.
  5. One of:
    • profit!
    • refactor
    • scrap this and try again.

Let’s take a look at the reality of pygame drawing in our current program. In main, after creating the layout, there are just these lines:

    view = DungeonView(dungeon)
    view.main_loop()

There goes our plan! We’ll either edit DungeonView or create a new one, not a new main, depending on what we find next. On to DungeonView:

class DungeonView:
    def __init__(self, dungeon, testing=False):
        self.dungeon = dungeon
        self.key_lock = None
        if testing:
            return
        pygame.init()
        pygame.display.set_caption("Dungeon")
        self.screen = pygame.display.set_mode(
            (screen_width, screen_height))
        self.clock = pygame.time.Clock()

    def main_loop(self):
        running = True
        moving = False
        background = "gray33"
        color = "darkblue"
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.KEYDOWN:
                    self.handle_keydown(event)
                elif event.type == pygame.KEYUP:
                    self.handle_keyup(event)
            self.screen.fill(background)
            # self.draw_grid(color)
            self.draw_rooms()
            self.draw_contents()
            self.clock.tick(60)
            pygame.display.flip()
        pygame.quit()

    def draw_rooms(self):
        for room in self.dungeon.rooms:
            RoomView(self.dungeon, room).draw(self.screen)

    def draw_contents(self):
        adventurer_cell = self.dungeon.player_cell
        cx = adventurer_cell.x*cell_size + cell_size//2
        cy = adventurer_cell.y*cell_size + cell_size//2
        pygame.draw.circle(self.screen, "red", [cx, cy], cell_size//2)
        x,y = adventurer_cell.x, adventurer_cell.y
        txt = f'({x},{y})'
        font = pygame.font.SysFont("monospace", 20)
        surf = font.render(txt, True, 'white')
        self.screen.blit(surf, (cx, cy - cell_size))

There are more methods in here, draw_grid, which we have already stopped using, plus the keyboard handling code. No matter, this is where much of the action will be.

Now let’s review one or more of our Arcade examples, to get a sense of what needs to be done at the beginning.

There are a few variations in the samples I have. Here is one that’s fairly typical:

def main():
    """ Main function """
    # Create a window class. This is what actually shows up on screen
    window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)

    # Create the GameView
    game = GameView()

    # Show GameView on screen
    window.show_view(game)

    # Start the arcade game loop
    arcade.run()

There are other versions that seem to set up the window inside the view. This one seems simple, and I think we’ll work from here. And I think what we should do is create a new class, might as well be GameView, that will incrementally use or replicate what we need.

I would really like to do this by refactoring the existing views, but right now that seems impractical.

I think we’ll try to break things for a while. Let’s try just banging on it to see what explodes. The dungeon code is green and committed. We can just hammer it a bit. However …

In some examples, their GameView inherits from arcade.Window, and in others, from arcade.View. I suspect View is better. Let’s just hammer:

class DungeonView(arcade.View):
    def __init__(self, dungeon, testing=False):
        super().__init__()
        self.dungeon = dungeon
        self.key_lock = None
        if testing:
            return

PyCharm installs arcade and reminds me to do the super init. I think we have to have an on_draw method so I’ll put one in.

    def on_draw(self):
        self.clear()

I haven’t defined a window, so I do this much in main:

main.py
    ...
    dungeon.place_player_at(chosen_cell)
    window = arcade.Window(screen_width, screen_height, 'Caveat Emptor')
    view = DungeonView(dungeon)
    window.show_view(view)
    arcade.run()

I get a black squareish window with the heading “Caveat Emptor”. Woot! The first thing that our old loop does is deal with the keys, which we’ll ignore. Then it calls draw_rooms. That’s this:

    def draw_rooms(self):
        for room in self.dungeon.rooms:
            RoomView(self.dungeon, room).draw(self.screen)

And in RoomView?

class RoomView:
    boundary_colors = {"open":"black", "partition":"green", "door":"white", "wall":"red"}

    def __init__(self, dungeon, room):
        self.dungeon = dungeon
        self.layout = dungeon.layout
        self.room = room

    def draw(self, screen):
        cells = set(self.room.cells)
        for cell in cells:
            self.draw_colored_cell(cell, screen)
            self.draw_border(cell, screen)

    def draw_colored_cell(self, cell, screen):
        dx = cell.x * cell_size
        dy = cell.y * cell_size
        pygame.draw.rect(screen, self.room.color,
                         (dx, dy, cell_size, cell_size))

We’ll ignore draw_border for now. I’ll comment out that call and edit draw_colored_cell, if I can, to draw a rectangle via arcade. I code just this much:

class DungeonView:
    def on_draw(self):
        self.clear()
        self.draw_rooms()

    def draw_rooms(self):
        for room in self.dungeon.rooms:
            RoomView(self.dungeon, room).draw(None)

class RoomView:
    def draw(self, screen):
        cells = set(self.room.cells)
        for cell in cells:
            self.draw_colored_cell(cell, screen)
            # self.draw_border(cell, screen)

    def draw_colored_cell(self, cell, screen):
        dx = cell.x * cell_size
        dy = cell.y * cell_size
        arcade.draw_rect_filled(arcade.rect.XYWH(dx, dy, cell_size, cell_size),
                                arcade.color.DARK_BLUE)

And this is what I get

dark blue dungeon layout with no borders

I think the hardest part of this may turn out to be choosing colors, because arcade has all those fancy color names like CORN and ALABAMA_CRIMSON, not your manly YELLOW and RED.

I think I’ll try converting the borders code. It starts like this:

    def draw_border(self, cell, screen):
        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)
            pygame.draw.line(screen, color, start, finish)

I take the easy way out on color, for now:

    def draw_border(self, cell, screen):
        borders: BorderList = self.layout.get_borders(cell)
        for border in borders:
            start, finish = border.line_coordinates(cell.xy, cell_size)
            color_name = border.select_boundary(self.boundary_colors)
            if color_name == "black":
                color = arcade.color.BLACK
            elif color_name == "partition":
                color = arcade.color.GREEN
            elif color_name == "door":
                color = arcade.color.WHITE
            else:
                color = arcade.color.RED
            arcade.draw_line(*start, *finish, color)

We get an interesting result, with walls offset from the flooring:

dungeon with borders offset from cell background

I suspect that I know what is up here. I suspect that in the XYWH rectangle, X and Y are the rectangle center. I’ll check the API. Yes. We can use LBWH (left, bottom, width, height). Try that.

dungeon with correct borders

So that’s perfect, except perhaps for tuning the colors. And we even found that arcade, despite offering FUSCHIA as a color, does allow the more macho colors like RED. I freely grant that I do not know what color FUSCHIA is and I’m gonna try it right now.

SCREEEEE! It’s some kind of hot pinky purpley color. My eyes, they burn! Back to GRAY.

I think we’re done here.

Summary

Well, all my good ideas kind of fell by the wayside. Instead, a few very small patches to the existing code resulted in our dungeon displaying (without Intrepid Adventurer Dot) on the arcade software rather than pygame.

I will take a well-deserved break and continue this next time. I’m committing this code: I think we can refactor from here as needed. And we will want to do some refactoring at some point. We’re recomputing the rectangles and borders on every on_draw, and drawing them one at a time. We will surely want to stuff the rectangles into a sprite list or whatever its called, and draw them as a batch. I do not expect that we’ll go to the extreme of using that super high speed scheme we looked at yesterday. That seems to be useful only in very graphics-intensive apps.

There were no serious issues along the way. I had a syntax error due to a word that got deleted somehow, and I forgot that I had no screen member to pass to the DungeonView, which doesn’t want it anyway. And I got an error early on because I didn’t have a window open before trying to use it: window creation line had to be moved up one.

If we were further along with a lot more drawing going on, I think it might make sense to build a little shim object that accepted pygame-style drawing calls and translated them to arcade ones. Then where we had pygame.whatnot, we’d just do self.shim.whatnot instead, and work out the arcade issues in the shim.

Here, we have so little graphics going on, we don’t need it. It might still be worth doing, just to centralize our arcade stuff. We’ll keep the idea in mind. In essence, we’d be defining our own little graphics interface and then implementing it using arcade. In a large system, relying on some third-party software, that sort of thing is often worth having done.

I say “having done”, because almost no one ever does it before it’s needed, because no one really expects to replace their database or game platform, so when the demand comes along, it’s often too late. Putting the shim in then is often the best one can do.

Here, we just don’t need it. And I’m quite sure we won’t need it again, because I’m not mad enough to cal for a third game platform. Am I?

Next time, we’ll do Dot, then look around and see how to clean this up, and then maybe try to make something like a game out of it.

See you then!