Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Last active December 13, 2021 02:05
Show Gist options
  • Save cellularmitosis/cab849408fbd2dff9f116e119fb2e4e7 to your computer and use it in GitHub Desktop.
Save cellularmitosis/cab849408fbd2dff9f116e119fb2e4e7 to your computer and use it in GitHub Desktop.
Bowling scoring system

Blog 2018/8/29   (imported from my work account gist on 2020/4/22)

<- previous | index | next ->

Bowling scoring system

As part of a weekly programming puzzle, I implemented a bowling scoring system (in Python), plus a test script.

To run the tests:

$ chmod +x bowling.py test.py
$ mkdir tests
$ mv tests_* tests/
$ ./test.py

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

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.

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.

#!/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,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"
}
{
"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"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment