FTJ2014 Post-mortem I – Rendering

Fuck This Jam 2014 has wrapped up, so I can finally take the time to pop stack and talk about what I’ve learned in the process. Together with a former classmate, I built “Majo no Mahouki” [link], a side-scrolling shoot ’em up. The game itself leaves much to be desired (as to be expected from a 7-day jam), but I think some of the underlying framework code is worth discussing.

The game is built in C++ on top of the SFML library. SFML provides (among other things) a 2D graphics abstraction over OpenGL. The core interface to this abstraction is sf::RenderTarget::draw, which accepts a buffer of vertices and a RenderState (pointing to textures, shaders, etc.) Every call to draw results in a GL draw call. As such, these calls are *not* cheap.

However, the overhead is more or less the same between 4 vertices and 4000. Therefore, the solution is obvious: batching!

Majo handles this by creating a single VertexArray in the Level object (which handles the actual rendering for all in-game sprites). Each Entity, then, has a method with the signature virtual void render(sf::Vertex * quad) const. The level hands each entity a pointer to a quad in its vertex array, and the entity renders itself to it.

An example (default static sprite rendering):

void Entity::render(sf::Vertex * quad) const
{
    const double radius = m_collider.getRadius();
    const sf::Vector2f position = m_collider.getPosition();
    const sf::Vector2f halfsize(radius, radius);

    quad[0].position = sf::Vector2f(position.x - halfsize.x, position.y - halfsize.y);
    quad[1].position = sf::Vector2f(position.x + halfsize.x, position.y - halfsize.y);
    quad[2].position = sf::Vector2f(position.x + halfsize.x, position.y + halfsize.y);
    quad[3].position = sf::Vector2f(position.x - halfsize.x, position.y + halfsize.y);

    quad[0].texCoords = sf::Vector2f(m_spriteBounds.left, m_spriteBounds.top);
    quad[1].texCoords = sf::Vector2f(m_spriteBounds.left + m_spriteBounds.width, m_spriteBounds.top);
    quad[2].texCoords = sf::Vector2f(m_spriteBounds.left + m_spriteBounds.width, m_spriteBounds.top + m_spriteBounds.height);
    quad[3].texCoords = sf::Vector2f(m_spriteBounds.left, m_spriteBounds.top + m_spriteBounds.height);
}

This convention has issues, obviously: Level assumes that each entity only renders 4 vertices, and Entity assumes it is passed 4 vertices to render to. In hindsight, passing a reference to the array and an index into it may be a better calling convention.

In any case, we now have our vertices set up. However, this is not enough. While the vertices define position and texture coordinates, they do *not* define what texture is actually used (nor shaders, nor transforms, though Majo uses neither.) For proper batching, we must sort the list of entities by texture before rendering them to the vertex array:

void Level::render()
{
    m_projectileCount = 0;
    m_enemyCount = 0;

    m_verts.clear();
    m_verts.resize(m_entities.size() * 4);

    // Sort by texture type -- we want to batch drawing by texture.
    std::sort(m_entities.begin(), m_entities.end(), 
        [](const Entity * a, const Entity * b) {
        return a->getTextureId() < b->getTextureId();
    });

    // Scan the list, rendering to vertex buffer and 
    // recording counts of each texture type.
    for (int i = 0; i < m_entities.size(); ++i)
    {
        const TextureId texId = m_entities[i]->getTextureId();
        if (texId == TEXTURE_PROJECTILES) m_projectileCount++;
        else if (texId == TEXTURE_ENEMIES) m_enemyCount++;

        m_entities[i]->render(&m_verts[i * 4]);
    }
}

(A TextureId is Majo’s internal handle to a cached texture.)

Now Level has an array of vertices, and offsets within the array for each texture switch. (This particular method of recording the offset will not scale well, but Majo uses a total of 3 textures, so it works here.) The draw call, then, is straightforward:

void Level::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    if (m_entities.size() == 0) return;

    const sf::Vertex * projectileVerts = &m_verts[0];
    const sf::Vertex * enemyVerts = &m_verts[m_projectileCount * 4];
    const sf::Vertex * playerVerts = &m_verts[(m_projectileCount + m_enemyCount) * 4];

    states.texture = Resources::getTexture(TEXTURE_PROJECTILES);
    target.draw(projectileVerts, m_projectileCount * 4, sf::Quads, states);

    states.texture = Resources::getTexture(TEXTURE_ENEMIES);
    target.draw(enemyVerts, m_enemyCount * 4, sf::Quads, states);

    states.texture = Resources::getTexture(TEXTURE_PLAYER);
    target.draw(playerVerts, 4, sf::Quads, states);

    // Hacky UI drawing elided.
}

Thus, we can draw all Entities with only 3 calls to RenderTarget::draw. This is a fairly extreme example — while individually rendering entities is prohibitively expensive, it’s not necessarily worthwhile to batch *every* draw. However, it can be done.

Next time, we’ll explore how Majo handles sprite animation.

The Tale of trimZeros, or UI: Detail Counts

Continuing in a similar vein to my last post (Dragging Roads), another post on UI code. My CS professor has a few labs that we come back to and build on over the course of the year — one of these is an RPN calculator, which we built last semester (and which has become a coding kata for me in several languages), and which we are now developing GUIs for: first with Swing, and now on Android. (Spoilers: no, nothing in today’s post is actually related to Android UI in particular.)

Although the calculator code itself works on doubles, as you would expect, the UI stores the currently entered number as a String. This seems like a dirty hack (it is), but in order to support floating point entry and a backspace key, it’s the simplest means. The alternative, using exponentiation to place each digit in the right place on a double, has more nasty corner cases than are worth dealing with in the timeframe of a lab exercise. This String-backed UI has some consequences, namely that the user can input values that are either (a) invalid or (b) look wrong. Today’s post examines (b) — more specifically, removing unnecessary leading zeros.

Trimming zeros is easy, right?

private void trimZeros() {
    if (currentEntry.length() < 1) return;
    
    int i;

    for (i = 0; currentEntry.length() > i 
             && currentEntry.charAt(i) == '0'; ++i);

    currentEntry = currentEntry.substring(i);
}

(currentEntry is the String used to store the current entered number.)

This is somewhat more “clever” than it probably should be, but it’s still straightforward code — find the index of the first non-‘0’ character in the currentEntry string, then lop off the zeros all at once with a substring call.

However, it doesn’t quite preform how we want:

|  Input  |  Output  |
+---------+----------+
|  "0123" |   "123"  |
| "0.123" |  ".123"  |
|   "000" |      ""  |
|     "0" |      ""  |

Leading zeros on integers are trimmed properly, but zero values are reduced to empty strings, and decimal values lose the leading zero. With the exception of the empty strings, these are perfectly valid — feed them into #parseDouble and we’ll get the right double out, but we can do better.

private void trimZeros() {
    if (currentEntry.length() < 1) return;
    
    int i;
    int decimalPoint = currentEntry.indexOf('.');

    for (i = 0; currentEntry.length() - 1 > i 
             && currentEntry.charAt(i) == '0'; ++i) {

        if (i + 1 == decimalPoint) break;
    }

    currentEntry = currentEntry.substring(i);
}

Let’s check our table again:

|  Input  |  Output  |
+---------+----------+
|  "0123" |   "123"  |
| "0.123" | "0.123"  |
|   "000" |     "0"  |
|     "0" |     "0"  |

We’ve made two changes here: first, we never try to consume the last character in the string. Therefore, if we have an input of “00”, we only chop off the first ‘0’, leaving “0” — exactly what we want. Second, we look up the location of the decimal point (if there is one), and ensure we stop one character short of it, preserving a leading zero before the decimal point.

(An aside for those not well versed in Java: #indexOf returns -1 for a character not in the string. Because i starts at 0 and is only incremented, we can guarantee that i + 1 != -1 (overflow excepted — it’s not ever a problem here).)

Minor UI polish like this is not particularly difficult — the total diff between these two methods is 3 lines of code. However, issues like this exist in every system and piece of data that comes close to a user. It’s worth investing the time to hunt them down and fix them, though — users may not notice a well-behaved UI, but they will notice an awkward one.

Dragging Roads

I’m currently working on a game prototype (using C++ and SFML) that involves, among other things, dragging roads on a tilemap. I had a very specific behavior in mind for dragging, which took some work to get right. I’m going to document my results here.

# Plumbing

Before we can even start to worry about how the dragging works, we have to implement a dragging action. SFML exposes events for the mouse being clicked, released, and moved — these form the basis of our action.

//... Inside main loop

sf::Event event;
while (window.pollEvent(event))
{
    switch(event.type)
    {
    //... Other event handling
    case sf::Event::MouseButtonPressed:
        // Map global pixel location to a location relative to the window.
        sf::Vector2f clickPos = window.mapPixelToCoords(
            sf::Vector2i(event.mouseButton.x, event.mouseButton.y)
            m_map->getView());

        // Map this relative pixel location to a specfic tile.
        sf::Vector2i tilePos = m_map->mapPixelToTile(clickPos);

        // Tell the current tool where to start a drag.
        m_currentTool->startDrag(tilePos);
        break;
    case sf::Event::MouseButtonReleased:
        // End the active drag.
        m_currentTool->endDrag();
        break;
    case sf::Event::MouseMoved:
        sf::Vector2f mousePos = window.mapPixelToCoords(
            sf::Vector2i(event.mouseMove.x, event.mouseMove.y)
            m_map->getView());

        sf::Vector2i tilePos = m_map->mapPixelToTile(mousePos);

        // Update the current position of the drag.
        m_currentTool->updateDrag(tilePos);
        break;
    }
}

//...

This is all fairly straightforward: map the mouse coordinates to a particular tile and pass off to the Tool class. We now have the groundwork in place:

class Tool
{
private:
    bool isDragging;
    sf::Vector2i m_dragStart;
    sf::Vector2i m_dragCurrent;
    std::vector<sf::Vector2i> m_tiles;

    void updatePath();  // <-- The magic happens here.

public:
    void startDrag(const sf::Vector2i& pos)
    {
        if (m_isDragging) return;

        m_isDragging = true;
        m_tiles.push_back(pos);
        m_dragStart = pos;
        m_dragCurrent = pos;
    }
    void updateDrag(const sf::Vector2i& pos)
    {
        if (!m_isDragging) return;

        m_dragCurrent = pos;
        updatePath();
    }
    void endDrag()
    {
        if (!m_isDragging) return;
        m_tiles.clear();
    }
};

# The Algorithm

Our goal is to produce a list of tile coordinates that connect m_dragStart and m_dragCurrent. We want a linear path, with an optional dogleg, i.e.:

* * * * * *
          * *
            * *
              * *

Finally, if there is a diagonal segment, we want the tip to “rotate” as we extend the drag:

* * *
    * *
      *
    |
    v
* * *
    * *
      * *
    |
    v
* * *
    * *
      * *
        *

The first draft of the algorithm succeeds at everything except rotating:

void Tool::updatePath()
{
    // Clear output vector.
    m_tiles.clear();

    sf::Vector2i curr = m_dragStart;

     while (curr != m_dragCurrent) {
        m_tiles.push_back(curr);

        // Signed deltas
        const int dx = curr.x - m_dragCurr.x;
        const int dy = curr.y - m_dragCurr.y;

        if (abs(dx) > abs(dy)) {
            curr.x -= 1 * sign(dx);
        }
        else {
            curr.y -= 1 * sign(dy);
        }

    }

    m_tiles.push_back(curr);
}

(The sign function simply maps negative numbers to -1, positive numbers to 1, and 0 to 0.)

This is a very simple algorithm, but it doesn’t handle the diagonals exactly the way we’d like. The “rotation” of the diagonal end is determined by what branch is run when abs(dx) == abs(dy) — here, it’s the vertical shift, and as such the diagonal always ends with a vertical shift. This causes a “wiggling” motion when dragging diagonally, as the entire diagonal segment shifts back and forth to compensate. Not ideal.

The key is that as we extend either the vertical or horizontal delta for the entire path, we want the diagonal end to switch rotations. This leads to the following:

void Tool::updatePath()
{
    // Clear output vector.
    m_tiles.clear();

    sf::Vector2i curr = m_dragStart;

    // Manhattan distance from m_dragStart to m_dragCurrent.
    const int dist = abs(m_dragStart.y - m_dragCurrent.y) +
                     abs(m_dragStart.x - m_dragCurrent.x);
    const bool odd = dist % 2 != 0;

    while (curr != m_dragCurrent) {
        m_tiles.push_back(curr);

        // Signed deltas
        const int dx = curr.x - m_dragCurr.x;
        const int dy = curr.y - m_dragCurr.y;

        if (abs(dx) > abs(dy)) {
            curr.x -= sign(dx);
        }
        else if (abs(dx) < abs(dy)) {
            curr.y -= sign(dy);
        }
        else {
            if (odd) {
                curr.x -= sign(dx);
            }
            else {
                curr.y -= sign(dy);
            }
        }
    }

    m_tiles.push_back(curr);
}

In this modified version, we take the Manhattan distance between start and current tiles. Then, if this number is odd, we shift horizontally when the deltas are equivalent, and if it’s even we shift vertically. Which rotation is used for odd/even is arbitrary, it only matters that they are different. Now, as we drag along the diagonal, the rotation of the end tile changes as the mouse crosses the tile boundaries, preserving our previous path and eliminating the “wiggle”.

Profiler Evidence (That I am an Idiot)

In this post, I wrote:

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.

This has bugged me ever since, as I didn’t have any actual evidence as to whether Python’s range-based for loop is actually inefficient. (Spoiler: it’s not.)

Today I wrote some simple tests to profile:

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

import cProfile as profile

def for_range(n):
    for i in range(n):
        for j in range(n):
            # just some random calculations to make the loops actually do something
            i * j + j * i / 54321 

def for_xrange(n):    
    for i in xrange(n):
        for j in xrange(n):
            i * j + j * i / 54321 

def as_while(n):
    i = 0
    while i < n:
        j = 0
        while j < n:
            i * j + j * i / 54321 
            j += 1            
        i += 1


if __name__ == '__main__':
    
    # warm-up

    for i in xrange(10):
        for_range(100)
        for_xrange(100)
        as_while(100)

    profile.run("for_range(100)")
    profile.run("for_xrange(100)")
    profile.run("as_while(100)")

And run:

> python .\looptests.py
       104 function calls in 0.002 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000    0.002    0.002 <string>:1(<module>)
      1    0.002    0.002    0.002    0.002 looptests.py:5(for_range)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
    101    0.000    0.000    0.000    0.000 {range}


       3 function calls in 0.002 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000    0.002    0.002 <string>:1(<module>)
      1    0.002    0.002    0.002    0.002 looptests.py:11(for_xrange)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


       3 function calls in 0.003 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000    0.003    0.003 <string>:1(<module>)
      1    0.003    0.003    0.003    0.003 looptests.py:17(as_while)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Already it looks like my theory is wrong, but let’s get a clearer result, bumping n up to 10000:

 > python .\looptests.py
       10004 function calls in 22.219 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000   22.219   22.219 <string>:1(<module>)
      1   21.430   21.430   22.219   22.219 looptests.py:5(for_range)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  10001    0.789    0.000    0.789    0.000 {range}


       3 function calls in 22.508 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000   22.508   22.508 <string>:1(<module>)
      1   22.508   22.508   22.508   22.508 looptests.py:11(for_xrange)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


       3 function calls in 30.369 seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000   30.369   30.369 <string>:1(<module>)
      1   30.369   30.369   30.369   30.369 looptests.py:17(as_while)
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Well, then. As we can see, range() beats out both our manual while loop and the xrange() generator in terms of speed. And indeed, the “optimized” while loop performs terribly by comparison. I don’t have Heapy set up to check with, but I’m fairly certain we’d see xrange() come out with the most efficient memory usage.

This is not, overall, a surprising result. (Indeed, the only thing that really surprises me is that xrange() doesn’t appear to generate function calls in the profile.) But it serves as a reminder that even if you think you understand a system, you cannot accurately identify bottlenecks and inefficiencies without actual data.

Python Locale Heisenbugs

“Heisenbug” refers to a bug that only occurs under very specific circumstances. As I’ve been discovering this week, localization creates quite a few of these.

Localization is a tough problem to crack, and no language really gets it right. With the nearly infinite combinations of character encodings and regional formatting quirks, it’s nigh impossible to get it completely right. And as a result, there are always a few corners of each language that fail to work correctly under various locales.

Consider the following code:

try:
    with open(filename) as f:
        return f.read()
except IOError, ioerr:
    logger.error("Failed to open file: %s" % str(ioerr))

This has a few issues on a non-ascii system:
– it will not cope properly with the encoding of the file opened.
– it will not cope with any encoded characters in the error message from the system.

Here’s a good attempt at fixing it:

try:
    with codecs.open(filename, encoding="utf-8") as f:
        return f.read()
except IOError, ioerr:
    logger.error(u"Failed to open file: %s" % unicode(ioerr))

In reality, if an IOError is ever raised with a message containing encoded characters, the logging call fails, the exception propagates up the stack, and your program spews a traceback on stderr. This traceback, assuming it’s visible (and not redirected to /dev/null or similar) mentions only UnicodeDecodeError, not the original error opening the file.

What’s breaking? IOError.__unicode__ is implemented in a way that simply calls IOError.__str__ and attempts to convert the resulting string to unicode, using the ascii codec. So if the error message returned from the system is non-ascii, unicode() fails to convert it. Furthermore, unicode(), when called on an object, does not allow specifying a particular encoding.

So how do we fix this?

The intermediate fix, given that Linux systems use utf-8 encoding for error messages:

try:
    with codecs.open(filename, encoding="utf-8") as f:
        return f.read()
except IOError, ioerr:
    logger.error(u"Failed to open file: %s" % unicode(str(ioerr), 'utf-8'))

This works! …at least, until you try to run this code on a Windows machine. The standard encoding on Windows machines is not utf-8, and therefore we yet again get a UnicodeDecodeError.

The final, fixed code:

try:
    with codecs.open(filename, encoding="utf-8") as f:
        return f.read()
except IOError, ioerr:
    logger.error(u"Failed to open file: %s" % unicode(str(ioerr), sys.stderr.encoding))

Our best guess for the encoding in error messages is the encoding of stderr. In theory this may not be guarenteed, but this works across Linux and Windows, so we’ll call that good enough.

But wait, there’s more!

We’ve seen that messages from the system are suspect to encoding issues, but there are other instances where locale plays a critical but hidden role.

For example, take the function strftime(), a C function for formatting dates (taking locale into account). Python implements this via a method on date, time, and datetime objects.

Our initial, un-localized code:

>>> dt = datetime.datetime.now()
>>> print dt.strftime("%c")
Fri Apr 26 20:25:28 2013

The “%c” format specification formats the datetime according to the active locale, and returns a utf-8 encoded string. The following code will break, then, under any non-ascii locale:

logger.info(u"Timestamp: %s", dt.strftime("%c"))

When the datetime string is formatted into the unicode string, it is coerced to unicode by Python, using the ascii codec. As we’ve seen, this is a recipe for frustrating bugs.

Again, the solution is to specify the proper encoding:

logger.info(u"Timestamp: %s", unicode(dt.strftime("%c"), "utf-8"))

Complicating the issue, Python’s interpreter does not run under the system locale, instead running with the default ‘C’ locale — the result being that code that works in the interpreter may not necessarily work in a program:

>>> import datetime
>>> dt = datetime.datetime.now()
>>> print unicode(dt.strftime("%c"))
Fri Apr 26 20:51:00 2013
>>>
>>> import locale
>>> locale.setlocale(locale.LC_ALL, locale.getdefaultlocale())
'ja_JP.UTF-8'
>>> print unicode(dt.strftime("%c"))
Traceback (most recent call last):
  File "", line 1, in
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 4: ordinal not in range(128)

The lesson to be learned: If you care about localization, learn where your code is impacted by locale settings, and test against *non-ascii* locales. (No, “en_US.utf-8” doesn’t count). Python’s locale module makes this relatively straightforward.

Mocking for UI Controller Tests in PyQt4

What is Mocking?

Mocking is essentially taking a ‘heavy’ component — a UI element, database/file accessor — and swapping it out with a lighter component that only *pretends* to be the heavier component.  This is useful in cases where you don’t care about how the heavy component works, but need to test an object coupled tightly to it.  A unit test that does not require filesystem access or setting up a stack of GUI elements will run faster and possibly even give more relevant results.

UI Mocking

The main use for mocking is testing code coupled to the UI. This sometimes requires some design changes. For example, consider the following Qt4 window class:

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__(self)

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.actionLoad.triggered.connect(self.fileLoad)
        self.ui.actionSave.triggered.connect(self.fileSave)
        self.ui.actionExit.triggered.connect(self.fileExit)
        # Other triggers...
    # Logic code...

This class cannot easily be tested — any QtGui object requires that the entire application base be created first. This incurs serious overhead for very little gain — we’re interested in testing the logic code, not the rendering.

The solution is to break our logic out into a separate object. Similarly to the UI itself, both the window and the controller need to hold references to the other so that signals can be routed properly.

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__(self)

        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.controller = MainWindowController(self)
        self.ui.actionLoad.triggered.connect(self.controller.fileLoad)
        self.ui.actionSave.triggered.connect(self.controller.fileSave)
        self.ui.actionExit.triggered.connect(self.controller.fileExit)
    #...

class MainWindowController(object):
    def __init__(self, window):
        self.window = window

    def fileLoad(self):
        #....

The window can route signals from the UI through the controller reference, while the controller can manipulate the UI through the window.ui reference. Furthermore, we can now test the MainWindowController class by passing in a mock of the MainWindow class, eliminating the overhead of setting up the GUI.

So what do these mocks look like? Here are some examples from my current project:

class UiMock(object):

    def __init__(self):
        pass

class WindowMock(object):

    def __init__(self):
        super(WindowMock, self).__init__()
        self.ui = UiMock()

class ButtonGroupMock(object):

    def __init__(self, buttons):
        super(ButtonGroupMock, self).__init__()
        self.buttons = buttons

    def checkedButton(self):
        for button in self.buttons:
            if button.isChecked():
                return button
        else:
            return 0

class RadioButtonMock(object):

    def __init__(self):
        super(RadioButtonMock, self).__init__()
        self.checked = False

    def isChecked(self):
        return self.checked

These, obviously, don’t emulate the objects they’re mocking very deeply. This is intentional — in keeping with the spirit of TDD, we only need to mock the interfaces we actually use in our code. The ButtonGroupMock doesn’t worry about exclusivity, for example, as I’ve not needed it in my tests yet, and UiMock is just an empty namespace. The dynamic nature of Python means that I need only create enough interface to fool the calling code, and no more. (Mocking in static languages, such as Java, is much more involved.)

As a result, our test setup looks something like this:

class TestMainWindowController(unittest.TestCase):
    def setUp(self):
        win_mock = WindowMock()
        
        win_mock.ui.radio_button = RadioButtonMock()
        win_mock.ui.check_box = CheckboxMock()
        # etc...

Hopefully this makes testing UI just a little simpler

Building Tetris in Pygame – Part V

# 009 Spawning blocks

Last time we got our basic display code up and running, but before we can go any further we need a way to spawn pieces.

Let’s switch back to our game_state git branch:

$ git status
# On branch display
nothing to commit (working directory clean)
$ git checkout master
Switched to branch 'master'
$ git merge display
Updating d2c7908..e4bcf30
Fast-forward
 tetris.py       |   36 +++++++++++++++++++++++++++++++++++-
 tetris/const.py |   29 +++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+), 1 deletion(-)
$ git checkout game_state
Switched to branch 'game_state'
$ git merge master
Updating d2c7908..e4bcf30
Fast-forward
 tetris.py       |   36 +++++++++++++++++++++++++++++++++++-
 tetris/const.py |   29 +++++++++++++++++++++++++++++
 2 files changed, 64 insertions(+), 1 deletion(-)
$ git status
# On branch game_state
nothing to commit (working directory clean)

When we left off last, GameState still had no way of tracking the coordinates of the current piece. As always, test first:

tests/test_state.py

    # ...
    def test_spawn_piece_coords(self):
        self.state.spawn_piece()

        self.assertEquals(self.state.piece_x, 4)
        self.assertEquals(self.state.piece_y, 0)

(This test case may not be valid when we move up to the full tetriminos, but for now it will do.)

Add the test to run_tests.py, and run the test suite:

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

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

In test_spawn_piece_coords (tests.test_state.TestGameState)
Traceback (most recent call last):
  File ".../tetris/tests/test_state.py", line 58, in test_spawn_piece_coords
    self.assertEquals(self.state.piece_x, 4)
AttributeError: 'GameState' object has no attribute 'piece_x'

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

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

This is a pretty simple test to pass:

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

        self.piece_x = 4
        self.piece_y = 0

This is… getting ugly. I’ll need to keep this section in mind for later cleanup and refactoring. For now, though, we have more pressing problems.

Even if we modify our main function to call spawn_piece(), we won’t get any blocks on screen, because we don’t actually draw the active piece! Oops.

Let’s switch back over to the display branch and fix that:

$ git add .
$ git status
...
$ git commit -m "Spawn piece at coordinates"
$ git checkout master
Switched to branch 'master'
$ git merge game_state
Updating e4bcf30..44833c8
Fast-forward
 run_tests.py        |    1 +
 tests/test_state.py |    6 ++++++
 tetris/state.py     |    3 +++
 3 files changed, 10 insertions(+)
$ git checkout display
Switched to branch 'display'
$ git merge master
Updating e4bcf30..44833c8
Fast-forward
 run_tests.py        |    1 +
 tests/test_state.py |    6 ++++++
 tetris/state.py     |    3 +++
 3 files changed, 10 insertions(+)

First, a quick addition to main() so that we get a piece spawned:

def main():
    # ...
    while True:
        # ...
        # Game logic
        if game_state.curr_piece == None:
            game_state.spawn_piece()

        # Display
        draw_screen(displaysurf, game_state)

Now let’s add code for drawing the current piece:
tetris.py

    # ... in draw_screen()

    # Draw current piece:
    curr_left = const.MARGIN_LEFT + (game_state.piece_x * const.BLOCK_SIZE)
    curr_top = const.MARGIN_TOP + (game_state.piece_y * const.BLOCK_SIZE)
    curr_color = const.C_LIST[game_state.curr_piece[0][0]]

    pygame.draw.rect(surface, curr_color,
        (curr_left, curr_top, const.BLOCK_SIZE, const.BLOCK_SIZE))

    pygame.display.flip()

Run the game, and you should get a random colored block at the top of the well.

Finally, at long last, we’re ready to work on the actual game logic — moving pieces and afixing them to the board once they land. We will work on that next time, but for now we will get everything commited to git:

$ git add .
$ git status
...
$ git commit -m "Draw current piece."
 [display 3965dcd] Draw current piece.
 2 files changed, 15 insertions(+), 5 deletions(-)
$ git checkout master
Switched to branch 'master'
$ git merge display
Updating 44833c8..3965dcd
Fast-forward
 tetris.py       |   14 ++++++++++++--
 tetris/const.py |    6 +++---
 2 files changed, 15 insertions(+), 5 deletions(-)