Advent of Typescript 2023 Day 21 : TIC TAC TOE
Tags: typescript, adventofcode, adventoftypescript
Published: 2023-12-21 12:34:41

Hello Typescript Wizards, i hope you are having fun with the Advent of Typescript 2023.
With Trash we created some awesome challenges for you to solve this year.
I'm starting to do Blog posts to explain the solutions to the challenges since they are getting more and more complex.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4y2hi3to9u7pzs3pz0h2.png)

## Description

This challenge is about creating a Tic Tac Toe game in typescript type system.
For this we are working with some simple primitives types to describe the game.

**The game**
```ts
type TicTacToeChip = '❌' | '⭕';
type TicTacToeEndState = '❌ Won' | '⭕ Won' | 'Draw';
type TicTacToeState = TicTacToeChip | TicTacToeEndState;
type TicTacToeEmptyCell = '  ';
type TicTacToeCell = TicTacToeChip | TicTacToeEmptyCell;

type TicTactToeBoard = TicTacToeCell[][];

type TicTacToeGame = {
  board: TicTactToeBoard;
  state: TicTacToeState;
};
```

The game is played on a 3x3 board, each player has a chip, the first player has the `❌` chip and the second player has the `⭕` chip.
The game is won when a player has 3 chips in a row, column or diagonal.
The game is a draw when the board is full and no player has won.

The Game is represented by a `TicTacToeGame` object, which has a `board` Matrix and a `state`.

The `board` is a 3x3 matrix of `TicTacToeCell` which can be either a `TicTacToeChip` or a `TicTacToeEmptyCell`.

## The challenge

Playing the game is done by calling the `TicTacToe` Utility type with the current game state and the next move.
The utility type will return the next game board and state.

**Example**
```ts
  Expect<
    Equal<
      TicTacToe<NewGame, 'top-center'>,
      {
        board: [
          [ '  ', '❌', '  ' ],
          [ '  ', '  ', '  ' ],
          [ '  ', '  ', '  ' ]
        ];
        state: '⭕';
      }
    >
  >
```

## Solution to place a chip on the board

Here we will follow a step by step approach to solve this challenge.

### Step 1: Map move to board position

The board can be described as a 3x3 matrix of `TicTacToeCell`.
Row and column are 0 based index, starting from the top left corner.

Here a mapping of the board position to the row and column index, keeping the index as string. This will come in handy later.

```ts
type PositionMap = {
  'top-left': ['0', '0'];
  'top-center': ['0', '1'];
  'top-right': ['0', '2'];
  'middle-left': ['1', '0'];
  'middle-center': ['1', '1'];
  'middle-right': ['1', '2'];
  'bottom-left': ['2', '0'];
  'bottom-center': ['2', '1'];
  'bottom-right': ['2', '2'];
};
```

### Step 2: Set the chip on the board

Now that we have a mapping of the board position to the row and column index, we can set the chip on the board using Object Mapping.
Indeed we learned in previous challenges that we can use the `Object` Mapping to do a For Loop on Tuples.

We can divide the problem in 2 parts:
- Set the chip on the row
- Set the row on the board

We are also carreful to not override an existing chip on the board.
In such case we keep the existing board state.

**Notice** that we use `K extends index` to only set the chip on the row index. `K`  is a string, that's why the mapping is done with string index.

```ts
type PlaceChipOnTuple<
  T extends TicTacToeCell[],
  X,
  Chip extends TicTacToeChip
> = {
  [K in keyof T]: K extends X ? 
    T[K] extends '  ' ?  // set only if the cell is empty
      Chip : 
      T[K] : 
    T[K];
};

type PlaceChipOnMatrix<
  Board extends TicTactToeBoard,
  Position extends [string, string],
  Chip extends TicTacToeChip,
  $y = Position[0], // alias for row index
  $x = Position[1]  // alias for column index
> = {
  [K in keyof Board]: K extends $y
    ? PlaceChipOnTuple<Board[K], $x, Chip>
    : Board[K];
};
```

We are not done yet, we can create a utility type to set the chip on the board given the current state. If the state is a win or a draw, we keep the current board.

**Notice a trick here**, we use `Extract` to cast the result to `TicTactToeBoard` since typescript has a hard time to infer the type.

```ts
type PlaceChip<
  Board extends TicTactToeBoard,
  Position extends [string, string],
  State extends TicTacToeState
> = State extends TicTacToeChip
  ? Extract<PlaceChipOnMatrix<Board, Position, State>, TicTactToeBoard>
  : Board;
```

## Solution to check if the game is won

Now that we can place a chip on the board, we can check if the game is won.
For this we need to check if we have 3 same chips is in a row, column or diagonal.

### Step 1: Check if rows are won

#### Enabler

We can check if a row is won by checking if all the chips in the row are the same.
We create a utility type to check if a row is won, given a Tuple.
This will come in handy for other checks.

```ts
type CheckCells<T extends TicTacToeCell[]> = 
  T extends [infer A, ...infer B]
  ? B[number] extends A
    ? A extends '  '
      ? false
      : A
    : false
  : false;
```

here we check that the first cell is the same as the rest of the cells in the row. We also check that the first cell is not empty to be sure that it's a Chip.
**Note** that we return the Chip if the row is won, this will come in handy later.

#### Check one row

So to check if a row is won, we can use the `CheckCells` utility type on the Board

```ts
type CheckRow<Board extends TicTactToeBoard, Row extends number> = 
  CheckCells<Board[Row]>;
```

#### Check all rows

Here we use a trick to check all the rows, we use typescript [Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types) to iterate over the rows.

```ts
type CheckRows<
  Board extends TicTactToeBoard,
  $iter extends number = 0 | 1 | 2
> = $iter extends number
  ? CheckRow<Board, $iter> extends infer Chip extends TicTacToeChip
    ? Chip
    : never
  : never;
```

### Step 2: Check if columns are won

#### Check one column

We can reuse the `CheckCells` utility type to check if a column is won.

```ts
type CheckColumn<
  Board extends TicTactToeBoard,
  Column extends number
> = CheckCells<[Board[0][Column], Board[1][Column], Board[2][Column]]>;
```

#### Check all columns

It's the same as rows now :

```ts
type CheckColumns<
  Board extends TicTactToeBoard,
  $iter extends number = 0 | 1 | 2
> = $iter extends number
  ? CheckColumn<Board, $iter> extends infer Chip extends TicTacToeChip
    ? Chip
    : never
  : never;
```

### Step 3: Check if a diagonal is won

#### Enabler

We can reuse the `CheckCells` utility type to check if a diagonal is won.
But first we create a Map to get the diagonal cells from the board.

```ts
type DiagonalMap = [
  [[0, 0], [1, 1], [2, 2]], 
  [[0, 2], [1, 1], [2, 0]]
];
```

#### Check one diagonal

Now we can use the `DiagonalMap` to get the diagonal cells from the board and check if the diagonal is won.

```ts
type CheckDiagonal<
  Board extends TicTactToeBoard,
  N extends number
> = CheckCells<
  [
    Board[DiagonalMap[N][0][0]][DiagonalMap[N][0][1]],
    Board[DiagonalMap[N][1][0]][DiagonalMap[N][1][1]],
    Board[DiagonalMap[N][2][0]][DiagonalMap[N][2][1]]
  ]
>;
```

#### Check all diagonals

It's the same as rows and columns now :

```ts
type CheckDiagonals<
  Board extends TicTactToeBoard,
  $iter extends number = 0 | 1
> = $iter extends number
  ? CheckDiagonal<Board, $iter> extends infer Chip extends TicTacToeChip
    ? Chip
    : never
  : never;
```

### Step 4: Check if the game is won

We can combine the previous checks to check if the game is won by using all the checks in a union.

```ts
type Winner<Board extends TicTactToeBoard> = CheckRows<Board> | CheckColumns<Board> | CheckDiagonals<Board>;
```

### Step 5: Check if the game is a draw

checking if the game is a draw is simple, we just check if the board is full. And it's full if there is no empty cell.

```ts
type CheckFilled<Board extends TicTactToeBoard> =
  '  ' extends Board[number][number] ? false : true;
```

## Implementing the TicTacToe Utility type

#### Step 1: Next game state

Now that we have all the utilities to check if the game is won or a draw, we can implement a `GameNextState` utility type, so that after a move we can check if the game is won or a draw.

```ts
type NextGameState<
  Board extends TicTactToeBoard,
  State extends TicTacToeState,
  $winner = Winner<Board>
> = [$winner] extends [never]
  ? CheckFilled<Board> extends false
    ? State extends '❌'
      ? '⭕'
      : '❌'
    : 'Draw'
  : `${$winner & string} Won`;
```

#### Step 2: Implement the TicTacToe Utility type

Now that we have all the utilities to place a chip on the board and check if the game is won or a draw, we can implement the `TicTacToe` utility type.

**Notice 1** that we use `PositionMap[Position]` to get the row and column index from the position.
**Notice 2** that we use `PlaceChip` to place the chip on the board.
**Notice 3** that we use `NextGameState` to get the next game state.

And we use `$` default variables to simplify the logic.

```ts
type TicTacToe<
  Game extends TicTacToeGame,
  Position extends TicTacToePositions,
  $Position extends [string, string] = PositionMap[Position],
  $nextBoard extends TicTactToeBoard = PlaceChip<
    Game['board'],
    $Position,
    Game['state']
  >
> = {
  board: $nextBoard;
  state: $nextBoard extends Game['board']
    ? Game['state']
    : NextGameState<$nextBoard, Game['state']>;
};
```

## Conclusion

We have seen how to create a Tic Tac Toe game in typescript type system.
We have seen how to use Object Mapping to iterate over Tuples.
We have seen how to use Distributive Conditional Types to iterate over a range of numbers.
We have seen how to use `Extract` to cast a type.
We have seen how to use `$` default variables to simplify the logic.

You can check the full solution on [TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgUQI4FcCGAbANCgDzAFMBjeAXzgDMoIQ4ByGATxIFoZiBnGYAOwDmjANwAoMaxJwAKsFIzMCiMQDCAC2Bg4AXiaAZckZwAPk0Cq1KMltis+YuXFk-ACYBlGJi66DcAOoR+I1NGMz8AoKYAEShMAHdLKRs5BSUZFXdPGz1k+zS1TW1THNSVJzcPLnFE2xSHZHBWVWJsbG8jJirrGtyVJpbvYocNLRNukscGlj7sTulBvIBNAAUIbmA+AO42mAgwCMYQYGdnbGJ9gCMIGB2QBK75lQANFbWN-i29RlPqGH3SYn4XCg+ygwEE6l+sySdnGL3WwE23gABgASBAPYjLVbwzYUdhojHPbFvbgUJFQsbkPIAIQgmCgzgGMKGzWwAG0ALqcikYgDimBAWUQYjgcEu9OcAC5KTAaXSGeJRbxMtKMRlKmIKOIrNJ6mBWLSJd42Wz2ox8GaLaLGBz8KbrVamI6bXbLUwHe6mByOdrqgA5YixfmC7wIEVi+VSlCTQ0K8PKrjSxiGcRaiTVOFvACymG0ejD1p2YHY31+0tNAAZzUwqz7w8xdux-oDiMDy4wqxaAIw2xVMIvsUHgstwSvVxgAJl79cOx1OJeIP0Y7Z7FtrfYORxOxCbAKBy9HjFXTB7dets+3g7BEIPpuPk+n1su13oC6X7ana8fTGfN13LbbQ9PxPb9GF-V8hxvD9xynOs0x1GwlmwJR8i0AB5fgZHQMBTgAHnDGQ4GIAguBcLYMWmTlcHDR5qNFYZtGI0jnHI5k8gYsQAD5Q3DNkAGk4AEOAAGtiBYCBqFkDlpQEpiARYuBHjgAB+OAAAoZH4jkiJI+StjNFS4AYuBVS0gBKEzZC01NfS6JCUIYjCcxgUECHw0VY0ZOSyJlOUJTouBMwRfgdOYrY2V4UEhHwSKBEEW1w2M7yFIogoApRFhvCCgI2QrBLRRRAgsuJYK2S7DkuJ40V+MEkLRPEyTPOkuBZN0nyMvDUVVPs-5HMw7C8M8rT8EK-AGM4zrLKGvi4Ns6QetQsB3LgTzQr03yVE8gLspC5Lwti6K4AO+KAvVGw9spPIzsqvQzrWnzUq0cNVOQEiYnIXCFr65zXNwrbApKgJ8DOzj8EGWVNsjCbRWlTy5psDQyGE6ZuFwwiLoo1lOW47J7oUtkBGoVs4AAQXwAA6SnCeJ6kKq6la2X4dAQHOVttIuknJtUkm8f061Jvp6gcG4YgBcsznRRhmhhdFqWhewEX4aM9QkYAJQgWI-sjXmNuIf71diHWmZZ1sceVpGUeWoaDYqzilcR0hhIN1Hw1WjHmQhvXI3S9ZiYu43WagbwK1GLtRgnG64BRX2g-95nA+e83HYNrX-KjmPuIu6mg6StqUrY3oCi55Wnslyz+GIAA3Vtw2lCvq6ge2Vcd1QIGwZn+Ct7X3dqT2tsStuO6N+Oa7Nh3kdZVG2SGvK2Vb9uQH4W0VsjMquXnjvl6Gid18HxfvTt9MunHjfF5djzu7z1je78hkfaBYeTaDvQQ9MMPTAjs3o4fuOn8Tk+96d3+t-U2Ots4l0YlfS6hdS6S1UhxMudcq41ylvXVsStIjAEwIIAIOAcx5lHCaCs+A8p2i7PgcqdoJz4B3svIhNC6HkLgJQ0c1C4B5W9E3JGmDsG4OwF3I0Pd7B929uGP0j8E5j2bhPFo59RyTSGjwnB-A8G5jZH6Lks8OFsiUXw-B6jNFcnKvlSWiisHKNUWAAxa9creh0eYvRaiNE2OMQFC+Ep7G8JUdgfRzid62K5Lo7xviuT+OMeGW2XDHZBOFgIhkOtwa32cPfP2UCA7ExfqHSOIDY5pJHlAf+0iYn8OARnMB-AiY5wKAkgui1i4IMlkghutc4BoMbkfaQvgBAVygKneJQilAiIlDjcMphx7Oz6c4aGowAEL3eJM6ZYyikOO8ajTyh8EJJ2EgAMWAC0YgzhJk1JvpDYZuh6zWh1kNdJUAuQ3O0qpeWItLIuXQMQJWAYSLBmIGdOJXkoGJNOXfcMd0BkODOulWI3SMl+Ghb09Zkc2QoihfwHp7MoGM2QbcwpSNdn7MOesnWTzZZwLgKCqByZGBi1UiEKlZcpaUsmkmaIcQ6WWVRAgZFcK4AADIjouTilQfw-ByQdOhLUPIy1vnHJ6MQb521Aa7QBbUna3B0o7R1hFAVh1jraT0DtfRO0TEogriRN2yqTleyNPq5CvUCjLVFN8004oGQukmiiHabi4BOsYAmM4dM4ATTNgWCMEppQmqYnDeMFRiDhtNTAc1YVvUCmIM6yMNpi4+r9RmxBcBPkwG+b8iNZrvbJsFKabNHJD7wWqFwXgAB9EAEBq5dnrYMrArRsi1NwgGIMKaLQDmbPuQ+AB6EdksAB6ylNl1pgI25txBW3ERIOQA5VVQ0MnLJNNknpxyGGdHAEx1Vd3OgPUe0cJ7L0GQDWeflKpzCWDTLWngc6m0tu8K9FdMBcJoA7bhWd86W1tvIB2-AAG32LvrcusgXApkbOfQ2iDE5gMwA7UyCVKh-0vsA5B9tOAB2NlLIwUd47RRTpndhpDUGiAwbXfmcMLqoxsm3bSi0+7d3LxYx6N0ZpONl3tE6K91pOHRvvZS1MFHEMLonB+mjH1f04Cw1J6uyG8N4DgOB6T1Gv0HM4vBromnq4AGYUNoa7Rh4gSnX1abUxaC884h2tmI+IMdk7p0Ies8Z7TtHGT0dFIxrd-GH1sfHJc89O6ePsevV6iL3G4vuhvX2P1SYLA2Uk554gRnZNfp-RgRThnMumfwxpyjC6TPQdXXBpWBWAAsRXO3QMswVkztmfxXD-ERkjbn0s4bqxV2D66AvyKC6xgwoWEsxavVFsLk3Rs8bCxEpLMakwpk1BSWr2WYO5b-bV+rYHSvVz63J2DentQ9YIPWlF9X0Oyqs71vbbWXwgH-MOlzpG4DkY8-Wi7V3+t0eFP5yMgXJYRYsCFs9s3uPTYm9u4LY2z2LdE4mHwwrH3rewz9oSehP1bYU-wgDmP+APYJ5dgQ3nKunbFRp6wOGACs13zO3d261sC7WILXl+F1sj7mDM04g-Tv7vmAcbqY7Dub0OjDham+NyXs2wfS8PYjpUy2H1pa+-zzb8m8v475wu+nrXEh0-Jyd-T0gAMQFJ0TtTN3xj-t19XfXIHiubjnDuTrb3utfYt7947-2Q1DeYyN+XlKIdi+DxLw9cuZchcV6KW9yXzBhECGlsQrmBCVxwEcOAEG4DOHCPAUg6hMBCBsDAFWIu4DF8ZH6nr6fM-OAZ41u7EHW0s8HXuJzXOPs87N9huv2AjjG79wxoHw2QfS9DyN+LMvI9i+nwjuPS2xOpbW7X-gGeB9C5x1rnbff1-1+J3vjfg-BeU566zag0BiD1ucDEQ2fmRfA+quL8cFg+Mg5fyF9-z+ovB+E-HlXWlCTL7W-OIRvDEO7C-K-G-O-C0cCZ7SCTnD3bnHrUA2IIfIXf3UfQPD-cPV-F0OAFjPAr-fAFjX-fAkTZXMTFleIYAgzbDNAzXb9PHO7NAw-BtNg0-O2IAA).

Hope you enjoyed this challenge, see you next time.