I spent most of week 2 at RC doing group activities, one of which was a Code Retreat lead by a resident, Emil Sit. The retreat was focused on TDD and we used Conway’s game of life as a playground. The idea was to pair with random people for short sessions (45 minutes each), and write tests and code that makes tests pass in ping-pong style – one person writes a unit test, another writes code for that test and another test etc. One of the main conventions was to delete the code as soon as a session finishes. As you will see below, I cheated and saved the code so I can blog about it 🙂

Session #0. Warm up, no constraints.

It’s hard to show the dynamic of ping-pong code writing here and I haven’t been able to save all the intermediate steps, there are only results of the whole session. Here are the tests:

import unittest
import game

class GameTest(unittest.TestCase):
    def setUp(self):
        self.game = game.Game()

    def test_update(self):
        self.game.board = [[False, True, False], [False, True, False], [False, False, False]]
        self.assertEqual(self.game.update(), [[False, True, False], [False, True, False], [False, False, False]])

    def test_rule1(self):
        self.game.board = [[False, True, False], [False, True, False], [False, False, False]]       
        self.assertEqual(self.game.update_cell(1, 1), False)
        self.game.board[0][1] = False       
        self.assertEqual(self.game.update_cell(1, 1), False)
        self.game.board[1][1] = False
        self.assertEqual(self.game.update_cell(1, 1), False)
        self.game.board[0][1] = True        
        self.assertEqual(self.game.update_cell(1, 1), False)

    def test_rule2(self):
        self.game.board = [[False, True, False], [False, True, False], [False, True, False]]
        self.assertEqual(self.game.update_cell(1, 1), True)
        self.game.board = [[False, True, False], [False, True, False], [False, True, True]]
        self.assertEqual(self.game.update_cell(1, 1), True)

    def test_rule3(self):
        self.game.board = [[True, True, True], [False, True, False], [False, True, False]]      
        self.assertEqual(self.game.update_cell(1, 1), False)
        self.game.board[1][1] = False
        self.assertEqual(self.game.update_cell(1, 1), False)

    def test_rule4(self):
        self.game.board = [[True, True, False], [False, False, False], [False, True, False]]        
        self.assertEqual(self.game.update_cell(1, 1), True)

if __name__ == '__main__':
    unittest.main()

And here’s the code for them:

class Game(object):
    def __init__(self):
        self.board = None

    def update_cell(self,i,j):
        live_neighbors = 0
        for row_delta in [-1, 0, 1]:
            for column_delta in [-1, 0, 1]:
                if not (row_delta == 0 and column_delta == 0):
                    current_cell = self.board[i+row_delta][j+column_delta]
                    if current_cell == True:
                        live_neighbors += 1
        if live_neighbors < 2:
            return False
        elif live_neighbors == 3:
            return True
        elif live_neighbors > 3:
            return False
        return self.board[i][j]

As you can see, we didn’t spend much time writing the code this time around. This ratio will be changing as each session adds a set of constraints.

Session #1. This time functions/methods should only be at most 3 lines long, this includes the unit tests. It’s easy to write one-liners in Python, but overall it changed the way we thought about tests and implementation. Here’s the tests:

import unittest
import game

class GameTest(unittest.TestCase):

    def test_rule_one(self):
        rule1_board = game.update_rule_one([[0,1,1], [0,0,0], [0,0,1]])
        self.assertEqual(rule1_board[0][1], 0)
        self.assertEqual(rule1_board[2][2], 0)

    def test_rule_two(self):
        board = [ [0,1,1], [0,1,0], [0,0,1] ]
        self.assertEqual(game.update_rule_two(board, board)[0][1], 1)
        self.assertEqual(game.update_rule_two(board, board)[0][2], 1)

    def test_rule_three(self):
        board = [ [1,1,1], [1,1,0], [1,0,1] ]
        self.assertEqual(game.update_rule_three(board,board)[1][0], 0)
        self.assertEqual(game.update_rule_three(board,board)[1][1], 0)


if __name__ == '__main__':
    unittest.main()

The code is still somewhat short compared to tests and it’s mostly because of Python’s ability to contain a lot of semantics in one-liner list comprehensions.

def count_neighbors(board, i, j):
    return sum(map(sum, board[max(0, i-1):min(len(board), i+2)][max(0, j-1):min(len(board[0]), j+2)])) - 1*board[i][j]

def update_rule_one(board):
    new_board = [[0]*len(board) for _ in range(len(board[0]))]
    return [[0 if count_neighbors(board, i, j) < 2 else board[i][j] for j in range(len(board[0]))] for i in range(len(board)) ]

def update_rule_two(old_board, updating_board):
    return [ [1 if old_board[i][j] == 1 and 2 <= count_neighbors(old_board, i, j) <= 3 else updating_board[i][j]  for j in range(len(old_board[0]))] for i in range(len(old_board)) ]

def update_rule_three(old_board, updating_board):
    return [[0 if count_neighbors()]]

Session #2. The dynamics changed substantially when the constraint of having no primitive data types passed between method boundaries was introduced. The tests:

import unittest
import game

class GameTest(unittest.TestCase):
    def setUp(self):
        self.board = game.Board()

    def test_neighbors(self):
        g = self.board.get_neighbors(1, 1)
        self.assertEqual(g, game.Neighbors([0]*8))
        g = self.board.get_neighbors(0, 0)
        self.assertEqual(g, game.Neighbors([0]*3))
        g = self.board.get_neighbors(0, 1)
        self.assertEqual(g, game.Neighbors([0]*5))

    def test_sum_neighbors(self):
        s = 0
        self.assertEqual(s, int(game.Board().get_neighbors(5, 5)))

    def test_rule_one(self):
        self.board.board[0][1] = 1
        self.board.board[1][1] = 1
        new_board = self.board.update()
        self.assertEqual(new_board[1][1], 0)

    def test_rule_two(self):
        self.board.board[0][1] = 0

if __name__ == '__main__':
    unittest.main() 

We ended up writing more code compared to the unit tests.

import random, itertools
class Neighbors(object):
    def __init__(self, neighbors):
        self.neighbors = neighbors
    def __eq__(self, other):
        return self.neighbors == other.neighbors
    def __int__(self):
        return sum(self.neighbors)

class Board():
    def __init__(self, m=10, n=10):
        self.board = []
        for i in range(m):
            row = []
            for j in range(n): row.append(0)
            self.board.append(row)

    def get_neighbors(self, i, j):
        rows = filter(lambda x: 0 <= x < len(self.board), [i - 1, i, i + 1])
        cols = filter(lambda x: 0 <= x < len(self.board[0]), [j - 1, j, j + 1])
        pairs = []
        for r in rows:
            for c in cols: pairs.append((r, c))
        output = map(lambda x: self.board[x[0]][x[1]], filter(lambda y: y != (i, j), pairs))
        return Neighbors(list(output))

    def update(self):
        newBoard = self.board
        for i in range(len(self.board)):
            for j in range(len(self.board[i])):
                print(int(self.get_neighbors(i, j)))
                if int(self.get_neighbors(i, j)) < 2: newBoard[i][j] = 0
        return newBoard

Session #3. Last session was most fun and kind of experimental. We actually had to vote on what constraint to impose and ended with no conditionals allowed. No conditionals tests:

import unittest
import game

class GameTest(unittest.TestCase):

    def setUp(self):
        self.world = game.World()

    def test_rule_one_loner(self):
        self.world.grid[0][1] = True
        print(self.world.grid)
        new_grid = self.world.update()
        print(new_grid)
        self.assertEquals(any(map(any, new_grid)), False)   

if __name__ == '__main__':
    unittest.main()

And no conditionals code:

class World(object):
    def __init__(self):
        self.n = 10
        self.grid = [[False for i in range(self.n)] for j in range(self.n)]

    def update(self):
        return [[self.next(i,j) for j in range(self.n)] for i in range(self.n)]

    def neighbors_of(self,i,j):
        coords = []
        for ix in range(max(0,i-1),min(i+2,self.n)):
            for jx in range(max(0,j-1),min(j+2,self.n)):
                coords.append((ix,jx))
        coords.remove((i,j))
        return coords

    def lonely(self,i,j):
        print("here")
        print([self.grid[neighbor[0]][neighbor[1]] for neighbor in self.neighbors_of(i,j)])
        return any([self.grid[neighbor[0]][neighbor[1]] for neighbor in self.neighbors_of(i,j)])

    def next(self,i,j):
        return self.grid[i][j] and not self.lonely(i,j) 

All in all, this was a great exercise. Both in pair programming and TDD. Mini-retrospectives on each session and a general one in the end were super-helpful. Thanks to Emil Sit and everyone I paired with.

Share →