Programming question - Bowling Score
Contents
About
I was asked this question in a code interview in Sep 2023. It was my first "pair programming" interview, which was a neat concept where I did most of the talking (pretty sure that was the idea!) and my interviewer wrote the code I suggested - making it a more realistic model of working in pairs.
I was pretty fuzzy on the rules of bowling, so it took time to work out how a spare and strike work - and the very last frame is tricky because it kind-of has its own rules. I'll type out the gist of the question below:
PROBLEM: Bowling Score
Create a program, which, given a valid sequence of rolls for one line of American Ten-Pin Bowling, produces the total score for the game. Here are some things that the program will not do:
- We will not check for valid rolls.
- We will not check for the correct number of rolls and frames.
- We will not provide scores for intermediate frames.
- Depending on the application, this might or might not be a valid way to define a complete story, but we do it here for purposes of keeping the kata light. I think you’ll see that improvements like those above would go in readily if they were needed for real.
We can briefly summarize the scoring for this form of bowling:
- Each game, or “line” of bowling, includes ten turns, or "frames" for the bowler.
- In each frame, the bowler gets up to two tries to knock down all the pins.
- If in two tries, he fails to knock them all down, his score for that frame is the total number of pins knocked down in his two tries.
- If in two tries he knocks them all down, this is called a "spare" and his score for the frame is ten plus the number of pins knocked down on his next throw (in his next turn).
- If on his first try in the frame he knocks down all the pins, this is called a "strike". His turn is over, and his score for the frame is ten plus the simple total of the pins knocked down in his next two rolls.
- If he gets a spare or strike in the last (tenth) frame, the bowler gets to throw one or two more bonus balls, respectively. These bonus throws are taken as part of the same turn. If the bonus throws knock down all the pins, the process does not repeat: the bonus throws are only used to calculate the score of the final frame.
- The game score is the total of all frame scores.
SOLUTION: In Python3
So this might not be the most elegant solution.. for that, honestly, just ask ChatGPT, but it's the one I came up with and tested. I tried to make it compact but I also wanted to make sure the test cases were readable, hence the array of arrays. Other representations might use symbols like '/' and 'X' to represent a strike and spare - and actually, that might be easier to parse.
bowling.py:
# Question from: https://codingdojo.org/kata/Bowling/
# Solution from: https://andrewnoske.com/wiki/Programming_question_-_Bowling_Score
NUM_FRAMES = 10
def score_in_last_frame(frame: list[int]) -> int:
has_three_bowls = len(frame) >= 3 # Had extra bowl.
pins_in_frame = sum(frame)
strike = frame[0] == 10
strike_times_two = strike and len(frame) >= 2 and frame[1] == 10
strike_times_three = strike_times_two and len(frame) >= 3 and frame[2] == 10
spare = not strike and (frame[0] + frame[1] == 10)
if strike_times_three:
return 30
elif strike_times_two and has_three_bowls:
return 20 + (frame[2] * 2)
elif strike:
return 10 + (frame[1] * 2)
elif spare and has_three_bowls:
return 10 + (frame[2] * 2)
else:
return pins_in_frame
def bowling_score(game: list[list[int]]) -> int:
total: int = 0
for i, frame in enumerate(game):
if not frame: # Sanity check.
continue
last_frame = (i == NUM_FRAMES - 1)
if last_frame: # Last frame has it's own rules.
total += score_in_last_frame(frame)
else:
pins_in_frame = sum(frame)
strike = frame[0] == 10
spare = pins_in_frame == 10 and not strike
print(i, 'pins_in_frame=', pins_in_frame,
', spare=', spare, ', strike', strike)
total += pins_in_frame # Add pins from this frame.
if spare or strike: # For spare or strike: add pins from next bowl.
total += game[i+1][0] # Add next bowl (1st bowl next frame).
if strike: # If strike: add pins from next, next bowls.
second_last_frame = (i == NUM_FRAMES - 2)
if second_last_frame:
total += game[i+1][1] # Add 2nd bowl in last frame.
else:
if game[i+1][0] == 10: # If next bowl was strike: go to next frame.
total += game[i+2][0] # Next next frame, 1st bowl.
else:
total += game[i+1][1] # Next frame, 2nd bowl.
return total
main.py:
import bowling as main
###############################################################################
# TESTS:
###############################################################################
def def_assert_equals(dis):
print(dis)
def test_empty_game():
game = [[0, 0]] * 10
score = main.bowling_score(game)
assert( score == 0)
def test_all_1s_game():
game = [[1, 1]] * 10
score = main.bowling_score(game)
assert( score == 20)
def test_1_spare_game():
game = [
[2, 8], [2, 2], [2, 2], [2, 2], [2, 2],
[2, 2], [2, 2], [2, 2], [2, 2], [2, 2]]
score = main.bowling_score(game)
assert( score == 46+2)
def test_1_strike_game():
game = [
[10, 0], [2, 2], [2, 2], [2, 2], [2, 2],
[2, 2], [2, 2], [2, 2], [2, 2], [2, 2]]
score = main.bowling_score(game)
assert( score == 46+4)
def test_perfect_game():
game = [
[10], [10], [10], [10], [10],
[10], [10], [10], [10], [10, 10, 10]]
score = main.bowling_score(game)
assert( score == 300)
def test_2_strikes_last_frame_game():
game = [[0, 0]] * 9 # 0.
game.append([10, 10, 0]) # 20 -> 20.
score = main.bowling_score(game)
assert( score == 20)
def test_spare_last_frame_game():
game = [[8, 1]] * 9 # 9 * 9 = 81.
game.append([9, 1, 2]) # 12 + 2 = 14 -> 95.
###############################################################################
if __name__ == '__main__':
# Demo input:
game = [[8, 1]] * 9 # 9 * 9 = 81.
game.append([9, 1, 2]) # 12 + 2 = 14 -> 95.
print('DEMO BOARD:', game)
score = main.bowling_score(game)
print('SCORE = ', score)
# Tests:
test_empty_game()
test_all_1s_game()
test_1_spare_game()
test_1_strike_game()
test_perfect_game()
test_2_strikes_last_frame_game()
PROBLEM: Render Bowling Frame
I decided that a slightly fancier version should show the (intermediate) score for each bowling frame and render out a grid.
SOLUTION: In Python3
This time I decided to use a class and actually I feel it came out cleaner:
class BowlingGame:
def __init__(self, frames):
self.frames = frames
def _is_strike(self, frame):
return frame[0] == 10
def _is_spare(self, frame):
return sum(frame) == 10 and not self._is_strike(frame)
def score(self) -> list[int]:
"""Calculates a cumulative for each frame, returned as an list of scores.
The total score is the value of the last element."""
total = 0
scores = []
for i, frame in enumerate(self.frames):
if self._is_strike(frame) and i < 9: # Not the last frame.
next_frame = self.frames[i + 1]
bonus = next_frame[0] + (
next_frame[1] if len(next_frame) > 1 else self.frames[i + 2][0])
elif self._is_spare(frame) and i < 9:
bonus = self.frames[i + 1][0]
else:
bonus = 0
total += sum(frame) + bonus
scores.append(total)
return scores
def display(self):
"""Prints a scoring grid for the bowling game.
Shows the cumulative score after each frame."""
scores = self.score()
FRAME_WIDTH = 6 # Frame width in characters.
# Display the grid:
frame_pieces = []
for i, frame in enumerate(self.frames):
frame_piece = ''
if self._is_strike(frame):
frame_pieces.append(' X'.ljust(FRAME_WIDTH))
# print(f'X | ', end='')
else:
if self._is_spare(frame):
frame_pieces.append(f' {frame[0]} / '.ljust(FRAME_WIDTH))
# print(f'{frame[0]} / | ', end='')
else:
frame_pieces.append(f' {frame[0]} {frame[1]} '.ljust(FRAME_WIDTH))
# print(f'{frame[0]} {frame[1]} | ', end='')
# Display the intermediate scores
score_pieces = []
for score in scores:
score_pieces.append(f' {score}'.ljust(FRAME_WIDTH))
# print(f'{score} | ', end='')
print(' |' + '|'.join(frame_pieces) + '|')
print(' |' + '|'.join(score_pieces) + '|' )
print(' .... Final Score: ' + str(scores[-1]) + '\n')
if __name__ == '__main__':
frames = [[10], [7, 3], [9, 0], [10], [0, 8], [8, 2], [0, 6], [10], [10], [10, 8, 1]] # Expect: 167 (good game).
# frames = [[10], [10], [10], [10], [10], [10], [10], [10], [10], [10, 10, 10]] # Expect: 300 (perfect game).
# frames = [[0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [9, 1, 4]] # Expect: 14 (one spare at end).
# frames = [[5,5], [3,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0, 0, 0]] # Expect: 16 (one spare at start).
game = BowlingGame(frames)
game.display()
This came rendered out as:
| X | 7 / | 9 0 | X | 0 8 | 8 / | 0 6 | X | X | X |
| 20 | 39 | 48 | 66 | 74 | 84 | 90 | 120 | 148 | 167 |
.... Final Score: 167
See Also
Links
- codingdojo.org - Where I found out this question comes from.