Andy Murray as an event source

With Wimbledon’s final this weekend, I recalled that I had not blogged about the “Evil Tennis kata” I played with, a few months ago. Its aim was to compute a tennis score from a series of game events. This kata is inspired by the original tennis kata, but does not stop at the game level and computes the score for a whole match.

Let’s consider a tennis match. We can see it as a list of points, either won by player A or player B. For instance, here are the first points from Sunday’s final:

["Murray"; "Murray"; "Murray"; "Djokovic"; "Djokovic"; "Djokovic"; "Djokovic"; "Djokovic"; "Murray"; "Murray"; "Murray"; "Djokovic"; "Murray"; "Djokovic"; "Murray"; "Murray"; "Murray"; "Djokovic"; "Djokovic"; "Djokovic"; "Murray"; "Murray"; "Djokovic"; "Murray"; "Djokovic"; "Murray"; "Murray"; "Djokovic"; "Djokovic"… ]

One of the goals of the kata is to be able to determine the final score from the match events. It can also be to print nicely every score from the start until the end, like it is done for instance on the following page:
http://www.flashscore.com/match/SSmzQE8d/#point-by-point;1

Once again, it has been a nice way to practice F#:

type Player = PlayerA | PlayerB
type GameEvent = Point of Player
type GameScore = GameScore of int * int
type SetScore = GameScore list
type Match = SetScore list

type Outcome =
    | Win of Player
    | Pending

I use a single union type to define the score of a game, composed of the number of points won by player A and the number of points won by player B. For instance, GameScore(3,1) means that the current game score is 40-15. Using classical integers to count the points is much easier than trying to reason directly in the tennis scoring system, which is seriously twisted. In order to display a classical score from this representation, here is the first function:

let displayGame (GameScore(a, b)) =
    let displayScore score =
        match score with
        | 0 -> "0"
        | 1 -> "15"
        | 2 -> "30"
        | 3 -> "40"
        | _ -> failwith "Invalid argument score"
    match a, b with
    | a, b when a <= 3 && b <= 3 -> sprintf "%s - %s" (displayScore a) (displayScore b)
    | a, b when a = b -> "Deuce"
    | a, b when a = b + 1 -> "Adv A"
    | a, b when a = b - 1 -> "Adv B"
    | a, b when a > b -> "Game A"
    | _ -> "Game B"

Now what we want is to be able to build a GameScore from a series of GameEvent. This can be achieved nicely with a function and a fold:

let updateGame (GameScore(a, b)) event =
    let newGame =
        match event with
        | Point(PlayerA) -> GameScore(a + 1, b)
        | Point(PlayerB) -> GameScore(a, b + 1)
    newGame

[Point(PlayerA);Point(PlayerA);Point(PlayerB);Point(PlayerA)]
|> Seq.fold updateGame (GameScore(0, 0)) 
|> displayGame

The previous sample code prints out the score : “40 – 15”.

When we try to go a step further, displaying the score of a particular game becomes less interesting. What we really want is to check the victory condition at the game level, in order to determine whether the next point will start a new game:

let getGameOutcome (GameScore(a, b)) =
    match (a, b) with
    | a, b when a > 3 && a > b + 1 -> Win(PlayerA)
    | a, b when b > 3 && a < b - 1 -> Win(PlayerB)
    | _ -> Pending

From there, we can build an updateSet method:

let updateSet previousSet event =
    match previousSet with
    | previousGame :: previousGames ->
        match getGameOutcome previousGame with
        | Win(_) -> updateGame (GameScore(0, 0)) event :: previousGame :: previousGames
        | Pending -> updateGame previousGame event :: previousGames
    | [] -> updateGame (GameScore(0, 0)) event :: []

Similarly, we need to determine at some point whether the set has been won. In order to do this we have to count the games won by each player. Here is how I did it:

let countWins games =
    let rec countWinsRec games (a, b) =
        match games with
        | game :: otherGames ->
            let wins =
                match getGameOutcome game with
                | Win(PlayerA) -> (a + 1, b)
                | Win(PlayerB) -> (a, b + 1)
                | _ -> (a, b)
            countWinsRec otherGames wins
        | _ -> (a, b)
    countWinsRec games (0, 0)

From there, we can determine whether a set is won:

let getSetOutcome (setScore:SetScore) =
    let (a, b) = countWins setScore
    match (a, b) with
    | a, b when a > 5 && a > b + 1 -> Win(PlayerA)
    | a, b when b > 5 && a < b - 1 -> Win(PlayerB)
    | _ -> Pending

The next step is to move to the match level:

let updateMatch previousMatch event =
    match previousMatch with
    | previousSet :: previousSets ->
        match getSetOutcome previousSet with
        | Win(_) -> updateSet [] event :: previousSet :: previousSets
        | Pending -> updateSet previousSet event :: previousSets
    | [] -> updateSet [] event :: []

Finally, we can use the updateMatch method in a fold to build a representation of the match:

let buildMatch events = List.fold updateMatch [] events

The result is a reversed list of reversed list of GameScores…

[[GameScore (6,8); GameScore (1,4); GameScore (2,4); GameScore (2,4);
  GameScore (4,1); GameScore (4,1); GameScore (4,2); GameScore (4,2);
  GameScore (0,4); GameScore (2,4)];
 [GameScore (0,4); GameScore (2,4); GameScore (2,4); GameScore (4,0);
  GameScore (5,7); GameScore (4,6); GameScore (1,4); GameScore (4,2);
  GameScore (4,1); GameScore (4,2); GameScore (3,5); GameScore (4,1)];
 [GameScore (0,4); GameScore (5,3); GameScore (5,7); GameScore (0,4);
  GameScore (0,4); GameScore (4,2); GameScore (4,1); GameScore (6,8);
  GameScore (1,4); GameScore (5,3)]]

That is not a very readable result… Here I go for a last function:

let displayMatchScore events = 
    let displaysWins (a, b) = sprintf "%i - %i" a b
    let displaySet games = displaysWins (countWins games)
    let rec getSetScoreToDisplay sets =
        match sets with
        | games :: [] ->
            match games with
            | game :: _ -> displaySet games :: []
            | _ -> displaySet games :: []
        | games :: otherSets -> displaySet games :: getSetScoreToDisplay otherSets
        | [] -> failwith "Error"
    getSetScoreToDisplay (buildMatch events |> List.rev)

This last one aggregates the events, in order to display a much more simple list of scores:

let score = displayMatchScore events

And the final score is displayed : ["4 - 6"; "5 - 7"; "4 - 6"] !

This entry was posted in Events and tagged , . Bookmark the permalink.

Comments are closed.