typescriptの型だけでマルバツゲームを作る

# はじめに

どうも私です

先日とある記事を読んでいて`TypeScriptで三目並べを型チェックだけで書くことができる`という文言を目にしました

まさかまさかそんなできるわけ...tsならできかねないですね

ということで今回は半分お遊びのマルバツゲームをtypescriptで作ってみたという話です

`読んでいた記事`

https://logmi.jp/tech/articles/330373

# できたもの

できたものがこちらです

おおよそ80行ほどで実装できました(おそらくもっと短く作ることはできますが...)

```ts
type Player = "A" | "B";

type Col = 0 | 1 | 2;

type Row = 0 | 1 | 2;

type Square = [Col, Row];

type Board = Square[];

type Play = [Square, Player];

type PlayHistory = Play[];

type PlayerHistory<
  B extends PlayHistory,
  P extends Player,
  History extends Square[] = []
> = B extends [infer First extends Play, ...infer Rest extends Play[]]
  ? First[1] extends P
    ? PlayerHistory<Rest, P, [First[0], ...History]>
    : PlayerHistory<Rest, P, History>
  : History;

type MarkCount<
  B extends Board,
  D extends 0 | 1,
  R extends Row,
  Count extends any[] = []
> = B extends [infer First extends Square, ...infer Rest extends Board]
  ? First[D] extends R
    ? MarkCount<Rest, D, R, [First, ...Count]>
    : MarkCount<Rest, D, R, Count>
  : Count["length"];

type IncludesTuple<
  T extends readonly any[],
  Tuple extends readonly any[]
> = T extends [infer First, ...infer Rest]
  ? IsSame<First, Tuple> extends true
    ? true
    : IncludesTuple<Rest, Tuple>
  : false;

type IsSame<T, U> = [T, U] extends [U, T] ? true : false;

type DiagonalCheck<B extends Board> = IncludesTuple<B, [1, 1]> extends true
  ? IncludesTuple<B, [0, 0]> extends true
    ? IncludesTuple<B, [2, 2]> extends true
      ? true
      : false
    : IncludesTuple<B, [0, 2]> extends true
    ? IncludesTuple<B, [2, 0]> extends true
      ? true
      : false
    : false
  : false;

type MarkCheck<P extends Board, D extends 0 | 1, R extends Row> = MarkCount<
  P,
  D,
  R
> extends 2
  ? true
  : false;

type Check<B extends Board, S extends Square> =
  | MarkCheck<B, 0, S[0]>
  | MarkCheck<B, 1, S[1]>
  | DiagonalCheck<[...B, S]> extends false
  ? false
  : true;

type PlayBoard<PH extends PlayHistory, P extends Play> = Check<
  PlayerHistory<PH, P[1]>,
  P[0]
> extends true
  ? `${P[1]} is win`
  : [P, ...PH];

type Play1 = PlayBoard<[], [[0, 0], "A"]>;
type Play2 = PlayBoard<Play1, [[1, 0], "B"]>;
type Play3 = PlayBoard<Play2, [[1, 1], "A"]>;
type Play4 = PlayBoard<Play3, [[1, 2], "B"]>;
type Play5 = PlayBoard<Play4, [[2, 2], "A"]>;

```


`Play1`において一番最初に0行0列にAが入ります

```
A　　　　　
　　　　　　　　 
　　　　　　　　
```

`Play2`において一番最初に1行0列にBが入ります


```
A　　　　　 
 B　　　　　　
　　　　　　　　
```

`Play3`において一番最初に1行1列にAが入ります


```
A　　　   
 B A　　　　
　　　　　　　　 
```

`Play4`において一番最初に1行2列にBが入ります

```
A　　 B
 B A　　　　
　　　　　　　　 
```

`Play5`において一番最初に1行2列にAが入ります

```
A　 　B
 B A　　 　
　　　 　A
```


これでAが斜めに三つ揃ったのでAの勝ちです

`Play5`には`A is win`の文字が入りそれ以降は型エラーが発生します


![image](https://tosaken-blog.s3.ap-northeast-1.amazonaws.com/98b3f791-f483-460f-bd5d-20c60d3ff7ff)


![image](https://tosaken-blog.s3.ap-northeast-1.amazonaws.com/d709d786-c106-411a-b9fd-1ae10c9df678)


## 基本型
これらはゲームの制約を決める型です
```ts
// playerの種類
type Player = "A" | "B";

// 列数
type Col = 0 | 1 | 2;

// 行数
type Row = 0 | 1 | 2;

// マス目
type Square = [Col, Row];

// ゲーム板
type Board = Square[];

// 1ターンの情報
type Play = [Square, Player];

// ゲーム全体の情報
type PlayHistory = Play[];
```


## 特定のプレイヤーの過去の手順を取得する型
次に特定のプレイヤーの過去の手順を取得する型`PlayerHistory`です

全ての手順情報とPlayerの種類を受け取り全ての手順からそのPlayerに関する手順を返します

手順配列から手順を一つ取り出しそれがPlay型を満たしていない場合(配列に要素がない->`never`になるためPlayを満たさない)そのPlayerが今までおいたマス目の情報を返します

満たしていた場合その手順のPlayerが指定されたプレイヤーである場合PlayerHistoryを再起的に呼び出しHistoryにその手順を追加します

指定されたプレイヤーでなかった場合PlayerHistoryを再起的に呼び出します

これにより全ての手順から特定のプレイヤーが打った手順を取得することができます

```ts
type PlayerHistory<
  B extends PlayHistory,
  P extends Player,
  History extends Square[] = []
> = B extends [infer First extends Play, ...infer Rest extends Play[]]
  ? First[1] extends P
    ? PlayerHistory<Rest, P, [First[0], ...History]>
    : PlayerHistory<Rest, P, History>
  : History;
```

## 指定した行or列を満たす手順が何個あるかカウントする型
次に指定した行or列を満たす手順が何個あるかカウントする型`MarkCount`です

こちらはBoard,Direction(0|1), Row(行 or 列の数)を引数に取り指定された行or列を満たす手順の個数を返します

先ほどの型と同様に条件分岐により条件を満たすもののみを配列に追加し、最終的にその配列の長さを返しています


```ts
type MarkCount<
  B extends Board,
  D extends 0 | 1,
  R extends Row,
  Count extends any[] = []
> = B extends [infer First extends Square, ...infer Rest extends Board]
  ? First[D] extends R
    ? MarkCount<Rest, D, R, [First, ...Count]>
    : MarkCount<Rest, D, R, Count>
  : Count["length"];
```



## 縦、横が揃っているかをチェックする型
次に縦、横が揃っているかをチェックする型`MarkCheck`です

こちらは先ほどのMarkCountを使って手順の個数を取得しその個数が2であれば`true`そうでなければ`false`を返します
2個である理由は最新の手を配列に追加していないからですね


```ts
type MarkCheck<P extends Board, D extends 0 | 1, R extends Row> = MarkCount<
  P,
  D,
  R
> extends 2
  ? true
  : false;
```

## 斜めが揃っているかをチェックする型
次に斜めが揃っているかをチェックする型`DiagonalCheck`です

こちらはかなり力技で条件分岐をしています

まず中央があるかをみて存在しなければfalse

ある場合は0行0列、2行2列を見て存在しなければfalse

存在すればtrueを返します

同様に0行2列 2行0列も見ます

```ts
type DiagonalCheck<B extends Board> = IncludesTuple<B, [1, 1]> extends true
  ? IncludesTuple<B, [0, 0]> extends true
    ? IncludesTuple<B, [2, 2]> extends true
      ? true
      : false
    : IncludesTuple<B, [0, 2]> extends true
    ? IncludesTuple<B, [2, 0]> extends true
      ? true
      : false
    : false
  : false;
```


## その手によってどこかが揃うかをチェックする型
次にその手によってどこかが揃うかをチェックする型`Check`です

これは先ほどの`DiagonalCheck`と`MarkCheck`の論理和を返します
```ts
type Check<B extends Board, S extends Square> =
// 行をチェック
  | MarkCheck<B, 0, S[0]>
// 列をチェック
  | MarkCheck<B, 1, S[1]>
// 斜めをチェック
  | DiagonalCheck<[...B, S]> extends false
// 全てfalseならfalseを返す
  ? false
  : true;

```


## ゲーム進行をする型
最後にゲームの進行をする型`PlayBoard`です

これはPlayHistory型とPlay型を受け取り、Play型によってどこかが揃った場合は`<Player> is win`という文字を返し、そうでない場合は新たにPlayHistoryにPlayを追加したPlayHistory型を返します

```ts
type PlayBoard<PH extends PlayHistory, P extends Play> = Check<
  PlayerHistory<PH, P[1]>,
  P[0]
> extends true
  ? `${P[1]} is win`
  : [P, ...PH];
```


## あとはゲームをするのみ

これらの型を用いて次のように型を書けばゲームが進行します

```ts
type Play1 = PlayBoard<[], [[0, 0], "A"]>;
type Play2 = PlayBoard<Play1, [[1, 0], "B"]>;
type Play3 = PlayBoard<Play2, [[1, 1], "A"]>;
type Play4 = PlayBoard<Play3, [[1, 2], "B"]>;
type Play5 = PlayBoard<Play4, [[2, 2], "A"]>;
```

# 開発中詰まった点

いきなりですが次の方はどのような型になると思いますか？

```ts
type A = true & false extends false ? false : true;
```

正解はfalseです

![image](https://tosaken-blog.s3.ap-northeast-1.amazonaws.com/756c4edf-5427-4b5f-91d7-3d173892ac89)



では次に次はどうでしょうか

```ts
type A = true & false extends true ? false : true;
```

そりゃあextendsの条件分岐がfalse->trueになるからtrueでしょう

`false`です


![image](https://tosaken-blog.s3.ap-northeast-1.amazonaws.com/2b62790a-667b-4071-8aad-cbac9ebb6f7c)


これは開発中めっちゃ引っ掛かっちゃったんですけど

trueとfalseの交差型(true & false)はfalseではなくneverを返します

頭の中でtrue & falseならfalseでしょ！っていう脳内変換が邪魔しすぎてすっぽ抜けてましたね

普通に考えるとtrueでありfalseである方など存在しません

のでneverになるわけですね

そしてnever型というのは全ての型を継承できるようです(だからさっきの条件分岐でfalse->trueに変えても結果が変わらなかった)


ので変換表としては以下ですね

```ts
true & false extends true -> true

true & false extends false -> true

true & true extends true -> true

true | false extends true -> false

true | true extends true -> true

true | false extends boolean -> true

```



# 終わりに

今回はみんな大好きtsの型パズルの話でした

今回作った型に関しては実用性がないかもしれませんがconditional typeなどは実務やライブラリ作成などで使うこともあるので覚えておいて損はないと思います！

tsって楽しい

終わり