1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-11

はじめに

どうも私です

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

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

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

読んでいた記事

できたもの

できたものがこちらです

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

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

image

基本型

これらはゲームの制約を決める型です

// 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を再起的に呼び出します

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

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列を満たす手順の個数を返します

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

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個である理由は最新の手を配列に追加していないからですね

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列も見ます

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です

これは先ほどのDiagonalCheckMarkCheckの論理和を返します

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型を返します

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"]>;

開発中詰まった点

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

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

正解はfalseです

image

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

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

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

falseです

image

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

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

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

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

のでneverになるわけですね

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

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

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って楽しい

終わり

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?