Enter the Dungeon
Today, we’ll start populating our dungeon. We’ll start with an intrepid adventurer named Dot.
- Our story begins …
-
A lone intrepid adventurer named Dot finds herself in a random dungeon. She can move in any of the cardinal directions, except of course that she cannot walk through walls.
We’ll guide Dot by typing on the arrow keys, if things go as I intend. Key down and she moves if she can, then the key must go up before she can move again. At this moment, I don’t know what I think about more than one key down at a time, but I think we’d like to ignore all but the first.
Our own first step this morning will be to review and recall how keyboard events work in PyGame. We’ll be modifying our main loop:
class DungeonView:
def __init__(self, dungeon):
self.dungeon = dungeon
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
self.screen.fill(background)
self.draw_grid(color)
self.draw_paths()
self.draw_rooms()
self.clock.tick(60)
pygame.display.flip()
pygame.quit()
From the look of that, we want to see what other event.type we might encounter. We find that there are events KEYDOWN and KEYUP. The events can return key, mod, unicode and scancode. There are key names: K_UP, K_DOWN, K_RIGHT, and K_LEFT among many others.
With a bit of searching on my own site, I find a little patch of code that tells us what we need to know:
def main_loop(self):
running = not self._testing
while running:
for event in pygame.event.get():
if event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
elif event.type == QUIT:
running = False
...
Let’s do just a bit of design thinking here, to get a sense of what we will want in the future, and what we might want to do as our first few steps into the future.
I think that the Dungeon will be the main state-bearing object during game play, at least for now: it knows where all the rooms are. So it will also know where all the stuff is, including Intrepid Adventurer Dot. However, for some reason having to do with a tendency to over-design, or perhaps to follow some good advice I heard somewhere, I’ve already committed to there being “view” objects, specifically DungeonView and RoomView. We’ll have to work with those.
I can think of at least two ways to have it work: we could draw all the paths and rooms and then draw all the contents, including Dot, on top of that. Or we could draw each room or path and at that time, its contents.
I think for now, we’ll just have Dot know her coordinates in the dungeon, so we’ll take the approach of drawing contents last. And we’ll code something directly into the DungeonView, for now.
First step, let’s put Dot somewhere and make her display.
At the end of main, right before we enter the game loop, we’ll “populate” the dungeon. This will, I guess, be the process of placing all the goodies, including Dot. For now, only Dot.
main.py
...
path_room = Room(path, 'path')
dungeon.add_room(path_room)
dungeon.populate()
view = DungeonView(dungeon)
view.main_loop()
Need to write that:
class Dungeon:
def populate(self):
dot_room = random.choice(self.rooms)
dot_cell = random.choice(dot_room.cells)
self.adventurer_cell = dot_cell
Random cell in a random room. Now we need to display something. In DungeonView.
class DungeonView:
self.draw_paths()
self.draw_rooms()
self.draw_contents()
self.clock.tick(60)
pygame.display.flip()
pygame.quit()
def draw_contents(self):
adventurer_cell = self.dungeon.adventurer_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)
This isn’t as arcane as it may look, since we use a similar pair of expressions to calculate cell positions. Here we offset by half the size to get the center and draw a red circle of radius half the cell size. And we get this in our current excessively simple dungeon:

That’s pretty much exactly what we wanted. Let’s commit a save point.
We’ll divert to create a more interesting dungeon, since this setup was just there to show how our wiggly path logic looks. I’ll spare you the bandwidth of the picture, but that’s done, let’s do the moving code:
For now, let’s just send the key to the dungeon with a handle_key_up or handle_key_down method. We’ll sort things out once they’re wired up.
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
self.dungeon.handle_keydown(event)
elif event.type == pygame.KEYUP:
self.dungeon.handle_keyup(event)
Then:
class Dungeon:
def handle_keydown(self, event):
key = event.key
if key == pygame.K_RIGHT:
self.adventurer_cell = self.adventurer_cell.east()
elif key == pygame.K_LEFT:
self.adventurer_cell = self.adventurer_cell.west()
elif key == pygame.K_UP:
self.adventurer_cell = self.adventurer_cell.north()
elif key == pygame.K_DOWN:
self.adventurer_cell = self.adventurer_cell.south()
And:
def east(self):
return self.attempt_move(1,0)
def north(self):
return self.attempt_move(0, -1)
def south(self):
return self.attempt_move(0, 1)
def west(self):
return self.attempt_move(-1, 0)
def attempt_move(self, dx, dy):
return self.at_offset(dx, dy) or self
This nearly works. It allows Dot to move into available space. attempt_move needs to be a bit more stringent.
def attempt_move(self, dx, dy):
cell = self.at_offset(dx, dy)
if not cell or cell.is_available:
return self
return cell
Works as intended. We don’t get another key down on a given key until we lift that key, though you can in fact press and hold another arrow key if your fingers will perform. So, no surprise, we’ll need a little interlock to ensure that we don’t allow another down until we see a suitable up. That should be easy enough.
Here’s a video.
We’ll call it a morning. Commit: Intrepid Adventurer Dot can traverse the dungeon.
Summary
Clearly, this is little more than a spike, an experiment to see how things might work. We have learned, or re-learned, quite a bit:
- We found
KEY-UPandKEY_DOWNevents and access to the keys we care about. - We have a tentative decision to draw the dungeon contents after drawing everything else.
- We found a decent way to start Dot in a random location. In real dungeons, we will more likely have a defined start, but perhaps not always.
- We can move Intrepid Adventurer Dot and keep her from wandering outside the dungeon.
- And I learned that jagged rooms and paths are a pain to navigate compared to simple rectangular rooms and straight paths, because of the keyboard dancing that is required to switch between repeated single direction moves and zig-zag moves.
Certainly we’ll want more objects and more conditions and more everything, including a lot more good design, since what we have is very direct, quite ad-hoc, and not readily able to be generalized. But all that will come, assuming that we press forward with this little game for a while.
A good morning’s work, putting I.A.Dot in the dungeon and on the screen.
See you next time!