今更なんで2番煎じなことを。
ポーカーの役をTSの型システムだけで判定しようという記事はすでに以下のような記事がある。
TypeScriptの型システムなら《コンパイル時に》ポーカーの役を判定できるかも? - Qiita
ではなんで改めてやってみようと思ったか?
- 記事がすでに三年前のもので、あれからTSにはstring literalという文字列を扱う型推論の方法が登場したから
- 記事ではストレート系の役が判定できていないから。
- TypeScriptの型定義で麻雀の役判定をする 【dwango Advent Calendar 2日目】- MANA-DOTという記事に触発されたから
目標
- ハンドを入れたら役を返す。
- カードの順序は問わない。(これをソート済み限定にしてしまうと簡単そうだから)
- ロイヤルからブタまで全役判定する
- なるべく楽して書く(努力目標)
- 入力は正しくないことも想定する。
成果物
以下ソース
/* eslint-disable @typescript-eslint/no-unused-vars */
type Flush = 'Flush';
type Straight = 'Straight';
type StraightFlush = 'Straight Flush';
type RoyalStraight = 'Royal Straight';
type RoyalStraightFlush = 'Royal Straight Flush';
type FourCard = 'Four of a Kind';
type ThreeCard = 'Three of a Kind';
type TwoPair = 'Two Pair';
type FullHouse = 'Full House';
type OnePair = 'One Pair';
type NoHand = 'No Hand';
type LegalHand = 'Legal Hand';
type CardNumber =
| '02'
| '03'
| '04'
| '05'
| '06'
| '07'
| '08'
| '09'
| '10'
| '11'
| '12'
| '13'
| '01';
type CardMark = '♦' | '♥' | '♠' | '♣';
type Card = `${CardMark}${CardNumber}`;
type toCards<P extends string> = P extends '' ? []
: P extends `${infer A}${infer B}${infer C}${infer Rest}`
? `${A}${B}${C}` extends Card ? [`${A}${B}${C}`, ...toCards<Rest>]
: never
: never;
type isLegalHand<P extends string> = toCards<P> extends [infer A, infer B, infer C, infer D, infer E]
? A extends B | C | D | E ? never
: B extends C | D | E ? never
: C extends D | E ? never
: D extends E ? never
: LegalHand
: never;
type isFourCard<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends (BNum & CNum & DNum) | (CNum & DNum & ENum) | (BNum & DNum & ENum) | (BNum & CNum & ENum) ? FourCard
: BNum extends (CNum & DNum & ENum) ? FourCard
: never
: never
: never;
type isThreeCard<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
]
? ANum extends (BNum & CNum) | (BNum & DNum) | (BNum & ENum) | (CNum & DNum) | (CNum & ENum) | (DNum & ENum)
? ThreeCard
: BNum extends (CNum & DNum) | (CNum & ENum) | (DNum & ENum) ? ThreeCard
: CNum extends (DNum & ENum) ? ThreeCard
: never
: never
: never;
type isFullHouse<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends (BNum & CNum) ? DNum extends ENum ? FullHouse
: never
: ANum extends (BNum & DNum) ? CNum extends ENum ? FullHouse
: never
: ANum extends (BNum & ENum) ? CNum extends DNum ? FullHouse
: never
: ANum extends (CNum & DNum) ? BNum extends ENum ? FullHouse
: never
: ANum extends (CNum & ENum) ? BNum extends DNum ? FullHouse
: never
: ANum extends (DNum & ENum) ? BNum extends CNum ? FullHouse
: never
: BNum extends (CNum & DNum) ? ANum extends ENum ? FullHouse
: never
: BNum extends (CNum & ENum) ? ANum extends DNum ? FullHouse
: never
: BNum extends (DNum & ENum) ? ANum extends CNum ? FullHouse
: never
: CNum extends (DNum & ENum) ? ANum extends ENum ? FullHouse
: never
: never
: never
: never;
type isOnePair<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends BNum | CNum | DNum | ENum ? OnePair
: BNum extends CNum | DNum | ENum ? OnePair
: CNum extends DNum | ENum ? OnePair
: DNum extends ENum ? OnePair
: never
: never
: never;
type isTwoPair<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends BNum ? CNum extends DNum | ENum ? TwoPair
: DNum extends ENum ? TwoPair
: never
: ANum extends CNum ? BNum extends DNum | ENum ? TwoPair
: DNum extends ENum ? TwoPair
: never
: ANum extends DNum ? BNum extends CNum | ENum ? TwoPair
: CNum extends ENum ? TwoPair
: ANum extends ENum ? BNum extends CNum | DNum ? TwoPair
: CNum extends DNum ? TwoPair
: never
: BNum extends CNum ? DNum extends ENum ? TwoPair
: never
: BNum extends DNum ? CNum extends ENum ? TwoPair
: never
: BNum extends ENum ? CNum extends DNum ? TwoPair
: never
: never
: never
: never
: never;
type isFlush<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${infer AMark}${CardNumber}`,
`${infer BMark}${CardNumber}`,
`${infer CMark}${CardNumber}`,
`${infer DMark}${CardNumber}`,
`${infer EMark}${CardNumber}`,
] ? AMark extends BMark & CMark & DMark & EMark ? Flush
: never
: never
: never;
// 重複チェック
type NotSameNumber<A, B, C, D, E> = A extends B | C | D | E ? never
: B extends C | D | E ? never
: C extends D | E ? never
: D extends E ? never
: LegalHand;
// 重複パターンが存在するが、NotSameNumberが先に入れば5枚の並び替えパターンをすべて網羅できる。
type StraightNumberPatternChecker<
B extends CardNumber,
A extends CardNumber,
C extends CardNumber,
D extends CardNumber,
E extends CardNumber,
> = `${A | B | C | D | E}${A | B | C | D | E}${A | B | C | D | E}${A | B | C | D | E}${
| A
| B
| C
| D
| E}`;
type isRoyalStraight<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
]
? LegalHand extends NotSameNumber<ANum, BNum, CNum, DNum, ENum>
? `${ANum}${BNum}${CNum}${DNum}${ENum}` extends StraightNumberPatternChecker<'10', '11', '12', '13', '01'>
? RoyalStraight
: never
: never
: never
: never;
type isStraight<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? LegalHand extends NotSameNumber<ANum, BNum, CNum, DNum, ENum> ? `${ANum}${BNum}${CNum}${DNum}${ENum}` extends
| StraightNumberPatternChecker<'10', '11', '12', '13', '01'>
| StraightNumberPatternChecker<'09', '10', '11', '12', '13'>
| StraightNumberPatternChecker<'08', '09', '10', '11', '12'>
| StraightNumberPatternChecker<'07', '08', '09', '10', '11'>
| StraightNumberPatternChecker<'06', '07', '08', '09', '10'>
| StraightNumberPatternChecker<'05', '06', '07', '08', '09'>
| StraightNumberPatternChecker<'04', '05', '06', '07', '08'>
| StraightNumberPatternChecker<'03', '04', '05', '06', '07'>
| StraightNumberPatternChecker<'02', '03', '04', '05', '06'> ? Straight
: never
: never
: never
: never;
type isStraightFlash<P extends string> = Flush extends isFlush<P> ? Straight extends isStraight<P> ? StraightFlush
: never
: never;
type isRoyalStraightFlush<P extends string> = Flush extends isFlush<P>
? RoyalStraight extends isRoyalStraight<P> ? RoyalStraightFlush
: never
: never;
type Hand<P extends string> = RoyalStraightFlush extends isRoyalStraightFlush<P> ? RoyalStraightFlush
: StraightFlush extends isStraightFlash<P> ? StraightFlush
: FourCard extends isFourCard<P> ? FourCard
: FullHouse extends isFullHouse<P> ? FullHouse
: Flush extends isFlush<P> ? Flush
: Straight extends isStraight<P> ? Straight
: ThreeCard extends isThreeCard<P> ? ThreeCard
: TwoPair extends isTwoPair<P> ? TwoPair
: OnePair extends isOnePair<P> ? OnePair
: LegalHand extends isLegalHand<P> ? NoHand
: never;
type IllegalHand1 = Hand<'wrong string'>; // never 記法があってない
type IllegalHand2 = Hand<'♣07♣05♣06♣08'>; // never 少牌
type IllegalHand3 = Hand<'♣07♣05♣06♣08♣04♣03'>; // never 多牌
type IllegalHand4 = Hand<'♣07♣07♣06♣08♣04'>; // never カードダブり
type RoyalStraightFlushSample = Hand<'♦01♦10♦11♦12♦13'>; // royal
type StraightFlushSample = Hand<'♣07♣05♣06♣08♣04'>; // straight flush
type FourCardSample = Hand<'♦12♣12♠12♦03♥12'>; // four card
type FullHouseSample = Hand<'♦03♦08♣08♠08♥03'>; // full house
type FlushSample = Hand<'♥02♥03♥09♥10♥13'>; // flush
type TwoPairSample = Hand<'♥12♣01♣12♦03♠01'>; // two pair
type OnePairSample = Hand<'♥11♣11♥12♥13♥01'>; // one pair
type StraightSample = Hand<'♥10♣07♦08♠09♦11'>; // straight
type NoHandSample = Hand<'♦12♣13♣04♠06♥01'>; // No hand
Hand<P>
に入力された手札に対して役が返ってきていることがわかる。画像はストレートフラッシュ。
解説
まずトランプを表現するために、カードとマークのunion型を作成する。string literal typeを使う都合上、数字表現に使われる文字の文字数はすべて同じであることが望ましいので0始まりで表現することにした。
type CardNumber =
| '02'
| '03'
| '04'
| '05'
| '06'
| '07'
| '08'
| '09'
| '10'
| '11'
| '12'
| '13'
| '01';
type CardMark = '♦' | '♥' | '♠' | '♣';
type Card = `${CardMark}${CardNumber}`;
次にカードを正しい手札として判定する。
type toCards<P extends string> = P extends '' ? []
: P extends `${infer A}${infer B}${infer C}${infer Rest}`
? `${A}${B}${C}` extends Card ? [`${A}${B}${C}`, ...toCards<Rest>]
: never
: never;
toCards
は文字列を受け取り、受け取った文字列の先頭三文字をカードとして受け取り、配列として残りを再びtoCards
にかける。
♥01♥02♥03♥04♥05
という文字列であれば、[♥01,♥02,♥03,♥04,♥05]
という5つのカードの配列として受け取れるようになった。
次に手札が正しい手札として形をなしているか判定する。正しい手札とは
- 手札は5枚である
- 5枚のカードは重複していない。
以上の条件を判定する。
type isLegalHand<P extends string> = toCards<P> extends [infer A, infer B, infer C, infer D, infer E]
? A extends B | C | D | E ? never
: B extends C | D | E ? never
: C extends D | E ? never
: D extends E ? never
: LegalHand
: never;
まずtoCards<P>
で5つのカードが詰まった配列が返ってくることを期待している。
次に、1枚目とそれ以降のカード、2枚目のカードは3枚目以降のカード、と言った具合ですべてのカードを比較し同じものがないことを確認する。以後役判定ではすべてこの正規手札判定チェックを通す。
役の判定
方針 強い役から順に判定するようにする。こうすることで3カードではフルハウスかどうかの考慮などをしなくていいようにする。
フラッシュ
強い役、ロイヤルの判定から説明をしたいのだが、ロイヤルはストレートとフラッシュの複合役であるため、フラッシュから説明に入る。
type isFlush<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${infer AMark}${CardNumber}`,
`${infer BMark}${CardNumber}`,
`${infer CMark}${CardNumber}`,
`${infer DMark}${CardNumber}`,
`${infer EMark}${CardNumber}`,
] ? AMark extends BMark & CMark & DMark & EMark ? Flush
: never
: never
: never;
infer
を用い5枚の手札のマークを取得し、すべて一致すればフラッシュと返すようにしている。LegalHand
のお陰で正しい手札である前提でコードを書けるのがありがたかった。
ストレート
5枚の数字がストレートかどうか、並び順不問でチェックするのは不毛すぎたのでチェッカーを用意した。
5つの数字が重複ありで出現するかどうか
と、5つの数字が重複していない
を両方かけることによってすトレードであることを証明しようとした。
数字のチェック
type StraightNumberPatternChecker<
B extends CardNumber,
A extends CardNumber,
C extends CardNumber,
D extends CardNumber,
E extends CardNumber,
> = `${A | B | C | D | E}${A | B | C | D | E}${A | B | C | D | E}${A | B | C | D | E}${
| A
| B
| C
| D
| E}`;
重複排除。これはLegalHand
の流用である。
type NotSameNumber<A, B, C, D, E> = A extends B | C | D | E ? never
: B extends C | D | E ? never
: C extends D | E ? never
: D extends E ? never
: LegalHand;
まず2つを利用してストレート単体の判定がこの様になる。
type isStraight<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? LegalHand extends NotSameNumber<ANum, BNum, CNum, DNum, ENum> ? `${ANum}${BNum}${CNum}${DNum}${ENum}` extends
| StraightNumberPatternChecker<'10', '11', '12', '13', '01'>
| StraightNumberPatternChecker<'09', '10', '11', '12', '13'>
| StraightNumberPatternChecker<'08', '09', '10', '11', '12'>
| StraightNumberPatternChecker<'07', '08', '09', '10', '11'>
| StraightNumberPatternChecker<'06', '07', '08', '09', '10'>
| StraightNumberPatternChecker<'05', '06', '07', '08', '09'>
| StraightNumberPatternChecker<'04', '05', '06', '07', '08'>
| StraightNumberPatternChecker<'03', '04', '05', '06', '07'>
| StraightNumberPatternChecker<'02', '03', '04', '05', '06'> ? Straight
: never
: never
: never
: never;
5枚のカードがすべて異なる数字であることをチェックしたら、5枚の数字が10から始まるストレート、または、9から始まるストレート、または、…2から始まるストレートであるかどうかをチェックし、どれかに引っかかったらストレートと判定する。
ロイヤルではこの内10から始まるストレートだけを判定にかけるようにする。
以下ロイヤルストレートの判定
type isRoyalStraight<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
]
? LegalHand extends NotSameNumber<ANum, BNum, CNum, DNum, ENum>
? `${ANum}${BNum}${CNum}${DNum}${ENum}` extends StraightNumberPatternChecker<'10', '11', '12', '13', '01'>
? RoyalStraight
: never
: never
: never
: never;
type isRoyalStraightFlush<P extends string> = Flush extends isFlush<P>
? RoyalStraight extends isRoyalStraight<P> ? RoyalStraightFlush
: never
: never;
フラッシュかつ10から始まるストレートでロイヤル
ここからは気合の列挙になる。
4カード
4枚のカードが同じ数字であることを判定する。4カードまでは割りと現実的な組み合わせの範囲でコードが収まってくれた。
type isFourCard<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends (BNum & CNum & DNum) | (CNum & DNum & ENum) | (BNum & DNum & ENum) | (BNum & CNum & ENum) ? FourCard
: BNum extends (CNum & DNum & ENum) ? FourCard
: never
: never
: never;
これまでの手順と同様にinfer
で数字部分だけ抜き出し1枚目と2,3,4枚目がすべて同じ、または2,3,5枚目と同じと1枚目が4カードに絡むパターン全てにチェックを入れる。
次に1枚目を除き4カードが絡むパターン(2,3,4,5枚目の1パターンだけ)を検証し終了
3カード
フルハウスより先に解説したい。4カードよりやや列挙パターンが多くなる。
type isThreeCard<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
]
? ANum extends (BNum & CNum) | (BNum & DNum) | (BNum & ENum) | (CNum & DNum) | (CNum & ENum) | (DNum & ENum)
? ThreeCard
: BNum extends (CNum & DNum) | (CNum & ENum) | (DNum & ENum) ? ThreeCard
: CNum extends (DNum & ENum) ? ThreeCard
: never
: never
: never;
1枚目が絡まないパターンに加えて、2枚目が絡まないパターンを列挙する必要がある。だがまだギリギリ見れるコードが書ける。
フルハウス
終わりである。3カードの|
で省略していた書き方が使えなくなる。なぜなら1枚目、2枚目、3枚目が同じカードだったら4枚目と5枚目の比較。1枚目と2枚目と4枚目が同じカードだったら3枚目と5枚目の比較。とunion型でごまかしができなくなってしまった。
type isFullHouse<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends (BNum & CNum) ? DNum extends ENum ? FullHouse
: never
: ANum extends (BNum & DNum) ? CNum extends ENum ? FullHouse
: never
: ANum extends (BNum & ENum) ? CNum extends DNum ? FullHouse
: never
: ANum extends (CNum & DNum) ? BNum extends ENum ? FullHouse
: never
: ANum extends (CNum & ENum) ? BNum extends DNum ? FullHouse
: never
: ANum extends (DNum & ENum) ? BNum extends CNum ? FullHouse
: never
: BNum extends (CNum & DNum) ? ANum extends ENum ? FullHouse
: never
: BNum extends (CNum & ENum) ? ANum extends DNum ? FullHouse
: never
: BNum extends (DNum & ENum) ? ANum extends CNum ? FullHouse
: never
: CNum extends (DNum & ENum) ? ANum extends ENum ? FullHouse
: never
: never
: never
: never;
(BNum & CNum) | (BNum & DNum) | (BNum & ENum) | (CNum & DNum) | (CNum & ENum) | (DNum & ENum)
と書いていた部分がA extends BNum & CNum
だったらDNum extends ENum
といちいち分けて書いた結果とんでもない三項演算子の量になってしまった。
1ペア
これも2ペアの前に紹介したい。
LegalHand
の応用でかける。1枚目から順に2枚目以降に同じ数字がないかどうかだけ調査する。
type isOnePair<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends BNum | CNum | DNum | ENum ? OnePair
: BNum extends CNum | DNum | ENum ? OnePair
: CNum extends DNum | ENum ? OnePair
: DNum extends ENum ? OnePair
: never
: never
: never;
2ペア
フルハウスと同様1ペアでUnion型で省略していた部分が省略できなくなるためコード量が増大する。
1枚目と2枚目が同じだったとき、3枚目と4枚目、または5枚目が同じ、または4枚目と5枚目が同じ。といった深いネストの比較を延々と掘り下げていく。
type isTwoPair<P extends string> = LegalHand extends isLegalHand<P> ? toCards<P> extends [
`${CardMark}${infer ANum}`,
`${CardMark}${infer BNum}`,
`${CardMark}${infer CNum}`,
`${CardMark}${infer DNum}`,
`${CardMark}${infer ENum}`,
] ? ANum extends BNum ? CNum extends DNum | ENum ? TwoPair
: DNum extends ENum ? TwoPair
: never
: ANum extends CNum ? BNum extends DNum | ENum ? TwoPair
: DNum extends ENum ? TwoPair
: never
: ANum extends DNum ? BNum extends CNum | ENum ? TwoPair
: CNum extends ENum ? TwoPair
: ANum extends ENum ? BNum extends CNum | DNum ? TwoPair
: CNum extends DNum ? TwoPair
: never
: BNum extends CNum ? DNum extends ENum ? TwoPair
: never
: BNum extends DNum ? CNum extends ENum ? TwoPair
: never
: BNum extends ENum ? CNum extends DNum ? TwoPair
: never
: never
: never
: never
: never;
勢いで書き上げたのでツーペアフルハウスあたりに抜けがあるかもしれない、あまりテストできていない。
最後にすべての役を強い順に判定する。判定順を誤ると、ツーペアがワンペアと判定されたりするので順番には注意する。
type Hand<P extends string> = RoyalStraightFlush extends isRoyalStraightFlush<P> ? RoyalStraightFlush
: StraightFlush extends isStraightFlash<P> ? StraightFlush
: FourCard extends isFourCard<P> ? FourCard
: FullHouse extends isFullHouse<P> ? FullHouse
: Flush extends isFlush<P> ? Flush
: Straight extends isStraight<P> ? Straight
: ThreeCard extends isThreeCard<P> ? ThreeCard
: TwoPair extends isTwoPair<P> ? TwoPair
: OnePair extends isOnePair<P> ? OnePair
: LegalHand extends isLegalHand<P> ? NoHand
: never;
役がないが正しい手札のときと、手札が正しくない場合を最後に分けて終了。
感想
string literalが来たら楽ができるんだろうとたかを括ったが、カードがソートされない前提の役判定は力技が多くなってしまい大変だった。もっと楽できる書き方はストレートのときのような組み合わせを使えばあるはずだが、今回は力尽きたのでここまで。残念だができたことにも満足している。
当たり前だがTypeScriptの型システムはJSにトランスパイルされる前なのでループによる列挙などを利用することができない。プログラム的に列挙することの大切さをプログラムを書きながら知ることになった。