Posted on 17 Apr 2013 in Python JavaScript d3.js Node.js
Another code challenge! This time the challenge is to read symbolic bowling scores from a file, translate them to numeric scores using proper rules, then get the average score per game across the data set. At first I was trying to come up with a clever solution to this problem. Yeah... not so much. Good enough is good enough. This time I solved it using using Python and also with JavaScript on Node. I also decided to chart the scores using d3.js.
The data consists of 200,000 bowling games to score.
1080632223x01814262
905171513145x51093/7
7/1/700/525/70082133-
9/51451253726/018150-
2654335/5002819/42x7/"""
This script reads a dataset of simulated bowling games.
It scores each game, then returns the average.
"""
import gzip
def score(game):
# List to hold our scores for this game.
scores = []
# Allow us to mark frame boundries
frame = False
# Loop and enumerate the game so we can look forward in the
# list to score strikes and spares.
for i, roll in enumerate(game):
# If frame has been set to true, we will be skipping this ball
# because it is the second roll of a frame and has already been
# accounted for.
if frame is True:
frame = False
continue
# Keeps us scoring the bonus roll.
if len(scores) < 10:
# Strike
if roll == 'x':
if game[i + 1] == 'x' and game[i + 2] == 'x':
# Turkey! Three consecutive strikes.
scores.append(30)
elif game[i + 1] == 'x' and game[i + 2].isdigit():
# Double! Two consecutive strikes.
scores.append(20 + int(game[i + 2]))
elif game[i + 1].isdigit() and game[i + 2] == '/':
# Strike followed by a spare.
scores.append(20)
elif game[i + 1].isdigit() and game[i + 2].isdigit():
# Strike followed by open frame.
scores.append(10 + int(game[i + 1]) + int(game[i + 2]))
# Spare
elif roll.isdigit() and game[i + 1] == '/':
if game[i + 2] == 'x':
# Spare followed by a strike
scores.append(20)
elif game[i + 2].isdigit():
# Spare followed by numeric (non-strike) roll
scores.append(10 + int(game[i + 2]))
frame = True
# Open frame. Add the two rolls and set frame to True.
else:
scores.append(int(roll) + int(game[i + 1]))
frame = True
# Return the total score for the game.
return sum(scores)
def unzip_data(f):
# Open the file for reading and assign it.
with gzip.open(f) as bowlarama:
return bowlarama.read().splitlines()
def avg_score(filename):
# Split the scores on new lines, then map each one
# to the score function, then sum the results
# to get the total score
games = unzip_data(filename)
total_score = round(sum(map(score, games)) / float(len(games)), 2)
return total_score
# Print the average of all the bowling games
print avg_score('bowlarama.txt.gz')var zlib = require('zlib');
var fs = require('fs');
function score(game) {
// Scores one bowling game score, returns the total
var scores = [], frame = false, i;
// Loop over the game string as an array
for (i = 0; i < game.length; i++) {
var roll = game[i];
// Check to see if we are mid frame, skip this ball if we are.
if (frame) {
frame = false;
continue;
}
// Keeps us from scoring the bonus third roll.
if (scores.length < 10) {
// Strike
if (roll === 'x') {
if (game[i + 1] === 'x' && game[i + 2] === 'x') {
// Turkey! Three consecutive strikes.
scores.push(30);
} else if (game[i + 1] === 'x' && typeof +game[i + 2] === 'number') {
// Double! Two consecutive strikes.
scores.push(20 + +game[i + 2]);
} else if (typeof +game[i + 1] === 'number' && game[i + 2] === '/') {
// Strike followed by a spare.
scores.push(20);
} else if (typeof +game[i + 1] === 'number' && typeof +game[i + 2] === 'number') {
// Strike followed by open frame.
scores.push(10 + +game[i + 1] + +game[i + 2]);
} else {
return "Error: " + roll;
} // Do not need to set frame to true since this is a strike i.e. one roll frame
// Spare
} else if (typeof +roll === 'number' && game[i + 1] === '/') {
// Spare followed by a strike
if (game[i + 2] === 'x') {
scores.push(20);
// Spare followed by numeric (non-strike) roll
} else if (typeof +game[i + 2] === 'number') {
scores.push(10 + +game[i + 2]);
} else {
return "Error: " + roll;
}
// Set frame to true because we are including two rolls in this frame and we want to
// skip the second ball in the function since the scoring is done in reference to the first ball
frame = true;
// Open frame.
} else {
// Add the two rolls.
scores.push(+roll + +game[i + 1]);
frame = true;
}
}
}
// Return the total score for the game using reduce by sum on the scores array
return scores.reduce(function(a, b){ return a + b; });
}
function inputfile(callback, filename) {
// Read a file and issue callback when complete.
fs.readFile(filename, function (err, content) {
if (err) return callback(err);
callback(content);
});
}
// Read the bowling scores dataset and process the dataset on callback.
inputfile(function (data) {
zlib.unzip (data, function(err, data) {
// If no errors, we have data
if (!err) {
// Set the dataset to a string and split it on new line.
var mass = data.toString().split('\n'), i, game_scores = [];
// Store the length before loop. Minus one because the last item
// is garbage from the \n split.
var games = mass.length - 1;
// Process each bowling game into an integer equal to the score.
for (i = 0; i < games; i++) {
// Push the score to an array so we can reduce it to get the sum of the scores.
game_scores.push(score(mass[i]));
}
// Take the sum of scores and divide by the number of games to get the average score
console.log(Math.floor((game_scores.reduce(function(a, b){
return a + b;
}) / game_scores.length) * 100) / 100);
}
});
}, "bowlarama.txt.gz");The results on each came back the same. Seeing as I wrote the Python version first, then essentially ported it over... no surprises there. PyPy 2.0.0-beta2 beats out Node v0.10.4 and Python 2.7.3.
$ time python bowlarama.py
91.46
python bowlarama.py 4.60s user 0.03s system 96% cpu 4.784 total
$ time pypy bowlarama.py
91.46
pypy bowlarama.py 0.79s user 0.06s system 96% cpu 0.882 total
$ time node bowlarama.js
91.46
node bowlarama.js 0.78s user 0.11s system 94% cpu 0.932 totalSo there you have it, a slightly skewed distribution. Now to cut down the mightiest tree in the forest with an herring.
2024. In dreams we're free, awake we strive.