For the November meeting of Front Range Pythoneers we did a bowling code kata. We worked as a group on the projector, but I also worked on my own version on my laptop. Here’s my code, which was fun to write. It’s a single file which contains its test cases and can be run on the command line or imported:

import unittest

class Frame(object):
    def __init__(self, tenth=False):
        self.rolls = []
        self.tenth = tenth

    def full(self):
        if len(self.rolls) >= self.max_rolls():
            return True
        if self.tenth:
            has_special = all([roll in ('X', '/') for roll in self.rolls])
            return len(self.rolls) == 2 and not has_special
        else:
            return self.strike()

    def roll(self, score):
        if self.full():
            raise RuntimeError('attempted to record a roll on a full frame')
        self.rolls.append(score)

    def pins(self):
        if len(self.rolls) == 0:
            return 0
        if self.strike() or self.spare():
            return 10
        else:
            return sum([int(roll) for roll in self.rolls])

    def first_roll_pins(self):
        if len(self.rolls) == 0:
            return 0
        elif self.rolls[0] == 'X':
            return 10
        else:
            return int(self.rolls[0])

    def score(self, subsequent_frames):
        if self.tenth:
            return self.tenth_frame_score()

        score = self.pins()
        if self.strike():
            score += sum([frame.pins() for frame in subsequent_frames])
        elif self.spare():
            if len(subsequent_frames) > 0:
                score += subsequent_frames[0].first_roll_pins()
        return score

    def tenth_frame_score(self):
        return min(Game(''.join(self.rolls)).score(), 40)

    def strike(self):
        return len(self.rolls) > 0 and self.rolls[0] == 'X'

    def spare(self):
        return len(self.rolls) > 0 and self.rolls[-1] == '/'

    def max_rolls(self):
        return 3 if self.tenth else 2

class Game(object):
    def __init__(self, roll_scores=''):
        self.frames = []
        for score in roll_scores:
            self.roll(score)

    def roll(self, score):
        if len(self.frames) == 0 or self.frames[-1].full():
            tenth = len(self.frames) == 9
            self.frames.append(Frame(tenth))
        self.frames[-1].roll(score)

    def score(self):
        return sum(self.frame_scores())

    def frame_scores(self):
        frame_scores = []
        for frame_index in xrange(len(self.frames)):
            frame = self.frames[frame_index]
            subsequent_frames = []
            if frame.strike() or frame.spare():
                subsequent_frames = self.frames[frame_index+1:]
                added_frames = 1
                if frame.strike() and len(subsequent_frames) > 0:
                    added_frames = 2 if subsequent_frames[0].strike() else 1
                subsequent_frames = subsequent_frames[:added_frames]
            frame_scores.append(frame.score(subsequent_frames))
        return frame_scores

class GameTest(unittest.TestCase):
    def test_initial_strike(self):
        self.assertEqual(Game('X').score(), 10)

    def test_two_strikes(self):
        self.assertEqual(Game('XX').score(), 20+10)

    def test_three_strikes(self):
        self.assertEqual(Game('XXX').score(), 30+20+10)

    def test_strike_spare_strike(self):
        self.assertEqual(Game('X9/X').score(), 20+20+10)

    def test_strike_strike_spare(self):
        self.assertEqual(Game('XX9/').score(), 30+20+10)
        self.assertEqual(Game('XX9/71').score(), 30+20+17+8)

    def test_perfect_game(self):
        game = Game('X'*12)
        self.assertEqual(len(game.frames), 10)
        self.assertEqual(game.score(), 300)

    def test_made_up_game(self):
        game = Game('X907/818/X70070/72')
        self.assertEqual(len(game.frames), 10)
        self.assertEqual(game.score(), 132)

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

I think that the tenth frame calculation is incorrect. My limited understanding of bowling slowed me down a fair bit. I got an object system that I’m fairly happy with, though!

I had some issues upgrading to Django 1.2.1, and needed to roll Django back to 1.1.2. I searched for “Downgrading Django”, and didn’t find instructions, so now that I’ve figured it out, I’m posting instructions here.

First, if you’re using easy_install, I suggest switching to pip. It has more features, is better designed, and uses the same repositories, so it’s easy to upgrade. To install it, type sudo easy_install pip.

To install Django 1.1.2, type sudo pip install Django==1.1.2. When I ran this, pip automatically removed the newer version of Django, and the two glitches I was encountering with the admin interface went away. Once I figure out what caused the glitches, I’ll upgrade back to Django 1.2 so I can take advantage of its new features.

After using virtualenv for months, I finally got around to putting my main virtualenv into my bash profile. It’s really simple. All I had to do was add the following to ~/.bash_profile:

# python virtualenv
source /Users/ben/virtualenv/pearl/bin/activate

This worked, but there was one thing I wanted to turn off. The activate script that virtualenv creates adds the name of the current virtualenv to the front of the prompt. This is useful when switching between virtualenv directories, but it’s not very useful to me when I’m on the default virtualenv. So I moved my prompt definition to the bottom of the file, so it gets executed after the source command above.

# prompt (moved after virtualenv so it isn't shown)
PS1="\[\e[31;40m\]\@ \[\e[32;40m\]\w \[\e[36;40m\]\$\[\e[0m\] "

By the way, to keep the prompt I use the most often short, I set my own colors. It’s easy for me to tell that it’s my main shell, and since it’s my main shell, I know the username and hostname associated with it.

On the microblogging site twitter, a blog post is called an update, or informally, a tweet. When someone copies a tweet and posts it to their twitter feed, it’s called a retweet. Sometimes opinions are retweeted by those who share them, other times it’s information, and other times it’s silly memes. This evening, a local friend of mine on twitter started a meme by saying “tweet.” and asking for a retweet. One person who retweeted it got his message retweeted.

retweets

I know I’m not the first person to graph retweets, but being curious as to what this particular graph would look like, I decided to do a graph in Python as a programming exercise. I did it using a GraphViz library. The code is here:

import pydot

graph = pydot.Dot('rt', graph_type='digraph')
tweeps = ('tysoncrosbie', 
           ('phxreguy', 
             ('sbowerman',
               ('phxwebguy', 'leaky_tiki'),
               'refriedchicken')), 
           'vhgill',
           'Yartibise')

def add_edge(source, dest):
  graph.add_edge(pydot.Edge(source, dest))

def first_flat(tree):
  if isinstance(tree, tuple):
    return first_flat(tree[0])
  else:
    return tree

def find_edges(tree):
  if isinstance(tree, tuple):
    source = tree[0]
    for dest in tree[1:]:
      add_edge(source, first_flat(dest))
      find_edges(dest)

find_edges(tweeps)
graph.write_png('rt_graph.png')

The code takes a nested list structure (a tree) and produces edges from it, which can be graphed by GraphViz. It uses pydot, a GraphViz library for Python. Here is the resulting image:

rt_graph1

Observations:

  • Python doesn’t have a built-in list flattening function. This was irritating. Ruby’s is Array#flatten. It would have been so much nicer to have been able to grab the first element of a flattened list rather than write the first_flat function or copy/paste a list-flattening function from the Internet.
  • pydot is really simple to use. I liked how it could produce a png file. I was planning just to have it create a dot file and then use GraphViz to create a png, but when I saw the write_png function I decided to just use that.
  • GraphViz has reasonable defaults. It produced a nice-looking graph on the first try. I think that’s a big part of why GraphViz is as popular as it is.

I was hoping to have a little more to show for tomorrow’s Python Interest Group meeting, but this will have to do!