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"

Building Tetris in Pygame – Part III

# 005 Actual Code

Having just talked about testing, we’re going to take a short break and get a window on the screen. I’m not writing unit tests for this — it’s theoretically possible, but in the end simply running the program is a simpler way to find out if it works.

tetris.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import pygame

from pygame.locals import *

def terminate(error=0):
    pygame.quit()
    sys.exit(error)

def main():
    pygame.init()
    pygame.display.set_mode((400, 500))

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

if __name__ == '__main__':
    main()

This puts a blank, black window on screen until the user closes it. Not terribly impressive, but we have to start somewhere. Note that I’ve broken the cleanup code down into a separate terminate() function. While the interpreter will clean up after you when you exit, it’s still good style to clean up as many resources as you can before closing. This way, we can quit out easily from anywhere, without worrying about cleaning up perfectly in each instance.

Run it, and we get a window. Yay. (If you don’t, go find out why.)

Now lets write something a bit more complicated:

# 006 The GameState Object

We talked about the GameState object last time, let’s work on implementing it. To start with, I’ll test something easy, the dimensions of the well.

First, we’ll commit our previous changes and set up a new branch:

$ git add tetris.py
$ git status
...
$ git commit -m "Basic event loop"
$ git branch game_state
$ git checkout game_state
Switched to branch 'game_state'

Now we can write our test:

tests/test_state.py

# -*- coding: utf-8 -*-

import unittest

from tetris import state, const

class TestGameState(unittest.TestCase):

    def setUp(self):
        super(TestGameState, self).setUp()
        self.state = state.GameState()

    def test_init_well(self):
        self.assertEquals(len(self.state.well), const.WELL_H)

        for row in self.state.well:
            self.assertEquals(len(row), const.WELL_W)

There are a few points to note:

1. I’m importing two separate modules — tetris.const is going to only hold what would otherwise be global constants. Putting them in a namespace makes things that much cleaner. (We’ll abstract out the screen size here as well.)
2. I’m not testing the well with a 10×20 grid and .assertEquals. That’s typo-prone, hard to adjust if the spec changes, and a lot more work.
3. GameState.init_well() will be called on the initialization of the GameState object. There’s no need for it at any other time, so we can assume it’s not meant to be called by external code.

Let’s plug it into the test framework and check that the test fails:

run_tests.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from lib.test_runner import ModuleTestRunner

from tests.test_state import TestGameState

suite = ModuleTestRunner()

suite.addTestList("State", [TestGameState('test_init_well'),
                            ])

if __name__ == '__main__':
    suite.run()

Create tetris/state.py and tetris/const.py, just to shortcut one round of errors, and run our test suite:

$ touch tetris/state.py tetris/const.py
$ ./run_tests.py
Running tests:
STATE:      E

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

In test_init_well (tests.test_state.TestGameState)
Traceback (most recent call last):
  File ".../tetris/tests/test_state.py", line 12, in setUp
    self.state = state.GameState()
AttributeError: 'module' object has no attribute 'GameState'

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

1 tests run in 0.000260 seconds, 0 failures, 0 skipped, 1 unexpected errors.

Imagine that, our empty files are not enough to pass.

This is not a treatise on TDD, and this post is going to be long already, so I’m not going to show all my iterations and will just skip ahead to the code that passes:

tetris/const.py

# -*- coding: utf-8 -*-

SCR_W = 400
SCR_H = 500

WELL_W = 10
WELL_H = 20

tetris/state.py

# -*- coding: utf-8 -*-

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

    def init_well(self):
        self.well = []
        for y in xrange(const.WELL_H):
            row = []
            for x in xrange(const.WELL_W):
                row.append(0)
            self.well.append(row)

Passing test and git commit:

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

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

1 tests run in 0.000146 seconds, 0 failures, 0 skipped, 0 unexpected errors.
$ git add .
$ git status
...
$ git commit -m "Begin implementation of GameState class, init_well"

(Yes, commit after *every* red/green cycle. Local commit histories are easily cleaned up before a push, and it will make rolling back mistakes that much easier.)

Next, let’s set up our scoring and levels. Modern Tetris games have a variety of different scoring algorithms, with bonuses for clearing multiple lines. For now, we’re going to keep it simple: one point per line cleared, and a level up every 10 points.

Again, starting with some basic tests:

tests/test_state.py

  # ... in TestGameState
    def test_init_score_level(self):
        self.assertEquals(self.state.score, 0)
        self.assertEquals(self.state.level, 1)

    def test_line_cleared(self):
        self.state.line_cleared()
        self.assertEquals(self.state.score, 1)
        self.assertEquals(self.state.level, 1)

        for i in xrange(10):
            self.state.line_cleared()

        self.assertEquals(self.state.score, 11)
        self.assertEquals(self.state.level, 2)

(Don’t forget to add the tests to run_tests.py)

Passing these tests is simple:

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
  # ...

    def line_cleared(self):
        self.score += 1
        if self.score % 10 == 0 and self.score > 0:
            self.level += 1

Another test run and git commit…

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

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

3 tests run in 0.000270 seconds, 0 failures, 0 skipped, 0 unexpected errors.
$ git add .
$ git status
# On branch game_state
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   run_tests.py
# modified:   tests/test_state.py
# modified:   tetris/state.py
#
$ git commit -m "GameState score/level updates"

There is more to be done with GameState, so we will be back to it next time.

Building Tetris in Pygame – Part II

# 003 Data Structures

Before we delve any deeper, it’s a good idea to stop and think about the data structures we’ll need to manage our game.

Ignoring UI cruft for now, the core of Tetris is a 10×20 well, and seven tetriminoes. For the sake of simplicity, it’s probably easiest to hold this in 2D arrays of numbers, like so:

well = [
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    # 19 more rows...
]

S_piece = [
  [
    [1, 0],
    [1, 1],
    [0, 1],
  ],
  [
    [0, 1, 1],
    [1, 1, 0],
  ],
]

Within this grid, a 0 is empty space, while a number 1-7 is a block. By giving each tetrimino a different number for its blocks, we will be able to easily track which type of tetrimino a given block came from, and will be able to color it appropriately.

In addition, there is a fair amount of other data we must keep track of:

  • Current score
  • Current level
  • The current piece, and its coordinates
  • The next piece

The simplest solution I see is a GameState object containing the well, score, level, and references to our current and next pieces. The tetriminoes themselves will have a module of their own.

And with that, we have an idea of where to begin.

# 004 On Testing…

If you’re following along and are not familiar with Test-Driven Development(TDD), take the time now to go read up on it, both its support and criticism.

TDD is a topic that inspires holy wars on the same scale as OS preferences and static/dynamic typing. I don’t have a particularly strong opinion either way. However, TDD does help you catch stupid mistakes earlier, and encourages tidy code, so I’m going to use it for this project.

We’ll be using Python’s builtin unittest module for writing our test cases. To actually run the test suite, there are plenty of options available — nosetests, py.test, etc. I use a custom framework (details, for those interested, in another post), and will be building my tests accordingly. Those of you following along are, of course, free to use whatever you want.

To start, we’ll write a sanity check test to get our framework up and running:

# tests/test_sanity.py

import unittest

class TestSanity(unittest.TestCase):

    def test_fail(self):
        self.assertTrue(False)

    def test_pass(self):
        self.assertTrue(True)

Now, to actually run these tests with our framework, we need to add them to run_tests.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from lib.test_runner import ModuleTestRunner

from tests.test_sanity import TestSanity

suite = ModuleTestRunner()

suite.addTestList("Sanity", [TestSanity("test_fail"),
                             TestSanity("test_pass"),
                             ])

if __name__ == '__main__':
    suite.run()

Finally, we run the tests in the terminal:

$ chmod +x run_tests.py
$ ./run_tests.py
Running tests:
SANITY:     F.

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

In test_fail (tests.test_sanity.TestSanity)
Traceback (most recent call last):
  File ".../tetris/tests/test_sanity.py", line 8, in test_fail
    self.assertTrue(False)
AssertionError: False is not true

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

2 tests run in 0.000280 seconds, 1 failures, 0 skipped, 0 unexpected errors.

(Note: chmod +x makes run_tests.py executable, so we don’t have to prefix ‘python’ every time we want to run the suite.)

One failure, one pass, and our test suite is properly set up. Remove/comment out the import and references to TestSanity in run_tests.py. (If you’re using nosetests or another test-discovery framework, you will probably want to delete or rename test_sanity.py.)

Git commit:

$ git add .
$ git status
...
$ git commit -m "Setup test framework"

This post is getting long, so we’ll stop here for today. Next time we’ll get a basic window on screen, and start implementing the data structures mentioned earlier.

Building Tetris in Pygame – Part I

# 000 Introduction

My interest in programming grew from playing video games as a kid. I don’t think I’m at all unique in that respect. So it’s natural that when I learned to program, one of the first things I wanted to do was write a game. Lately I’ve been using the Pygame framework to move up from the terminal to graphics.

There are a lot of Pygame tutorials out there. However, by and large, they all show signs of serious code smell — abuse of globals, magic numbers, etc. Since these are generally short one-file projects that’s not a problem, but it quickly becomes an issue if you want to scale up.

I want to attempt to build games using solid practices — clean namespaces, TDD-style unit testing, and so on. This will be a “live” series; when I inevitably screw something up, I’ll show how I work around it. While this isn’t necessarily a tutorial per se, feel free to follow along. (Some knowledge of basic programming and Python syntax is recommended. Zed Shaw’s Learn Python The Hard Way should get you up to speed should you need it.)

We’ll start with a Tetris clone. Tetris is fairly complex, with plenty of common game issues (controlling speed, collision detection) without being obscenely hard. It’s also a highly addicting game, and what better way to celebrate when we’re done than a six-hour gaming binge? Continue reading

Using Nested Functions for Cleaner Recursion

Python is not a particularly friendly language for recursion — a finite stack and significant overhead on function calls often make alternate implementations more desirable. However, there are some problems that are difficult to handle gracefully any other way — tree structures, for example — and other instances where clarity is preferable to performance. They do still have a place, and where we use them we may as well try to make them as clean and elegant as possible. Fortunately, Python makes this rather easy.
Continue reading