Scrapheap Challenge at SPA2007, part 3: Name that Tune

SPA 2007 Scrapheap Challenge workshop
A Christmas Quiz Game. I want a quiz game to play at Christmas. The most important feature is that I don't want to have to prepare the quiz beforehand. The second most important feature is that the quiz should help avoid family arguments by keeping scores. Exactly how the game is to be played is part of the challenge. What is the quiz about? Is there a quizmaster? If not, how do players make their guesses? Do all players try to answer the same questions or do they take turns?

I have been writing a location-aware music player in my research project at Imperial College, so I had quite a large collection of music on my laptop in Ogg Vorbis format categorised by artist, album and track and I knew how to use the GStreamer library in Python to play that music. So I decided to write a an automatic version of Name that Tune. The idea of the game would be to play ten seconds of a random song and the players would have to guess the artist and song name. This time Ivan tried the challenge as well on his laptop so we swapped ideas as we worked.

The GStreamer APIs are quite complex, dealing as they do with the asynchronous playback of generic media streams. While exploring my music collection I discovered I had a command-line program called ogg123 installed, which plays a single Ogg Vorbis file. We changed our plans and decided to run ogg123 from a command-line Python program and so avoid the difficulties of writing an event-driven application -- we were pressed for time, after all.

To play the first ten seconds we planned to run ogg123 in the background, sleep for ten seconds and then kill the background process. However, on reading the help text for ogg123 we found that it had a command-line argument to play the first n seconds. Our code became much simpler: there was no need to run the process in the background now, so we could concentrate on recording the scores and managing players.

While writing the program we made sure that it was always in a working state. We started with a simple program that picked a random music file and played ten seconds. We then added code to print out the artist and song name to act as a question. Next, we added a list of players, passed in on the command line, and made the program ask each player in turn. Then we made the program keep track of the scores for each player and print out the scores after each question and when the program exits. Finally we rotated the quiz-master responsibility between the players: when a player answers a question they become the quiz-master for the next player.

We just got the program working in time and didn't have time to clean it up. The code is at the end of this article.

At the end of the challenge there was some controversy as to whether our solution actually passed the first requirement: that I should not have to prepare the quiz data beforehand. After all, I had to rip the Ogg files from my CD collection onto my laptop's file system before I could run the quiz. If someone had done the same thing with their iTunes database we would have accepted that as a solution but because I don't store my main music collection on my laptop we conceded that our solution didn't meet the requirements. (Update: Carlos Villela has created a solution that controls iTunes on MacOS X.)

The winning pair wrote a similar program in Perl. Instead of playing music they asked questions about films. Their solution was ingenious: they screen-scraped IMDB to get the name of the film and then presented several questions about the film: what was the genre, when was it made, who were the starring actors , and so on. To verify the answers, the quizmaster switched to their web browser: the program had opened the IMDB page about the film in the browser while the quizmaster was asking the questions!

Here's our code:

#!/usr/bin/python

import sys
import os
import subprocess
import random
from itertools import *


class Track:
    def __init__(self, artist, album, track, file):
        self.artist = artist
        self.album = album
        self.track = track
        self.file = file

    def __str__(self):
        return self.__class__.__name__ + str(self.__dict__)

def all_tracks(root):
    for artist in os.listdir(root):
        artist_path = os.path.join(root, artist)
        if os.path.isdir(artist_path):
            for album in os.listdir(artist_path):
                album_path = os.path.join(artist_path, album)
                if os.path.isdir(album_path):
                    for track in os.listdir(album_path):
                        if track.endswith(".ogg"):
                            file_path = os.path.join(album_path, track)
                            yield Track(artist, album, track, file_path)


def run_turn(player, tracks, scores, time=5):
    track = random.choice(tracks)
    
    print "Can", player, "guess this track in", time, "seconds"
    print "Artist:", track.artist
    print "Album: ", track.album
    print "Track: ", track.track
    
    subprocess.call(["ogg123", "--quiet", "--end", str(time), track.file])

    answer = None
    while answer != 'y' and answer != 'n':
        sys.stdout.write("Correct? (y/n) ")
        answer = raw_input().lower()
    
    if answer == 'y':
        scores[player] = scores[player]+1
    
    print player, "has", scores[player], "points"
    
    print "Pass the computer to", player
    print player, "press Return to continue"
    raw_input()


def find_winners(scores):
    best_score = 0
    best_players = []
    
    for player, score in scores.items():
        if score > best_score:
            best_score = score
            best_players = [player]
        elif score == best_score:
            best_players.append(player)

    return best_players


def print_scores(scores):
    for player, score in scores.items():
        print player, "scored", score
    
    print ""
    winners = find_winners(scores)
    if len(winners) == 1:
        print "the winner is:", winners[0]
    else:
        print "the winners are:", ", ".join(winners)


tracks = list(all_tracks("/home/nat/music"))
players = sys.argv[1:]
scores = dict( (player,0) for player in players )

try:
    print players[-1], "asks the first question"
    print players[-1], "press Return to continue"
    raw_input()
    
    for player in cycle(players):
        run_turn(player, tracks, scores)

except KeyboardInterrupt:
    pass

print ""
print ""
print "Final scores:"
print_scores(scores)

Copyright © 2007 Nat Pryce. Posted 2007-04-05. Share it.