Skip to content

Instantly share code, notes, and snippets.

@bitwalker
Last active Oct 14, 2015
Embed
What would you like to do?
Bowling scorer in Elixir
defmodule Bowling do
@moduledoc """
Takes a bowling game score, with 10 frames seperated by spaces:
`X 53 18 5/ 5/ 44 36 35 9/ 9/4`
An ‘X’ marks a strike, a ‘/’ marks a spare. All frames can have 2 bowls,
except for the last which has up to 3. Your challenge is to come up with a
program that takes the above string as an input and outputs the integral score.
"""
@doc """
Scores a given bowling game given an input string which represents the scoring of each frame.
## Examples
iex> Bowling.score "X 53 18 5/ 5/ 44 36 35 9/ 9/4"
{:ok, 122}
iex> Bowling.score "X X X X X X X X X XXX"
{:ok, 300}
"""
def score(str) when is_binary(str) do
str
|> String.split(" ", trim: true)
|> calculate(0)
end
def score(_), do: {:error, "Scoring requires a string representing the score board."}
############
# Explanation of how `calculate` works:
#
# First, `score` takes the input string, and splits it into a list of substrings,
# So when `calculate` is called, it's first argument is that list, and it's second
# argument is the accumulator for the score, initialized to zero.
#
# `calculate` is recursive, and the recursion is controlled by pattern matching against
# the list of strings to be parsed. When we get an empty list, or pattern match against
# the last element of the list (as we do with the last frame section), we know we can stop
# recursing and return the score.
#
# Pattern matching in Elixir is quite rich, and allows you to nest matches, for instance,
# the version of `calculate` which matches a triple with a strike in the last frame. We
# are matching on three list elements, one of which is a string which we expect to start
# with a strike symbol, but we don't care what comes after.
#
# One of the advantages of this style of programming is that the rules for scoring
# are declaritive. Given a specific pattern for a given frame or set of frames, we
# can see precisely how that will be scored. It also allows us to avoid nested conditionals,
# error handling, and logic for different situations being mixed together.
#
# Another benefit of this approach, at least as it applies to Elixir/Erlang, is that the input
# is only ever iterated over twice (once to split the string, once to score each frame). We could
# easily modify this to read the string a single time, but for this exercise I went for the slightly
# clearer solution of splitting the string first.
# We've reached the end of the calculation, return the score
defp calculate([], score), do: {:ok, score}
# Triple
defp calculate(["X", "X", "X" | rest], score) do
calculate(["X", "X" | rest], score + 30)
end
# Special case: Triple, including strike in last frame
defp calculate(["X", "X", <<?X, _::binary>> = frame], score) do
calculate(["X", frame], score + 30)
end
# Special case: Triple, including strikes from last frame
defp calculate(["X", frame | rest], score) when frame in ["XX", "XXX"] do
calculate([frame | rest], score + 30)
end
# Double
defp calculate(["X", "X", <<roll::utf8, _::utf8>> = frame | rest], score) when roll in ?0..?9 do
calculate(["X", frame | rest], score + 20 + List.to_integer([roll]))
end
# Special case, double with strike from last frame
defp calculate(["X", <<?X, roll::utf8>> = frame | rest], score) when roll in ?0..?9 do
calculate([frame | rest], score + 20 + List.to_integer([roll]))
end
# Strike, 10 + value of next two rolls
defp calculate(["X", <<roll1::utf8, roll2::utf8>> = frame | rest], score) when roll1 in ?0..?9 and roll2 in ?0..?9 do
calculate([frame | rest], score + 10 + List.to_integer([roll1]) + List.to_integer([roll2]))
end
# Special case, spare followed by strike.
defp calculate([<<_::utf8, ?/>>, "X" | rest], score) do
calculate(["X" | rest], score + 20)
end
# Spare, 10 + value of next roll
defp calculate([<<_::utf8, ?/>>, <<roll2::utf8, _::binary>> = frame | rest], score) do
case roll2 do
?X -> calculate([frame | rest], score + 20)
n when n in ?0..?9 -> calculate([frame | rest], score + 10 + List.to_integer([roll2]))
end
end
# Special case, last frame
defp calculate([<<?X, ?X, ?X>>], score), do: {:ok, score + 30}
defp calculate([<<?X, ?X, roll3::utf8>>], score), do: {:ok, score + 20 + List.to_integer([roll3])}
defp calculate([<<?X, ?X>>], _), do: {:error, "Incomplete game, last frame has two strikes, one more roll left."}
defp calculate([<<?X, roll2::utf8>>], score), do: {:ok, score + 10 + List.to_integer([roll2])}
defp calculate([<<_::utf8, ?/, roll2::utf8>>], score) when roll2 in ?0..?9 do
{:ok, score + 10 + List.to_integer([roll2])}
end
# Normal frame, two rolls
defp calculate([<<roll1::utf8, roll2::utf8>> | rest], score) when roll1 in ?0..?9 and roll2 in ?0..?9 do
calculate(rest, score + List.to_integer([roll1]) + List.to_integer([roll2]))
end
defp calculate([frame | _], _) do
{:error, "Invalid frame: `#{frame}`"}
end
end
ExUnit.start autorun: true
defmodule BowlingTests do
use ExUnit.Case, async: true
# doctests don't work in Elixir Playground apparently :/
# doctest Bowling
test "can score a game" do
assert {:ok, 122} = Bowling.score "X 53 18 5/ 5/ 44 36 35 9/ 9/4"
end
test "can score a perfect game" do
assert {:ok, 300} = Bowling.score "X X X X X X X X X XXX"
end
test "input strings are validated" do
{:error, err} = Bowling.score "X 53 18 5/ 5/ 44 36 35 9/ 9/F"
assert ^err = "Invalid frame: `9/F`"
end
test "only complete games are scored" do
{:error, err} = Bowling.score "X 53 18 5/ 5/ 44 36 35 9/ XX"
assert ^err = "Incomplete game, last frame has two strikes, one more roll left."
end
end
@bitwalker
Copy link
Author

bitwalker commented Oct 13, 2015

Run it here

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