# 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:
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, range(1,8)) self.assertIn(self.state.curr_piece, 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.
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()
$ ./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:
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:
# ... 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():
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"