# 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.
#!/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:
# -*- 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:
#!/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:
# -*- coding: utf-8 -*- SCR_W = 400 SCR_H = 500 WELL_W = 10 WELL_H = 20
# -*- 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:
# ... 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.