

Table-driven tennis scoring
source link: https://blog.ploeh.dk/2021/03/29/table-driven-tennis-scoring/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Table-driven tennis scoring by Mark Seemann
Probably the most boring implementation of the tennis kata I've ever written.
Regular readers of this blog will know that I keep coming back to the tennis kata. It's an interesting little problem to attack from various angles.
The tennis scoring rules essentially describe a finite state machine, and while I was thinking about the state transitions involved, I came across an article by Michael McCandless about scoring tennis using finite-state automata.
This isn't the first time I've thought about simply enumerating all possible states in the state machine, but I decided to spend half an hour on actually doing it. While Michael McCandless shows that an optimisation is possible, his minimised version doesn't enable us to report all intermediary states with the correct labels. For example, he optimises away thirty-all by replacing it with deuce. The end result is still the same, in the sense that the minimised state machine will arrive at the same winner for the same sequence of balls, but it can't correctly report the score while the game is in progress.
For that reason, I decided to use his non-optimised state machine as a launch pad.
States #
I used F# to enumerate all twenty states:
type Score = | LoveAll | FifteenLove | LoveFifteen | ThirtyLove | FifteenAll | LoveThirty | FortyLove | ThirtyFifteen | FifteenThirty | LoveForty | FortyFifteen | ThirtyAll | FifteenForty | GamePlayerOne | FortyThirty | ThirtyForty | GamePlayerTwo | AdvantagePlayerOne | Deuce | AdvantagePlayerTwo
Utterly boring, yes, but perhaps boring code might be good code.
Table-driven methods #
Code Complete describes a programming technique called table-driven methods. The idea is to replace branching instructions such as if
, else
, and switch
with a lookup table. The book assumes that the table exists in memory, but in this case, we can implement the table lookup with pattern matching:
// Score -> Score let ballOne = function | LoveAll -> FifteenLove | FifteenLove -> ThirtyLove | LoveFifteen -> FifteenAll | ThirtyLove -> FortyLove | FifteenAll -> ThirtyFifteen | LoveThirty -> FifteenThirty | FortyLove -> GamePlayerOne | ThirtyFifteen -> FortyFifteen | FifteenThirty -> ThirtyAll | LoveForty -> FifteenForty | FortyFifteen -> GamePlayerOne | ThirtyAll -> FortyThirty | FifteenForty -> ThirtyForty | GamePlayerOne -> GamePlayerOne | FortyThirty -> GamePlayerOne | ThirtyForty -> Deuce | GamePlayerTwo -> GamePlayerTwo | AdvantagePlayerOne -> GamePlayerOne | Deuce -> AdvantagePlayerOne | AdvantagePlayerTwo -> Deuce
The ballOne
function returns the new score when player one wins a ball. It takes the old score as input.
I'm going to leave ballTwo
as an exercise to the reader.
Smoke test #
Does it work, then? Here's a few interactions with the API in F# Interactive:
> ballOne LoveAll;; val it : Score = FifteenLove > LoveAll |> ballOne |> ballTwo;; val it : Score = FifteenAll > LoveAll |> ballOne |> ballTwo |> ballTwo;; val it : Score = FifteenThirty > LoveAll |> ballOne |> ballTwo |> ballTwo |> ballTwo;; val it : Score = FifteenForty > LoveAll |> ballOne |> ballTwo |> ballTwo |> ballTwo |> ballOne;; val it : Score = ThirtyForty > LoveAll |> ballOne |> ballTwo |> ballTwo |> ballTwo |> ballOne |> ballTwo;; val it : Score = GamePlayerTwo
It looks like it's working.
Automated tests #
Should I be writing unit tests for this implementation?
I don't see how a test would be anything but a duplication of the two 'transition tables'. Given that the score is thirty-love, when player one wins the ball, then the new score should be forty-love. Indeed, the ballOne
function already states that.
We trust tests because they are simple. When the implementation is as simple as the test that would exercise it, then what's the benefit of the test?
To be clear, there are still compelling reasons to write tests for some simple implementations, but that's another discussion. I don't think those reasons apply here. I'll write no tests.
Code size #
While this code is utterly dull, it takes up some space. In all, it runs to 67 lines of code.
For comparison, the code base that evolves throughout my Types + Properties = Software article series is 65 lines of code, not counting the tests. When I also count the tests, that entire code base contains around 300 lines of code. That's more than four times as much code.
Preliminary research implies that bug count correlates linearly with line count. The more lines of code, the more bugs.
While I believe that this is probably a simplistic rule of thumb, there's much to like about smaller code bases. In total, this utterly dull implementation is actually smaller than a comparable implementation built from small functions.
Conclusion #
Many software problems can be modelled as finite state machines. I find that this is often the case in my own field of line-of-business software and web services.
It's not always possible to exhaustively enumerate all states, because each 'type' of state carries data that can't practically be enumerated. For example, polling consumers need to carry timing statistics. These statistics influence how the state machine transitions, but the range of possible values is so vast that it can't be enumerated as types.
It may not happen often that you can fully enumerate all states and transitions of a finite state machine, but I think it's worthwhile to be aware of such refactoring opportunities. It might make your code dully simple.
Comments
Hi Mark, I have had a similar experience whilst coding a
Shut the box game, when trying to detect if it was game over or not.
Originally it was a complex set of loops to calculate all the discrete summands for each roll of the dice,
then checking if the remaining flaps were in that set.
This was done along with a suite of tests for every possible combination set of summands up to 12 (for 2 dice).
Then whilst explaining the pain in writing this to a friend, they simply said, there's only a finite list,
why not hard code them?, and that's what I went with, a dictionary with each possible roll from 2 dice,
and the possible values from the flaps that could be used to meet that roll. All the tests were removed; as
you pointed out, they would just be a reimplmentation of the table.
Dave, thank you for writing. It's good to hear that you have a similar experience. I wonder if it's constrained to game simulation, or if 'real-world' examples exist.
Wish to comment?
Recommend
-
35
Microsoft has refreshed its ML.Net open source machine learning framework , fitti...
-
46
README.md Mask Scoring R-CNN (MS R-CNN) By Zhaojin Huang,
-
46
README.md DCIC-Group-Image-of-Consumers-----Intelligent-Scoring-of-Credits 比赛:消费者人群画像—信用智能评分 主办方:福建省数字福建建设领导小组办公室 &...
-
77
README.md 评教系统是一款为提高教师的教学质量,反馈学生的心声,提高学校教务管理能力的系统。针对各专业所授课程及教师的评价结果,直观的统计出每位教师的综...
-
62
自定义scoring scoring模块是whoosh控制搜索结果得分的。 使用whoosh自带的scoring就可以实现特别好的搜索结果,但架不住业务上的要求,就比如我们要将搜索结果内在售的排在前面, 而且还要将最近的年份的显示在前面,并...
-
7
Opening Pandora's box by scoring support tickets If you start attaching scores to things, some people will find a way to game it. It's completely ridiculous, but they will get away with it if nobody else can prove it's happening....
-
7
Understanding Similarity Scoring in Elasticsearch ...
-
11
Scoring tennis using finite-state automata For some reason having to do with the medieval French, the
-
5
Another table tennis game is coming to Meta Quest (formerly Oculus Quest) soon. VR Ping Pong Pro is set to arrive on the platform on May 26 and is now listed in the
-
3
Bounce Shot Brings Beer Pong To VR From Table Tennis Devs Skip to content...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK