PyGame: Treasure!

20 February 2012

Want to see where I am now? Here's the code! 

Setting some defaults

Now that I was hip deep in moving my little dude around, I realized that I was going to need some defaults. In games, nothing is a given. You might need to make the board bigger, smaller, change the size of graphics, or even swap out colors.

BLACK = (0,0,0)
WHITE = (255, 255, 255)
COLUMNS = 16
ROWS = 21
TREASURES = 10
TILE_SIZE = 48

Had I been less eager, I would have started off with defaults written out. Seeing as how this was only the second session, however, I think I can be forgiven.

Since a rogue-like is a fairly basic game, I didn't need too many defaults: a few colors, columns and rows, and the size of the tiles for movement.

Colors in PyGame

Colors in PyGame are set through triads of red, blue, and green. Colors are the first thing I recommend you go ahead and add to defaults, as they can be annoying to puzzle out later.

I don't have any specific tool that I use to calculate RGB. I tend to use what's at hand. When I'm using my Mint box, I use Gimp. When I'm on my Mac, I pull up any one of a million, billion websites that do color conversions. What I'm looking for is RGB values:

A screen shot of a Gimp color picker, with the RBG values highlighted

I can take those, and make some default colors that will make reading the rest of my code so much easier.

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

This way, if I decide later on that I want my whites to be off-white for usability reasons, I just have to change one value.

Why go binary?

I made a major change at this point, and it wasn't adding treasure: I moved to base 2.

Why do this? I'd never really thought about base 2 numbers, aside from the fact that they would crop up in computing. It was just one of those things that you learn to expect. I can't recall ever having it explained to me, and it wasn't quite interesting enough for me to look into.

I stumbled into the reasoning by accident, while reading, of all things, a crazy novel about religious extremists and MMO gold farmers.

Having everything in base two makes everything halve more easily, which is something you do all the time when making a game.

Let's say I have a tile width of 50px (which is what I started with). If I want something to be half on that tile, I can move it 25px. But what if I want it to be on a quarter of that tile? Now we're at 12.5... and there is no such thing as half a pixel.

As soon as I was out of the bath (yes, I read in the bath), I switched my tiles to be 48x48. Now, we were divisible all the way down, something that would become very important later.

Treasure!

Now that I had a little guy moving around a dungeon, I decided he needed something to find in the dungeon. Treasure! Finally, I had my Treasure class do something besides sit there and look sad.

ALL_TREASURES = {
     "hat": "Quite cunning",
     "sqord": "Knock-off sword. Probably from Ikea.",
     "book": "What the hell are you going to do with this?"
   }
LONG_STRING = "X" * 50

class Treasure(object):
	''' Not implemented yet. 
	'''
	def __init__(self):
		k = ALL_TREASURES.keys()
		r = random.randint(0, ALL_TREASURES.keys().__len__()-1)
		self.title = ALL_TREASURES.keys()[r]
		self.description = ALL_TREASURES[self.title]
		

The Treasure class, in its pupa stage, only has two things: a title, and a description. Why not go hog wild? Why not give it attributes and effects and all the awesomesauce I can?

Rogue discovering a chest. This delights him. Caption: Finally! Purpose!

Why? Because there's more things to consider before I go crazy. Being a little bit clever with the titles and descriptions was really all the complications I want at the moment. After all I have to get them on the board.

I decided to go with adding another layer to the Map. It worked for making the darkness, so it should work for treasures. Adding random treasures turned out to be quite easy, as was clearning them. Huzzah! This is going to be super easy to write!

Viewing area and fog

I realized, after placing treasures, that my rogue needed to be able to see a bit better. I paused on the treasure feature to give my guy a field of view.

I modified my move function to clear the blocks around the rogue, but only slightly. I still wanted some shadows, so my cleared layer got some subtle tweaks. Now, if the rogue hadn't been in a block, or by it twice, that block would be shadowy.

Screenshot, showing rogue's path through the darkness. There are shadows where he can see, but it's still kind of dark. Caption, with arrow pointing to shadows: Sort of dark here!

The 'walked by it twice' bit was an accident, but one that I ended up keeping. I imagined my little guy squinting into the darkness, looking really, really hard for treasures, worrying about the day I decide to add monsters.

One thing that bit me here: if I moved my rogue to the bottom of the screen, the top would light up. I was completely confused until I read up on some subtlties with python's arrays. They accept negative numbers as the index.

>>> a = [1,2,3,4,5]
>>> a[-1] = 0
>>> a
[1, 2, 3, 4, 0]
Lesson learned: test for your bounds.

Back to treasures!

Now that my guy could actually see treasures, I could get back to making them awesome! Placing and picking up treasure was a cinch, but I was pretty sure that the player would want to know what they had picked up. Time to add alerts and an inventory.

I already had some dead area, due to the size I'd made my tiles and board. Whenever my guy ran across a treasure, it would be added to an inventory, and its title and description would be displayed in an alert area at the bottom of the screen.

I discovered very quickly that text likes to persist in PyGame. My hacky solution was to write over the existing text with a bunch of dark letters before writing something new.

# In the Game class...
def draw_alert(self, alert, color=WHITE):
		''' Draws the alert box at the bottom 
		'''
		self.alert = self.font.render(LONG_STRING, True, BLACK, BLACK)
		self.screen.blit(self.alert, (0, 790))
		try:
			pygame.display.flip()
		except:
			pass
		self.alert = self.font.render(alert, True, color, BLACK)

This is a horrible, horrible solution, and it fills me with shame. Works, though.

Is that bug still there?

Oh yeah. Found it yet?

Next time

We add monsters! 

Share

Related tags: pygame python

Comments

1 Joshua Grigonis says...

I see a few things that might be considered "bugs" depending on how anal you are.

The first thing I notice is at the start of the game, the dude isn't displayed.

The second thing I noticed is that I can only ever get 1 copy of treasure. This may be intended behavior.

The third thing I notice, after looking at the code is that you've mixed tabs and spaces.

The fourth thing I noticed is with the controls. I notice that if I rapidly hit a movement button 3 times, the dude seems to move 4-5 squares. Similarly, 4 taps and he can move as many as 7 squares. It gets progressively worse, if you rapidly punch in mixed directions.

The first 2 don't really bother me at all, the third one grinds my OCD, but the fourth one does actually make the game play feel buggy.

It seems like you should only call the 2 move routines on KEYUP, otherwise you are moving on every event, whether that's an up or a down. The weird part is putting an

if event.type == KEYUP:

in front of the moves didn't solve the problem. Somehow the move routine is sort of expecting to get called twice.

More details to come...

Posted at 11:14 p.m. on February 21, 2012

2 Joshua Grigonis says...

Here's the fix. It's all in the run routine, obviously, you can take out my print statements:

def run(self):
    ''' The main loop of the game.
    '''
    hor = 0
    vert = 0
    while 1:
        self.clock.tick(30)

        for event in pygame.event.get():
            if not hasattr(event, 'key'): 
                continue
            if event.type == KEYDOWN:
                if event.key == K_ESCAPE: 
                    sys.exit(0)
                if event.key == K_LEFT:
                    print "left"
                    hor = -TILE_SIZE
                if event.key == K_RIGHT:
                    print "right"
                    hor = TILE_SIZE
                if event.key == K_UP:
                    print "up"
                    vert = -TILE_SIZE
                if event.key == K_DOWN:
                    print "down"
                    vert = TILE_SIZE
            if event.type == KEYUP:
                self.map.move_monsters()
                self.move(hor, vert)
                hor = 0
                vert = 0

Posted at 11:24 p.m. on February 21, 2012

3 Joshua Grigonis says...

It's still not perfect though, as the dude kind of does some weird animation if you hold down 2 buttons at once, both moves are implemented when you let up either or both.

I'd say this is still significantly better, but maybe I can improve it.

Posted at 11:33 p.m. on February 21, 2012

4 Katie Cunningham says...

Ding ding ding! You found it! That confounded me for DAYS!

There are bugs that, as I code, are smoothed out :) There's more code coming, in which I fix some of these things. Movement is something I keep refining as I go, especially as I add animation during movement.

Posted at 11:34 p.m. on February 21, 2012

5 Joshua Grigonis says...

Ah, here you go, now we wipe out the other moves, and whichever direction you pressed last will be the one executed when you finally KEYUP:

    hor = 0
    vert = 0
    while 1:
        self.clock.tick(30)

        for event in pygame.event.get():
            if not hasattr(event, 'key'): 
                continue
            if event.type == KEYDOWN:
                if event.key == K_ESCAPE: 
                    sys.exit(0)
                if event.key == K_LEFT:
                    hor = -TILE_SIZE
                    vert = 0
                if event.key == K_RIGHT:
                    hor = TILE_SIZE
                    vert = 0
                if event.key == K_UP:
                    vert = -TILE_SIZE
                    hor = 0
                if event.key == K_DOWN:
                    vert = TILE_SIZE
                    hor = 0
            if event.type == KEYUP:
                self.map.move_monsters()
                self.move(hor, vert)
                hor = 0
                vert = 0

Posted at 11:41 p.m. on February 21, 2012

6 Marius Gedminas says...

(I'm not sure if these posts are moderated, or if my Android tablet's sorry excuse for a browser just lost my comment. If the first, please ignore this one. If the second, I'm sorry for this unnecessary introduction.)

Allow me to recommend random.choice(), which is often rather handy for games. If you use it, this:

k = ALL_TREASURES.keys()
r = random.randint(0, ALL_TREASURES.keys().__len__()-1)
self.title = ALL_TREASURES.keys()[r]
self.description = ALL_TREASURES[self.title]

can be contracted to just

self.title = random.choice(ALL_TREASURES.keys())
self.description = ALL_TREASURES[self.title]

Posted at 8:23 a.m. on February 22, 2012

7 Katie says...

@Marius - Oh, I'll have to make a note of that for the refactor!

@Joshua - And I think that might solve the last issue I had with movement. Thanks!

You might not see it for a bit, since I coded for quite a while before writing up these posts. I just pick a commit and write about what I was doing until that point.

Posted at 1:21 p.m. on February 22, 2012