Solving Rock, Paper, Scissors in TypeScript

ยท

4 min read

We have officially started the last two weeks of the year, I can already feel Mariah Carey taking over the background noise at the dinner table, ah let's get started it before I get more sentimental.

Let's keep the fun going, today we are going to be playing a little bit of Rock-Paper-Scissors!

Day sixteen - Dec 16

I strongly encourage you to try the first challenge on your own before reading the solution here.

Challenge: Rock, Paper, Scissors

The challenge

It's Sunday and there's one week to go before the big day (Christmas Eve) when the elfs' work for the year will finally be complete. For the last 20 years the only game the elves have had to play together is StarCraft. They're looking for a fresh game to play.

So, they get the idea to try a Rock, Paper, Scissors tournament.

But the elves are sorta nerdy so they want to accomplish this using TypeScript types. The WhoWins should type to correctly determine the winner in a Rock-Paper-Scissors game. The first argument is the opponent and the second argument is you!

What's Rock, Paper, Scissors?

In case you haven't played it before, basically:

  • it's a two player game where each player picks one of three options: Rock (๐Ÿ‘Š๐Ÿป), Paper (๐Ÿ–๐Ÿพ), and Scissors (โœŒ๐Ÿฝ)

  • game rules:

    • Rock crushes Scissors (Rock wins)

    • Scissors cuts Paper (Scissors wins)

    • Paper covers Rock (Paper wins)

    • otherwise, a draw

The code to complete

type RockPaperScissors = '๐Ÿ‘Š๐Ÿป' | '๐Ÿ–๐Ÿพ' | 'โœŒ๐Ÿฝ';

type WhoWins = unknown;

The tests

import { Expect, Equal } from 'type-testing';

type test_0_actual = WhoWins<'๐Ÿ‘Š๐Ÿป', '๐Ÿ–๐Ÿพ'>;
type test_0_expected = 'win';
type test_0 = Expect<Equal<test_0_expected, test_0_actual>>;

type test_1_actual = WhoWins<'๐Ÿ‘Š๐Ÿป', 'โœŒ๐Ÿฝ'>;
type test_1_expected = 'lose';
type test_1 = Expect<Equal<test_1_expected, test_1_actual>>;

type test_2_actual = WhoWins<'๐Ÿ‘Š๐Ÿป', '๐Ÿ‘Š๐Ÿป'>;
type test_2_expected = 'draw';
type test_2 = Expect<Equal<test_2_expected, test_2_actual>>;

type test_3_actual = WhoWins<'๐Ÿ–๐Ÿพ', '๐Ÿ‘Š๐Ÿป'>;
type test_3_expected = 'lose';
type test_3 = Expect<Equal<test_3_expected, test_3_actual>>;

type test_4_actual = WhoWins<'๐Ÿ–๐Ÿพ', 'โœŒ๐Ÿฝ'>;
type test_4_expected = 'win';
type test_4 = Expect<Equal<test_4_expected, test_4_actual>>;

Note: The second param (think about it as "Player B") is the one that is being evaluated for whether it wins, loses, or results in a draw.

The brute force solution

There are a couple of ways of solving this problem, let's start with the more "naive" approach.

type RockPaperScissors = '๐Ÿ‘Š๐Ÿป' | '๐Ÿ–๐Ÿพ' | 'โœŒ๐Ÿฝ';

type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> = 
    A extends B ? 'draw' : 
    [A, B] extends ['๐Ÿ‘Š๐Ÿป', '๐Ÿ–๐Ÿพ'] ? 'win' :
    [A, B] extends ['๐Ÿ‘Š๐Ÿป', 'โœŒ๐Ÿฝ'] ? 'lose' : 
    [A, B] extends ['๐Ÿ–๐Ÿพ', 'โœŒ๐Ÿฝ'] ? 'win' : 
    [A, B] extends ['๐Ÿ–๐Ÿพ', '๐Ÿ‘Š๐Ÿป'] ? 'lose' : 
    [A, B] extends ['โœŒ๐Ÿฝ', '๐Ÿ‘Š๐Ÿป'] ? 'win' : 
    [A, B] extends ['โœŒ๐Ÿฝ', '๐Ÿ–๐Ÿพ'] ? 'lose' : never;

The parameters A and B represent the players, which have to be a value of the RockPaperScissors union.

Then we have a couple of conditional types, the first one:

A extends B ? 'draw' :

This translates to if both players choose the same move, the result is a draw.

Then we have a series of conditionals that represent all the possible moves. For the first move:

[A, B] extends ['๐Ÿ‘Š๐Ÿป', '๐Ÿ–๐Ÿพ'] ? 'win' :

If the first player chooses '๐Ÿ‘Š๐Ÿป' and the second player chooses '๐Ÿ–๐Ÿพ', the first player wins.

Then we have similar conditional checks for other combinations.

This solves the problem, but is far from DRY code, since we are hard coding all the different moves, we can definitely do better.

The optimized solution

type RockPaperScissors = '๐Ÿ‘Š๐Ÿป' | '๐Ÿ–๐Ÿพ' | 'โœŒ๐Ÿฝ';

type WinningMoves = {
  '๐Ÿ‘Š๐Ÿป': 'โœŒ๐Ÿฝ';
  '๐Ÿ–๐Ÿพ': '๐Ÿ‘Š๐Ÿป';
  'โœŒ๐Ÿฝ': '๐Ÿ–๐Ÿพ';
};

type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> =
  A extends B ? 'draw' :
  B extends WinningMoves[A] ? 'lose' :
  'win';

A really nice clean way of solving this problem is to leverage a dictionary of WinningMoves:

type WinningMoves = {
  '๐Ÿ‘Š๐Ÿป': 'โœŒ๐Ÿฝ';
  '๐Ÿ–๐Ÿพ': '๐Ÿ‘Š๐Ÿป';
  'โœŒ๐Ÿฝ': '๐Ÿ–๐Ÿพ';
};

This type represents a dictionary where each key (a move) has a corresponding value (the move it defeats).

Let's now break down what's happening in the WhoWins type:

type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> =
  A extends B ? 'draw' :
  B extends WinningMoves[A] ? 'lose' : 'win';

Here the params are the same as the first solution, A and B = the players, representing a value of the RockPaperScissors union.

Then we have two conditional types, the first one:

A extends B ? 'draw' :

This line is saying that If both players choose the same move, the result is a draw.

The second conditional type:

B extends WinningMoves[A] ? 'lose' : 'win';

Here we are stating that If the move chosen by the second player (B) is a winning move against the move chosen by the first player (A), then the first player loses.
Otherwise, the first player wins.

The WinningMoves is doing a lot of the heavy lifting for this solution, resulting in a short and elegant answer.

In full transparency, it just feels good when you keep getting better at something and things start to become "easier".
Thanks for sticking around for another TypeScript challenge, one less challenge, and one day closer to Christmas ๐ŸŽ„๐ŸŽ…๐Ÿผ!

If you like this content consider checking out what I post on Twitter/X ๐Ÿฆ

ย