Skip to content

Instantly share code, notes, and snippets.

@pepasflo
Created August 29, 2018 23:35
Show Gist options
  • Save pepasflo/cc4f31219a21058aea63783ae834af28 to your computer and use it in GitHub Desktop.
Save pepasflo/cc4f31219a21058aea63783ae834af28 to your computer and use it in GitHub Desktop.
Bowling scoring system
#!/usr/bin/env python
import sys
import json
# Safe access for lists.
def lget(l, i, default=None):
if len(l) <= i:
return default
else:
return l[i]
# Safe access for lists of integers.
def iget(l, i):
return lget(l, i, 0)
# Transform a list of frames into a list of pinfalls.
# e.g. [['X'],[1,'/'],[4,3]] -> [10, 1, 9, 4, 3]
def pinfalls(frames):
falls = []
for frame in frames:
for ball in frame:
if ball == 'X':
falls.append(10)
elif ball == '/':
falls.append(10 - falls[-1])
else:
falls.append(int(ball))
return falls
# Return the score of the first frame in the given list of frames.
# e.g. [['X'],[2,3]] -> 15
def single_frame_score(frames):
frame = frames[0]
falls = pinfalls(frames)
if lget(frame, 0) == 'X' or lget(frame, 1) == '/' or len(frame) == 3:
return iget(falls, 0) + iget(falls, 1) + iget(falls, 2)
elif len(frame) == 2:
return iget(falls, 0) + iget(falls, 1)
else:
return iget(falls, 0)
# Compute the (individual) scores of the given frames.
# e.g. [['X'],[2,3]] -> [15,5]
def frame_scores(frames):
if len(frames) == 0:
return []
elif len(frames) == 1:
return [single_frame_score(frames)]
else:
return [single_frame_score(frames)] + frame_scores(frames[1:])
# Compute the running totals for the given set of frame scores.
# e.g. [1,2,3] -> [1,3,6]
def running_totals(scores):
if len(frames) == 0:
return []
sum = 0
totals = []
for score in scores:
sum += score
totals.append(sum)
return totals
if __name__ == "__main__":
if len(sys.argv) > 1:
# if a command-line argument was given, assume that is a JSON array of frames.
input_string = sys.argv[1]
else:
# else, read the JSON array of frames from stdin.
input_string = sys.stdin.read()
frames = json.loads(input_string)
scores = running_totals(frame_scores(frames))
# print out the scores as JSON to stdout.
print json.dumps(scores)
$ ./test.py
👉 Test case: full-game-all-strikes.json
👉 Running ./bowling.py '[["X"], ["X"], ["X"], ["X"], ["X"], ["X"], ["X"], ["X"], ["X"], ["X", "X", "X"]]'
✅ Passed: [30, 60, 90, 120, 150, 180, 210, 240, 270, 300]
👉 Test case: full-game-no-score.json
👉 Running ./bowling.py '[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]'
✅ Passed: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
👉 Test case: ncna2013-example1.json
👉 Running ./bowling.py '[[4, "/"], [6, 2], [4, 2], [8, 1], ["X"], [6, 2], [7, 2], [9, 0], [0, 5], [6, 3]]'
✅ Passed: [16, 24, 30, 39, 57, 65, 74, 83, 88, 97]
👉 Test case: ncna2013-example2.json
👉 Running ./bowling.py '[[4, "/"], [6, 2], [4, 2], [8, 1], ["X"], [6, 2], [7, 2], [9, 0], [0, 5], [6, 3]]'
✅ Passed: [16, 24, 30, 39, 57, 65, 74, 83, 88, 97]
👉 Test case: ncna2013-example3.json
👉 Running ./bowling.py '[[4, "/"], [6, 2], [4, 2], [8, 1], ["X"], ["X"], [7, 2], [9, 0], [0, 5], [6, 3]]'
✅ Passed: [16, 24, 30, 39, 66, 85, 94, 103, 108, 117]
👉 Test case: ncna2013-example4.json
👉 Running ./bowling.py '[[4, "/"], [6, 2], [4, 2], [8, 1], ["X"], ["X"], [7, 2], [9, 0], [0, 5], [6, "/", 7]]'
✅ Passed: [16, 24, 30, 39, 66, 85, 94, 103, 108, 125]
👉 Test case: wikipedia-example1.json
👉 Running ./bowling.py '[["X"], [3, 6]]'
✅ Passed: [19, 28]
👉 Test case: wikipedia-example2.json
👉 Running ./bowling.py '[["X"], ["X"], [9, 0]]'
✅ Passed: [29, 48, 57]
👉 Test case: wikipedia-example3.json
👉 Running ./bowling.py '[["X"], ["X"], ["X"], [8, "/"], [8, 0]]'
✅ Passed: [30, 58, 78, 96, 104]
👉 Test case: wikipedia-example4.json
👉 Running ./bowling.py '[["X"], ["X"], ["X"], ["X"], ["X"], [7, 2]]'
✅ Passed: [30, 60, 90, 117, 136, 145]
#!/usr/bin/env python
# encoding: utf-8
import sys
import os
import subprocess
import json
# replace this with the name of your program.
exe = "./bowling.py"
for test_fname in sorted(os.listdir("tests")):
# parse out the test case JSON:
j = json.load(open("tests/%s" % test_fname))
frames = j["frames"]
expected_scores = j["scores"]
# feed this test case to the scoring program:
exe_input = json.dumps(frames)
sys.stdout.write("\n")
sys.stdout.write("👉 Test case: %s\n" % test_fname)
sys.stdout.write(" 👉 Running %s '%s'\n" % (exe, exe_input))
sys.stdout.flush()
output = subprocess.check_output([exe, exe_input]).rstrip()
try:
computed_scores = json.loads(output)
except Exception as e:
sys.stderr.write(" ❌ Failed to decode output JSON: '%s'\n" % output)
raise e
# compare the computed scores to the actual scores:
if expected_scores == computed_scores:
sys.stdout.write(" ✅ Passed: %s\n" % computed_scores)
else:
sys.stderr.write(" ❌ Failed: %s\n" % test_fname)
sys.stderr.write(" expected output: %s\n" % expected_scores)
sys.stderr.write(" actual output: %s\n" % computed_scores)
sys.exit(1)
{
"frames": [["X"],["X"],["X"],["X"],["X"],["X"],["X"],["X"],["X"],["X","X","X"]],
"scores": [30,60,90,120,150,180,210,240,270,300]
}
{
"frames": [[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],
"scores": [0,0,0,0,0,0,0,0,0,0]
}
{
"frames": [[4,"/"],[6,2],[4,2],[8,1],["X"],[6,2],[7,2],[9,0],[0,5],[6,3]],
"scores": [16,24,30,39,57,65,74,83,88,97],
"comment": "https://ncna-region.unl.edu/regional_2013_final.pdf"
}
{
"frames": [[4,"/"],[6,2],[4,2],[8,1],["X"],[6,2],[7,2],[9,0],[0,5],[6,3]],
"scores": [16,24,30,39,57,65,74,83,88,97],
"comment": "https://ncna-region.unl.edu/regional_2013_final.pdf"
}
{
"frames": [[4,"/"],[6,2],[4,2],[8,1],["X"],["X"],[7,2],[9,0],[0,5],[6,3]],
"scores": [16,24,30,39,66,85,94,103,108,117],
"comment": "https://ncna-region.unl.edu/regional_2013_final.pdf"
}
{
"frames": [[4,"/"],[6,2],[4,2],[8,1],["X"],["X"],[7,2],[9,0],[0,5],[6,"/",7]],
"scores": [16,24,30,39,66,85,94,103,108,125],
"comment": "https://ncna-region.unl.edu/regional_2013_final.pdf"
}
{
"frames": [["X"],[3,6]],
"scores": [19,28],
"comment": "https://en.wikipedia.org/wiki/Ten-pin_bowling#Traditional_scoring"
}
{
"frames": [["X"],["X"],[9,0]],
"scores": [29,48,57],
"comment": "https://en.wikipedia.org/wiki/Ten-pin_bowling#Traditional_scoring"
}
{
"frames": [["X"],["X"],["X"],[8,"/"],[8,0]],
"scores": [30,58,78,96,104],
"comment": "https://en.wikipedia.org/wiki/Ten-pin_bowling#Traditional_scoring"
}
{
"frames": [["X"],["X"],["X"],["X"],["X"],[7,2]],
"scores": [30,60,90,117,136,145],
"comment": "https://en.wikipedia.org/wiki/Ten-pin_bowling#Traditional_scoring"
}
@pepasflo
Copy link
Author

oops, I accidentally made tests_ncna2013-example1.json and tests_ncna2013-example2.json the same.

tests_ncna2013-example1.json should instead be:

{
    "frames": [[4,3],[6,2],[4,2],[8,1],[4,5],[6,2],[7,2],[9,0],[0,5],[6,3]],
    "scores": [7,15,21,30,39,47,56,65,70,79],
    "comment": "https://ncna-region.unl.edu/regional_2013_final.pdf"
}

@pepasflo
Copy link
Author

Note: you can re-use test.py and the test cases for your own implementation. Just edit the line which says exe = './bowling.py'. Design your implementation to accept a single command-line argument which is a JSON string describing the frames, and output a JSON string which describes the (running total) frame scores.

Note that test.py actually expects the test JSON files to be in a directory called tests.

To conveniently TDD your implementation, run this in a terminal: while true; do sleep 1; clear; ./test.py; done. Each time you save your source file, check the terminal to see if the tests passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment