I’ve recently been reading ploeh’s series on doing the Tennis Kata in F#. I decided to have a go in Erlang, we might not have a fancy type system but we do have pattern matching and partial functions.
You can find the source code to follow along here.
I decided to implement a “game” as a gen_server (when you have a hammer…), I refer you to a previous post for the pre-amble.
Love all
The first test is to check the initial score:
initial_score(Pid) -> fun() -> ?assertEqual(#{p1=>love,p2=>love}, gen_server:call(Pid, get_score)) end.
I decided to return a map, rather than a tuple or record, and to use the atom “love” rather than 0. The implementation is pretty trivial:
init([]) -> {ok, #{p1=>love,p2=>love}}. handle_call(get_score, _From, State) -> {reply, State, State};
If there was more information in the state map, you might want to return a projection rather than the whole object.
Fifteen love
fifteen_love(Pid) -> fun() -> ?assertEqual(#{p1=>15,p2=>love}, gen_server:call(Pid, {won_point, p1})) end.
This time we need to calculate the new score when a player has won a point:
handle_call({won_point, p1}, _From, State = #{p1:=CurrentScore}) -> NewState = State#{p1:=new_score(CurrentScore)}, {reply, NewState, NewState}; ... new_score(love) -> 15.
Love fifteen
love_fifteen(Pid) -> fun() -> ?assertEqual(#{p1=>love,p2=>15}, gen_server:call(Pid, {won_point, p2})) end.
We can re-use the new_score method from before:
handle_call({won_point, p2}, _From, State = #{p2:=CurrentScore}) -> NewState = State#{p2:=new_score(CurrentScore)}, {reply, NewState, NewState};
Fifteen all
fifteen_all(Pid) -> fun() -> won_point(Pid, p1), ?assertEqual(#{p1=>15,p2=>15}, won_point(Pid, p2)) end. won_point(Pid, Player) -> gen_server:call(Pid, {won_point, Player}).
No need for any new code, but it seemed like time for a little test refactoring.
Thirty fifteen
thirty_fifteen(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), ?assertEqual(#{p1=>30,p2=>15}, won_point(Pid, p1)) end.
Now we need to add another function head, to handle the new score:
new_score(love) -> 15; new_score(15) -> 30.
Thirty all
thirty_all(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p2), ?assertEqual(#{p1=>30,p2=>30}, won_point(Pid, p1)) end.
No new code required.
Forty thirty
forty_thirty(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p1), won_point(Pid, p2), ?assertEqual(#{p1=>40,p2=>30}, won_point(Pid, p1)) end.
Again, we need to handle a new score:
... new_score(30) -> 40.
P1 win
p1_wins(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p1), won_point(Pid, p1), ?assertEqual({game_over, p1}, won_point(Pid, p1)) end.
This requires a special case, for when the player has 40 points:
handle_call({won_point, p1}, _From, State = #{p1:=40}) -> {stop, normal, {game_over, p1}, State};
But now we have a problem, as I skipped over the troublesome “deuce”.
Deuce
deuce(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p1), won_point(Pid, p1), won_point(Pid, p2), ?assertEqual(#{p1=>deuce,p2=>deuce}, won_point(Pid, p2)) end.
Again, I decided to use an atom, rather than 40-40. But now we need to take both player’s scores into account when determining the new score:
handle_call({won_point, p1}, _From, State = #{p1:=P1Score,p2:=P2Score}) -> {NewP1Score, NewP2Score} = new_score(P1Score, P2Score), NewState = State#{p1:=NewP1Score, p2:=NewP2Score}, {reply, NewState, NewState}; ... new_score(love, Score) -> {15, Score}; new_score(15, Score) -> {30, Score}; new_score(30, 40) -> {deuce, deuce}; new_score(30, Score) -> {40, Score}.
Advantage
advantage_p1(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p1), won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p2), ?assertEqual(#{p1=>advantage,p2=>40}, won_point(Pid, p1)) end.
Another case to handle:
... new_score(deuce, deuce) -> {advantage, 40}.
Back to Deuce
advantage_p1_back_to_deuce(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p1), won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p2), won_point(Pid, p1), ?assertEqual(#{p1=>deuce,p2=>deuce}, won_point(Pid, p2)) end.
Now we need to add a guard to the win case:
handle_call({won_point, p2}, _From, State = #{p1:=P1Score, p2:=40}) when P1Score =/= advantage -> {stop, normal, {game_over, p2}, State}; ... new_score(40, advantage) -> {deuce, deuce}.
Advantage, then win
advantage_p1_then_win(Pid) -> fun() -> won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p1), won_point(Pid, p1), won_point(Pid, p2), won_point(Pid, p2), won_point(Pid, p1), ?assertEqual({game_over, p1}, won_point(Pid, p1)) end.
Finally, we need to allow a win from Advantage:
handle_call({won_point, p1}, _From, State = #{p1:=advantage}) -> {stop, normal, {game_over, p1}, State};
I think that’s everything covered. As Mark says, “it’s easy enough that it’s fun to play with, but difficult enough that it’s fun to play with”. I personally feel that the use of pattern matching made the intent of the code clearer, but of course YMMV.