Last active
April 15, 2026 06:43
-
-
Save cboudereau/0da33a2a2466a8d27443872c9c9225cc to your computer and use it in GitHub Desktop.
tennis kata for csharp with union types (note that the pattern matching lacks of exhaustiveness (L168) at this time)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Tennis Kata — C# 15 Union Types | |
| // Backport of https://gist.github.com/cboudereau/1f3ea7acd6d6712746c6d9f0e258deff | |
| // ── Tests (top-level statements must precede type declarations) ─────────────── | |
| Player left = new Left(); | |
| Player right = new Right(); | |
| static Score Default() => new PointsScore(new Love(), new Love()); | |
| static Score Points(Point l, Point r) => new PointsScore(l, r); | |
| int passed = 0, failed = 0; | |
| void Assert(Score actual, Score expected, string label) | |
| { | |
| if (actual.Equals(expected)) | |
| { | |
| Console.WriteLine($" ✓ {label}"); | |
| passed++; | |
| } | |
| else | |
| { | |
| Console.Error.WriteLine($" ✗ {label} expected={expected.Value} actual={actual.Value}"); | |
| failed++; | |
| } | |
| } | |
| void Run(string name, Action test) | |
| { | |
| Console.WriteLine(name); | |
| test(); | |
| Console.WriteLine(); | |
| } | |
| Run("left_player_always_wins", () => | |
| { | |
| var score = Default(); | |
| Assert(score, Points(new Love(), new Love()), "Love/Love"); | |
| score = left.Win(score); | |
| Assert(score, Points(new Fifteen(), new Love()), "Fifteen/Love"); | |
| score = left.Win(score); | |
| Assert(score, Points(new Thirty(), new Love()), "Thirty/Love"); | |
| score = left.Win(score); | |
| Assert(score, Points(new Forty(), new Love()), "Forty/Love"); | |
| score = left.Win(score); | |
| Assert(score, new Game(new Left()), "Game(Left)"); | |
| }); | |
| Run("left_wins_but_right_was_close", () => | |
| { | |
| var score = Default(); | |
| score = left.Win(score); | |
| Assert(score, Points(new Fifteen(), new Love()), "Fifteen/Love"); | |
| score = right.Win(score); | |
| Assert(score, new FifteenAll(), "FifteenAll"); | |
| score = left.Win(score); | |
| Assert(score, Points(new Thirty(), new Fifteen()), "Thirty/Fifteen"); | |
| score = right.Win(score); | |
| Assert(score, new ThirtyAll(), "ThirtyAll"); | |
| score = left.Win(score); | |
| Assert(score, Points(new Forty(), new Thirty()), "Forty/Thirty"); | |
| score = right.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = left.Win(score); | |
| Assert(score, new Advantage(new Left()), "Advantage(Left)"); | |
| score = right.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = right.Win(score); | |
| Assert(score, new Advantage(new Right()), "Advantage(Right)"); | |
| score = left.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = left.Win(score); | |
| Assert(score, new Advantage(new Left()), "Advantage(Left)"); | |
| score = left.Win(score); | |
| Assert(score, new Game(new Left()), "Game(Left)"); | |
| }); | |
| Run("right_wins_but_left_was_close", () => | |
| { | |
| var score = Default(); | |
| score = right.Win(score); | |
| Assert(score, Points(new Love(), new Fifteen()), "Love/Fifteen"); | |
| score = left.Win(score); | |
| Assert(score, new FifteenAll(), "FifteenAll"); | |
| score = right.Win(score); | |
| Assert(score, Points(new Fifteen(), new Thirty()), "Fifteen/Thirty"); | |
| score = left.Win(score); | |
| Assert(score, new ThirtyAll(), "ThirtyAll"); | |
| score = right.Win(score); | |
| Assert(score, Points(new Thirty(), new Forty()), "Thirty/Forty"); | |
| score = left.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = right.Win(score); | |
| Assert(score, new Advantage(new Right()), "Advantage(Right)"); | |
| score = left.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = left.Win(score); | |
| Assert(score, new Advantage(new Left()), "Advantage(Left)"); | |
| score = right.Win(score); | |
| Assert(score, new Deuce(), "Deuce"); | |
| score = right.Win(score); | |
| Assert(score, new Advantage(new Right()), "Advantage(Right)"); | |
| score = right.Win(score); | |
| Assert(score, new Game(new Right()), "Game(Right)"); | |
| }); | |
| Console.WriteLine($"Results: {passed} passed, {failed} failed"); | |
| if (failed > 0) Environment.Exit(1); | |
| // ── Point ───────────────────────────────────────────────────────────────────── | |
| record Love; | |
| record Fifteen; | |
| record Thirty; | |
| record Forty; | |
| union Point(Love, Fifteen, Thirty, Forty) | |
| { | |
| public Point Next() => Value switch | |
| { | |
| Love => new Fifteen(), | |
| Fifteen => new Thirty(), | |
| Thirty or Forty => new Forty(), | |
| _ => this | |
| }; | |
| } | |
| // ── Player ──────────────────────────────────────────────────────────────────── | |
| record Left; | |
| record Right; | |
| union Player(Left, Right) | |
| { | |
| public Score Win(Score score) => (Value, score) switch | |
| { | |
| (_, Game) => score, | |
| (Left, Advantage { Player: Left }) or | |
| (Left, PointsScore { Left: Forty }) => new Game(this), | |
| (Right, Advantage { Player: Right }) or | |
| (Right, PointsScore { Right: Forty }) => new Game(this), | |
| (Left, PointsScore { Left: Love, Right: Fifteen }) or | |
| (Right, PointsScore { Left: Fifteen, Right: Love }) => new FifteenAll(), | |
| (Left, FifteenAll) => new PointsScore(new Thirty(), new Fifteen()), | |
| (Right, FifteenAll) => new PointsScore(new Fifteen(), new Thirty()), | |
| (Left, PointsScore { Left: Fifteen, Right: Thirty }) or | |
| (Right, PointsScore { Left: Thirty, Right: Fifteen }) => new ThirtyAll(), | |
| (Left, ThirtyAll) => new PointsScore(new Forty(), new Thirty()), | |
| (Right, ThirtyAll) => new PointsScore(new Thirty(), new Forty()), | |
| (Left, PointsScore { Left: Thirty, Right: Forty }) or | |
| (Right, PointsScore { Left: Forty, Right: Thirty }) => new Deuce(), | |
| (_, Deuce) => new Advantage(this), | |
| (Left, Advantage { Player: Right }) or | |
| (Right, Advantage { Player: Left }) => new Deuce(), | |
| (Left, PointsScore pts) => new PointsScore(pts.Left.Next(), pts.Right), | |
| (Right, PointsScore pts) => new PointsScore(pts.Left, pts.Right.Next()), | |
| _ => score | |
| }; | |
| } | |
| // ── Score ───────────────────────────────────────────────────────────────────── | |
| record PointsScore(Point Left, Point Right); | |
| record FifteenAll; | |
| record ThirtyAll; | |
| record Deuce; | |
| record Advantage(Player Player); | |
| record Game(Player Player); | |
| union Score(PointsScore, FifteenAll, ThirtyAll, Deuce, Advantage, Game); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment