Building Tetris in Pygame – Part IV

# 007 Tetris for Idiots

While it might seem that the next goal is to implement the various tetriminoes, I am going to leave that for later. Instead, I’m first going to work on “Tetris for Idiots”, that is, all pieces as a single 1×1 block. This is in keeping with the “smallest possible iteration” component of TDD — get a simplified version running before refactoring and filling in the full feature set. (This is sometimes called a “vertical slice”, in that we build the smallest possible part of each layer of the program from low-level event handling to high-level UI.)

Keeping that in mind, our state object needs a few more methods. As always, tests first:

tests/test_state.py

class TestGameState(unittest.TestCase):

    # ...

    def test_spawn_piece_valid_piece(self):
        # Freshly initiated, no pieces exist yet.
        self.assertEquals(self.state.next_piece, None)
        self.assertEquals(self.state.curr_piece, None)

        self.state.spawn_piece()

        # Check piece is valid (contains number 1-7)
        self.assertIn(self.state.next_piece[0][0], range(1,8))
        self.assertIn(self.state.curr_piece[0][0], range(1,8))

    def test_spawn_piece_get_correct_next(self):
        # Seed our pieces
        self.state.spawn_piece()
        expect_next = self.state.next_piece

        # Get our next piece -- did we get the right one?
        self.state.spawn_piece()
        self.assertEquals(self.state.curr_piece, expect_next)

The algorithm behind new pieces is simple, but more complex than what we’ve done so far:

1. Check that next_piece exists.
2. If it does:
– curr_piece = next_piece
– Create new next_piece
3. If it doesn’t:
– Create new curr_piece and next_piece

Pieces will be chosen randomly from a list.

tetris/state.py

import random

from tetris import const

class GameState(object):
    """Object to store the current state of a Tetris game."""
    def __init__(self):
        super(GameState, self).__init__()
        self.init_well()
        self.score = 0
        self.level = 1
        self.curr_piece = None
        self.next_piece = None

    # ...

    def get_new_piece(self):
        return [[random.choice(range(1,8))]]

    def spawn_piece(self):
        if self.next_piece == None:
            self.next_piece = self.get_new_piece()

        self.curr_piece = self.next_piece
        self.next_piece = self.get_new_piece()

Tests passing:

$ ./run_tests.py
Running tests:
STATE:      .....

------------

5 tests run in 0.000418 seconds, 0 failures, 0 skipped, 0 unexpected errors.
$ git add .
$ git status
...
$ git commit -m "Pieces implemented for 1x1 version"

Note that randomized functions like these can potentially pass their unit tests regardless of bugs (if I mistakenly enter range(1,9), it can take 10+ iterations to catch the bug). We could catch them more quickly by repeating the generate and check block, however as our test suite grows that would cause a bottleneck. Instead, we’ll leave it be — over the course of development the test suite should be run often enough to isolate any rare errors.

The other potential problem is distribution — assuring we get an even mix of pieces. At the moment, with our 1×1 pieces, this is not an issue, so we’ll put that aside for the moment.

With the basic GameState implementation now fleshed out, we can turn our focus elsewhere — the game screen.

# 008 Display

First, we’ll check in our game_state branch in git:

$ git status
# On branch game_state
nothing to commit (working directory clean)
$ git checkout master
Switched to branch 'master'
$ git merge game_state
Updating 55485a3..d2c7908
Fast-forward
 run_tests.py        |    9 +++++++-
 tests/test_state.py |   58 +++++++++++++++++++++++++++++++++++++++++++++++++++
 tetris/const.py     |    7 +++++++
 tetris/state.py     |   38 +++++++++++++++++++++++++++++++++
 4 files changed, 111 insertions(+), 1 deletion(-)
 create mode 100644 tests/test_state.py
 create mode 100644 tetris/const.py
 create mode 100644 tetris/state.py

Next, we need to check out a new branch for our display code:

$ git branch display
$ git checkout display
Switched to branch 'display'

To start, a few edits to our main function — capturing our display surface from .set_mode() and using our GameState class:

tetris.py

def main():
    pygame.init()
    displaysurf = pygame.display.set_mode((const.SCR_W, const.SCR_H))
    game_state = state.GameState()

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                terminate()

        # Game logic

        # Display
        draw_screen(displaysurf, game_state)

(Again, I’m not writing tests for UI and display.)

Our display code is going to require a bunch of new constants, so let’s go define those now:

tetris/const.py

# ...
BLOCK_SIZE = 20
WELL_PX_W = WELL_W * BLOCK_SIZE
WELL_PX_H = WELL_H * BLOCK_SIZE

MARGIN_LEFT = 50
MARGIN_TOP  = 50

C_BLACK = (  0,   0,   0)
C_DGRAY = ( 20,  20,  20)
C_LGRAY = (125, 125, 125)
C_WHITE = (255, 255, 255)

C_CYAN   = (  0, 255, 255)
C_BLUE   = (  0,   0, 255)
C_ORANGE = (255, 128,   0)
C_YELLOW = (255, 255,   0)
C_GREEN  = (  0, 255,   0)
C_PURPLE = (200,   0, 200)
C_RED    = (255,   0,   0)

C_LIST = [C_BLACK,
          C_CYAN,
          C_BLUE,
          C_ORANGE,
          C_YELLOW,
          C_GREEN,
          C_PURPLE,
          C_RED,]

Most of this is self-explanatory.

The first run at draw_screen():

tetris.py

def draw_screen(surface, game_state):
    surface.fill(const.C_BLACK)

    # Draw well (with border)
    pygame.draw.rect(surface, const.C_LGRAY, (const.MARGIN_LEFT - 5, const.MARGIN_TOP - 5,
                                              const.WELL_PX_W + 10, const.WELL_PX_H + 10))
    pygame.draw.rect(surface, const.C_DGRAY, (const.MARGIN_LEFT, const.MARGIN_TOP,
                                              const.WELL_PX_W, const.WELL_PX_H))

    # Draw blocks:
    y = 0
    while y < const.WELL_H:
        x = 0
        while x < const.WELL_W:
            curr = game_state.well[y][x]
            if curr != 0:
                current_color = const.C_LIST[curr]
                pygame.draw.rect(surface, current_color,
                        (const.MARGIN_LEFT + x * const.BLOCK_SIZE,
                         const.MARGIN_TOP + y * const.BLOCK_SIZE,
                         const.BLOCK_SIZE, const.BLOCK_SIZE))
            x += 1
        y += 1

    pygame.display.flip()

This requires a bit of explanation.

Filling the window and drawing the well are fairly straightforward (though note the third argument — Pygame Rects are tuples of (left, top, width, height)).

What may seem odd is our code for drawing blocks on screen.

I am intentionally not using for loops. For loops in Python are inefficient. Were I to use range(const.WELL_H), the interpreter would allocate a list of all numbers in the range. xrange is more efficient for memory, but incurs function call overhead on each iteration. In many cases, this inefficiency is irrelevant (such as the initialization of the well structure), but we want display code to run as quickly possible.

The C_LIST list avoids a long if/elif tree of all possible block colors. Indexing a list is also slightly cheaper than evaluating 7 booleans, but keeping the code concise is the main motivation.

We now have a problem — we have display code, but no way yet to put a block on screen, and therefore no way to test the block display code. Next time, we’ll fix that.

Today’s git commit:

$ git add .
$ git status
...
$ git commit -m "Basic display code"

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s