Tennis Refactoring
Level: Intermediate 30–60 minConcepts: RefactoringLegacy CodeStrings
Solutions: C# | TypeScript | Python
You are given a working tennis scoring function. The code is correct but hard to read. Your job is to refactor it into something clean — without breaking existing behavior.
This is a refactoring kata. Write characterization tests first, then improve the code.
Tennis Scoring Rules
- A game starts at 0-0 (called “Love-All”)
- Scores go: 0 (“Love”), 1 (“Fifteen”), 2 (“Thirty”), 3 (“Forty”)
- If both players have 40, it’s “Deuce”
- If the score is Deuce and a player scores, they have “Advantage”
- If the player with Advantage scores again, they win
- If the other player scores when their opponent has Advantage, it goes back to Deuce
The Legacy Code
function getScore(p1Score, p2Score, p1Name, p2Name) {
var result = "";
if (p1Score == p2Score) {
if (p1Score == 0) result = "Love-All";
else if (p1Score == 1) result = "Fifteen-All";
else if (p1Score == 2) result = "Thirty-All";
else if (p1Score >= 3) result = "Deuce";
} else if (p1Score >= 4 || p2Score >= 4) {
var diff = p1Score - p2Score;
if (diff == 1) result = "Advantage " + p1Name;
else if (diff == -1) result = "Advantage " + p2Name;
else if (diff >= 2) result = "Win for " + p1Name;
else if (diff <= -2) result = "Win for " + p2Name;
} else {
var scores = ["Love", "Fifteen", "Thirty", "Forty"];
result = scores[p1Score] + "-" + scores[p2Score];
}
return result;
}
Your Task
Step 1: Write characterization tests. Cover every scoring combination. Your tests should capture the exact behavior of the current code.
Step 2: Refactor. Improve readability, extract concepts, remove duplication. Keep all tests green throughout.
Test Cases
| Player 1 Score | Player 2 Score | Output |
|---|---|---|
| 0 | 0 | Love-All |
| 1 | 1 | Fifteen-All |
| 2 | 2 | Thirty-All |
| 3 | 3 | Deuce |
| 4 | 4 | Deuce |
| 1 | 0 | Fifteen-Love |
| 0 | 1 | Love-Fifteen |
| 2 | 1 | Thirty-Fifteen |
| 3 | 1 | Forty-Fifteen |
| 4 | 3 | Advantage Player1 |
| 3 | 4 | Advantage Player2 |
| 5 | 4 | Advantage Player1 |
| 5 | 3 | Win for Player1 |
| 3 | 5 | Win for Player2 |
Bonus
- Refactor to use polymorphism — replace conditionals with a scoring strategy per game state
- Add a
TennisGameclass that tracks points viawonPoint(playerName)and reports score viagetScore() - Support tiebreak scoring (numeric, first to 7, win by 2)
Hint
The code has three distinct states: equal scores, endgame (scores ≥ 40), and normal play. Each state has different formatting rules. Extracting these into named methods or separate functions makes the logic immediately clearer.
Reference Walkthrough
Reference implementations in C#, TypeScript, and Python live at tddbuddy-reference-katas/tennis-refactoring. This is an F2 refactoring kata classified characterization test set only: the legacy function’s behavior is captured in a shared contract (SCENARIOS.md) that every language satisfies byte-for-byte, and each implementation refactors the legacy’s nested conditionals into three named branches (equal / endgame / in-play) with POINT_NAMES lifting the magic ["Love","Fifteen","Thirty","Forty"] array into a named lookup.
No builder in this one. Unlike Calc Refactor (the other F2 refactoring kata), tennis scoring is a pure four-argument function — a MatchBuilder would cost more lines than the scenarios it replaces. The F2 discipline here is the shared characterization contract itself.
Cousin kata — Tennis Score is the Pedagogy mode twin: same domain, different pedagogical question. Tennis Score shows how a clean tennis scorer emerges from TDD low-gear triangulation through the state-machine refactor at Advantage. Tennis Refactoring starts from legacy code and gets to the same destination via characterization tests. Pick Tennis Score to practice the TDD rhythm; pick Tennis Refactoring to practice the legacy-code-then-clean-code move.
- C# (.NET 8, xUnit, FluentAssertions 6.12.0) — walkthrough
- TypeScript (Node 20, Vitest 1.6, TS 5 strict) — walkthrough
- Python (3.11, pytest) — walkthrough
This kata ships in Agent Full-Bake mode at middle gear, the F2 tier’s characterization test set only sub-classification. See the repo’s Gears section for why middle gear is the deliberate choice once the legacy’s promises are written down.
Related reading
- Katas Are Rehearsal, Not Performance
You don't practice TDD on production code. You practice on katas and bring the muscle memory to production. The gap between knowing TDD and doing TDD is reps. - Agents Amplify Whatever Vocabulary They Find
Agents do not bring vocabulary to your codebase. They mirror what's already there. Strong vocabulary gets agent contributions that read like the team. Weak vocabulary gets faster mediocrity. The work you did for craft reasons just became leverage. - The Hidden Output of TDD Was Never Code
The visible artifact of TDD is a passing test suite. The actual artifact, the one that compounded value over the life of the codebase, was the vocabulary the team built while writing it. The dictionary, not the regression net.