Skip to content

Instantly share code, notes, and snippets.

@cboudereau
Last active April 15, 2026 06:43
Show Gist options
  • Select an option

  • Save cboudereau/0da33a2a2466a8d27443872c9c9225cc to your computer and use it in GitHub Desktop.

Select an option

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)
// 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