技術memo

関数型ゴースト

FSharperならこう書く(A Poker Game Program) #FsAdvent

これはF# Advent Calendar 2014の8日目の記事です。 前日はk_disposeさんのF#のlintツールであるFSharpLintを使ってみる - Qiitaでした。

今日は、

  • 「F#をやってる人って普段どんなコードを書いているの?」
  • 「何を考えてプログラミングしてるの?」

とお思いの全国n人の皆さんの声にお応えして、ちょっとした規模のコードに解説をつけてみます。え、n=0ですか?そんなことないですよね?

(去年と同様、ネタがないならコードを書けばいいじゃない、という安直さをお許しください) (また相変わらずの無駄な長さをお許しください)

目次

  • お題(Theme)
  • 構想(Plan)
  • 実装(Implementation)
    • カード(Card)
      • 柄(Suit)
      • 番号(Number)
      • カード(Card)
    • 山札(Deck)
    • 手札(Hand)
    • 役(Rank of Hands)
    • メイン処理(Main)
    • 動かしてみる(Play)
    • 最終的なモジュール構成(Modules)
  • ポイント(Points)
  • 明日のF# Advent Calendar

お題(Theme)

  • 簡単にできる範囲で、ポーカー(ポーカー - Wikipedia)ゲームを実装します。
  • プレイヤーは一人、山札からカードを引いては指定した手札を入れ替えて、役ができているかどうかをチェックします。
    • 同じ役同士や、役無し同士の強弱は判定しません。今後の課題とします。
  • 山札がなくなったら終了とします。
  • ジョーカーは含みません。今後の課題とします。

構想(Plan)

  • トランプを型安全に扱う。
    • カードは番号(1から13)と柄(ハートやスペード)の組で表します。
    • 山札はカードのリストで表します。始めは4×13枚あり、カードを引くたび減っていきます。
    • 手札は5枚のカードとし、長さ固定のリストで表します。
    • 役は列挙型(判別共用体)で表します。
  • 役(ワンペア、ストレート、フォアカードなど)の判定ロジックは、どう実装するか。
    • バグが出やすそうなので、なるべく単純に記述したいところです。
    • 「ある手札について、全ての順列で役に該当するかチェックし、それを全ての役について行う。該当した役の中で最高のものを取得する」という方針
      • 非効率ですが、最適化は処理速度が気になってから考えることにします。

実装(Implementation)

カード(Card)

柄(Suit)

絵柄は、スペード、ハート、ダイア、クラブの4通りを、判別共用体で表します。

    (* 柄 *)
    module Suit =
        (* 柄 *)
        type T = | Spade | Heart | Diamond | Club
        (* 全ての柄 *)
        let values = [Spade; Heart; Diamond; Club]

番号(Number)

番号は、1から13に含まれない値は生成できないようにしたいので、classになりそうです。 classにするとinterfaceの実装が煩わしいところですが、constructorを制限しようと思うと、レコードや判別共用体では厳しそうです。もし他に良い方法をご存知の方がいらっしゃったら教えてください。

    (* 番号 *)
    module Number=
        (* 最小値 *)
        let min = 1
        (* 最大値 *)
        let max = 13
        (* 番号 *)
        type T(value:int)=
            do
                (* 数値範囲チェック *)
                if not (min <= value && value <= max)
                    then failwith "The Number is required 1 to 13."
            member this.Value = value
            interface System.IEquatable<T> with
                member this.Equals other = this.Value = other.Value
            interface System.IComparable<T> with
                member this.CompareTo other = this.Value.CompareTo other.Value
            interface System.IComparable with
                member this.CompareTo other = other |> function
                    | :? T as x -> (this :> System.IComparable<_>).CompareTo x
                    | _ -> failwith "other is not T"
            override this.ToString () = value.ToString()
            override this.GetHashCode () = value.GetHashCode()
            member this.Equals (other : T) = (this :> System.IEquatable<_>).Equals other
            override this.Equals (other : obj) = other |> function
                | :? T as x -> this.Equals x
                | _ -> false
            (* ++演算子。番号に数値を加算する。maxを超えた場合はminに戻る *)
            static member (++) (l:T, r:int) =
                let l = l.Value
                let value = l + r
                let rec loop value =
                    if value > max then loop (value - max + min - 1)
                    else value
                new T(loop value)

        (* 番号の中の値を取得する *)
        let getValue (x:T) = x.Value
        (* 番号を生成する *)
        let create x = new T(x)

++演算子は、役のストレートの判定に使えそうなので定義しておきました。

getValueとcreateは、万一実装をクラスから変更する場合に影響が少ないようにするための備えです。こういった物があるとパイプライン演算子との相性も良くなります。

CompareToの実装は、現状では13が最大ですが、「同じ役同士の強弱を判定する」あたりの実装の時には、1 > 13 > 12 > ... という風にしたほうが良さそうですね。

IComparableはジェネリックでないバージョンの実装が必要であるのがちょっとした罠でした。理不尽です。

カード(Card)

    (* 通常のカードは番号と柄の組で表す。ジョーカーは含まない *)
    type NormalCard = { Number : Number.T; Suit : Suit.T } with
        override this.ToString () = sprintf "%A%d" this.Suit (Number.getValue this.Number)

単にレコードで表現します。

ここまでの、module Suit, module Number, NormalCardは、全てmodule Cardに含めておくことにします。

山札(Deck)

山札はカードのリストで表しておきます。

(* デッキ=カードの1セット。山札 *)
module Deck =
    (* デッキ(ジョーカー抜き) *)
    let deck_without_joker = List.ofSeq(seq{
            (* シーケンス式の二重ループでNumberとSuitの組み合わせを列挙する *)
            for num in Card.Number.min .. Card.Number.max do
            for suit in Card.Suit.values do
                yield { Card.Number = Card.Number.create num; Card.Suit = suit }
        })

    (* デッキをシャッフルする
        System.Guidを使う方法は、再現確認がしづらいので、本当は避けたほうが良さそう。
    *)
    let shuffle deck =
        deck |> Seq.sortBy (fun x-> System.Guid.NewGuid())

listもNormalCardもImmutableなので、モジュールに直接置いて構わないでしょう。モジュール変数は実質的にはstatic変数のようなものなので、値が変更されるような場合には避けるべきです。

ここで、「山札から指定の枚数のカードを引いて、残りの分と合わせて取得する」ような関数を用意します。

    (* リスト処理関数 *)
    module List =
        (* リストの先頭からcount個の要素を取得し、残りの要素とペアにして返す *)
        let take count list =
            let rec loop i acc tail =
                if i = count then Some ((List.rev acc), tail)
                else tail |> function
                    | [] -> None
                    | h::t -> loop (i + 1) (h::acc) t
            loop 0 [] list

それとなく抽象化しておきます。指定された要素数が処理できた場合は、Some(取得した要素, 残り)を、要素が足りなくなった場合はNoneを返します。

以下、再びDeckモジュールへ。

    (* デッキからカードを引いた結果 ... Tupleだと型が同じでわかりづらそうなので、Recordにしておく *)
    type PickResult = { Picked:Card.NormalCard list; Deck:Card.NormalCard list }
    (* デッキから指定された枚数のカードを引く *)
    let pick count deck =
        List.take count deck
            |> Option.map (fun (head, tail) -> { Picked = head; Deck = tail })

手札(Hand)

ここでは役の有無に関わらず、手札=Handとします。 手札はNormalCardのlistで表現します。単一ラベルの判別共用体ってどうなのでしょうか。

(* システム系モジュール *)
[<AutoOpen>]
module Infrastructure =
    (* 2変数関数のカリー化 *)
    let curry f x y = f (x, y)

(* 手札 *)
module Hand =
    (* 手札の枚数 *)
    let count = 5
    (* 手札 *)
    type T = | Hand of Card.NormalCard List with
        (* ToStringのcache *)
        member private this.String = this |> function
            | Hand xs ->
                xs
                    |> List.map (fun x -> x.ToString())
                    |> List.toArray
                    |> curry System.String.Join " / "
        (* Object.ToString *)
        override this.ToString () = this.String
        (* CUIの選択肢表示用ToString *)
        member this.ToString indexes = this |> function
            | Hand xs -> 
                xs
                    |> List.map2 (fun x y ->
                            sprintf "[%s]:%s" y (x.ToString())
                        ) <| indexes (* 逆向きパイプライン演算子<|はロマン *)
                    |> List.toArray
                    |> curry System.String.Join "/"

ToStringあたりは後付けの都合が含まれますが、だいたいこんなものでしょう。.Netの関数を使うときは、Curry化関数を作っておくと便利です。

手札に関連する処理としては、初回の「山札から5枚引く」パターンと、「手札から指定されたカードを入れ替える」パターンを用意します。

    (* 山札から最初のcount枚を取得して手札を作り、残りの山札とペアにして返す。
      山札の枚数が足りない場合はNone
    *)
    let createBy deck =
        Deck.pick count deck
            |> Option.map (fun pickResult -> 
                (* let束縛のレコードパターン *)
                let { Deck.Picked = picked; Deck.Deck = d } = pickResult
                ((Hand picked), d)
            )

    (* 手札のうちchangeCardsで指定されたカードを捨て、山札から同じ枚数を加え、残りの山札とペアにして返す。 *)
    let change hand deck changeCards =
        Deck.pick (Set.count changeCards) deck
            |> Option.map (fun pickResult ->
                let { Deck.Picked = picked; Deck.Deck = d } = pickResult
                let hand = hand |> function
                    | Hand xs -> xs |> List.filter (changeCards.Contains>>not)
                Hand (List.append picked hand), d
            )

どちらにしても、代入を使った状態の変更ではなく、古い状態を変更しないまま新しい状態を生成するように作ります。 changeメソッドのchangeCardsの型を何にするかは迷うところですが、順序に関係なく含まれるかどうかを判定したいだけなので、Set(集合)とします。生成コストがかかるようなら工夫できるところかもしれません。

役(Rank of Hands)

まずは役の一覧を定義しましょう。

(* 役 *)
module Rank =
    (* 役。弱い順で定義する=>強い方が大小の比較で大きい *)
    type T = 
        | OnePair | ThreeCards | TwoPair | Straight 
        | FullHouse | Flush | StraightFlush | RoyalStraightFlush | FourCards

大小比較が定義順に依存するのはちょっとしたポイントです。ソースは忘れました。

次にそれぞれの役の判定を考えます。 前述の方針「ある手札について、全ての順列で役に該当するかチェックし、それを全ての役について行う。該当した役の中で最高のものを取得する」に従って作ると以下のような形です。

    (* 役判定関数モジュール。
        カード配列は手札の全ての順列が列挙されると仮定する。
    *)
    module Evaluation =
        (* one pair: 1つ目の番号 = 2つ目の番号 *)
        let onePair (xs:Card.NormalCard array) =
            xs.[0].Number = xs.[1].Number

        (* three cards: one pairかつ、2つ目の番号=3つ目の番号 *)
        let threeCards (xs:Card.NormalCard array) =
            (onePair xs)
                && xs.[1].Number = xs.[2].Number

        (* four cards: three cards同様 *)
        let fourCards (xs:Card.NormalCard array) =
            (threeCards xs)
                && xs.[2].Number = xs.[3].Number

        (* two pair: one pairかつ、2つ目の番号!=3つ目の番号かつ、3つ目の番号=4つ目の番号 *)
        let twoPair (xs:Card.NormalCard array) =
            (onePair xs)
                && xs.[1].Number <> xs.[2].Number
                && xs.[2].Number = xs.[3].Number

        (* full house: two pair同様 *)
        let fullHouse (xs:Card.NormalCard array) =
            (threeCards xs)
                && xs.[2].Number <> xs.[3].Number
                && xs.[3].Number = xs.[4].Number

        (* straight: 全てのカードについて、前のカードの番号+1=今のカードの番号  *)
        let straight (xs:Card.NormalCard array) =
            xs
                |> Seq.ofArray
                |> (fun xs -> Seq.zip xs (Seq.skip 1 xs))
                |> Seq.forall (fun t -> (fst t).Number ++ 1 = (snd t).Number)

        (* flush: 全てのカードについて、前のカードの柄=今のカードの柄 *)
        let flush (xs:Card.NormalCard array) =
            xs
                |> Seq.ofArray
                |> (fun xs -> Seq.zip xs (Seq.skip 1 xs))
                |> Seq.forall (fun t -> (fst t).Suit = (snd t).Suit)

        (* straight flush: straight かつ flush *)
        let straightFlush (xs:Card.NormalCard array) =
            (straight xs) && (flush xs)

        (* royal straight flush: straight flushかつ、番号の並びが10, 11, 12, 13, 1 *)
        let royalStraightFlush (xs:Card.NormalCard array) =
            (straightFlush xs) && (
                let royal = [|10; 11; 12; 13; 1|]
                xs
                    |> Seq.ofArray
                    |> Seq.map (fun x-> Card.Number.getValue x.Number)
                    |> (fun xs-> System.Linq.Enumerable.SequenceEqual (xs, royal))
            )

引数の型を配列にしたのは、インデックスでのアクセスが多そうだったから、という程度の理由です。

定義した関数をひとまとめに扱えるように準備しておきます。

        (* key=役、value=判定関数の連想配列 *)
        let map =
            Map.ofList<T, (Card.NormalCard array -> bool)> [
                (OnePair, onePair); (ThreeCards, threeCards); (FourCards, fourCards);
                (TwoPair, twoPair); (FullHouse, fullHouse); (Straight, straight);
                (Flush, flush); (StraightFlush, straightFlush); (RoyalStraightFlush, royalStraightFlush)
            ]

        (* カードの配列 -> Option 役 となるような関数のリスト *)
        let evaluations =
            map
                |> Map.toList
                |> List.map (fun kv ->
                    let (key, f) = kv
                    (fun xs -> if (f xs) then Some key else None)
                )

Mapは不変の連想配列で、実装は確か平衡木(baranced tree)です。immutableなのでmoduleに直接定義してしまいます。

再び前述の「ある手札について、全ての順列で役に該当するかチェックし、それを全ての役について行う。該当した役の中で最高のものを取得する」に戻って、いよいよその中身を実装していきます。

まずは手札の全パターンの順列を列挙するところから作ります。 山札のところで作っていたmodule Listに追加しておきます。

        (* リストの差分 xs//ys ysに含まれないxsの要素を取得する *)
        let difference xs ys =
            let ys = Set.ofList ys
            xs |> List.filter (ys.Contains>>not)

        (* リストの全ての順列を列挙する *)
        let permutation (xs:'a list) :'a list list=
            let rec loop xs current acc = xs |> function
                | [] -> current::acc
                | xs -> List.foldBack (fun x state ->
                            loop (difference xs [x]) (x::current) state
                        ) xs acc
            loop xs [] []
                |> List.map List.rev
            (* foldBack周りはWebを参考にしたものの、理解度が微妙 *)

permutation関数は、「任意の型'aのリスト」を受け取って、「'aのリストのリスト」を返します。例えばF# Interactiveで実行すると以下のようになります。

> permutation [1;2;3;4];;

val it : int list list =
  [[1; 2; 3; 4]; [1; 2; 4; 3]; [1; 3; 2; 4]; [1; 3; 4; 2]; [1; 4; 2; 3];
   [1; 4; 3; 2]; [2; 1; 3; 4]; [2; 1; 4; 3]; [2; 3; 1; 4]; [2; 3; 4; 1];
   [2; 4; 1; 3]; [2; 4; 3; 1]; [3; 1; 2; 4]; [3; 1; 4; 2]; [3; 2; 1; 4];
   [3; 2; 4; 1]; [3; 4; 1; 2]; [3; 4; 2; 1]; [4; 1; 2; 3]; [4; 1; 3; 2];
   [4; 2; 1; 3]; [4; 2; 3; 1]; [4; 3; 1; 2]; [4; 3; 2; 1]]

準備ができたところで、手札に役チェック関数を適用する処理を実装しましょう。

    (* 判定処理の実装 *)
    module Evaluator=
        (* 0 <= x < Hand.count の範囲の全ての整数値の順列を列挙したint list list *)
        let perms =
            List.permutation [0..(Hand.count - 1)]

        (* 手札に対して評価関数の一覧を適用する *)
        let evaluate evals hand =
            (* 手札を取り出してカードの配列にしておく *)
            let hand =
                hand
                    |> function | Hand.Hand xs -> xs
                    |> List.toArray
            (* 手札のカード5枚の全ての順列を列挙した配列のリストを作る *)
            let hand_perms =
                perms
                    |> List.map (fun p ->
                        p
                            |> List.map (fun i -> hand.[i])
                            |> List.toArray
                     )
            (* 手札の順列を判定関数に通す *)
            hand_perms
                |> List.map (fun x ->
                    evals
                        |> List.map (fun f -> f x)
                        |> List.filter Option.isSome
                        |> List.map Option.get
                )

ここで、evaluateの引数evalsは、「カードの配列 -> Option 役 となるような関数のリスト」、つまり先ほどのそれぞれの役判定関数をまとめたものが使用できるようにしています。

ここで、ヘルパー関数をまたListモジュールに追加しておきます。

        (* リストが空でない場合は最大の要素を、それ以外ならNoneを返す *)
        let tryMax = function
            | [] -> None
            | xs -> List.max xs |> Some

標準で足りないと思った関数はどんどん追加していくと良い感じです。

そして手札の判定関数として呼びやすいような外部インターフェースを用意します。

    (* 手札の役判定を行う *)
    let evaluate hand =
        (* Evaluation.evaluationsに定義済みの関数を使って手札を評価、最高の役を取得する *)
        Evaluator.evaluate Evaluation.evaluations hand
            |> List.map List.tryMax
            |> List.filter Option.isSome
            |> List.map Option.get
            |> List.tryMax

メイン処理

プログラムのメイン処理は、今回の話としてはおまけなので、ざっくり簡単に書いてしまいます。

処理フローとしては、次のような形です。

  1. 山札を用意
  2. 山札から5枚引いて、手札を作る
  3. 手札を表示、入れ替える手札を入力させる
  4. 入力チェック、invalidなら3.に戻る
  5. 山札から入れ替える枚数のカードを引く。残りがなかったら終了。
  6. 3.で指定されたカードを山札から引いたカードを入れ替えて、新しい手札とする
  7. 6.でできた手札の役を判定し、手札と役を表示する。
  8. 3.に戻る。
module Program =
    let input_choices = ["a"; "b"; "c"; "d"; "e"]
    let input_exit = Set.ofList ["exit"; "quit"]
    let input_choices_set = Set.ofList input_choices

    (* メインのループ処理 *)
    let rec loop (hand:Hand.T) deck =
        printfn "-----------------"
        printfn "your hand: %s" (hand.ToString input_choices)
        printfn "input change cards"
        (* キー入力受付 *)
        let input = System.Console.ReadLine ()
        if input_exit.Contains input then begin
            printfn "exit game"
        end
        else begin
            let input = 
                input
                    |> Seq.map (fun x -> x.ToString())
                    |> Seq.distinct
                    |> Set.ofSeq
            if
                input |> Set.forall (fun x -> input_choices_set.Contains x) |> not
            then begin
                printfn "invalid input";
                loop hand deck
            end
            (* 手札交換を実施 *)
            let changes =
                input_choices
                    |> List.mapi (fun i x -> (i,x))
                    |> List.filter (fun x -> input.Contains (snd x))
                    |> List.map fst
                    |> List.map (fun i ->
                        let hand = hand |> function | Hand.Hand xs -> xs
                        hand.[i]
                    )
                    |> Set.ofList
            let changed = Hand.change hand deck changes
            changed |> function
                | None -> printfn "deck is empty. game end."
                | Some (hand, deck) ->
                    (* 手札交換完了時 *)
                    printfn "your hand: %s" (hand.ToString())
                    let rank = Rank.evaluate hand
                    printfn "rank: %A" rank
                    loop hand deck
        end

//(*
    [<EntryPoint>]
    let main args =
        let deck = Deck.deck_without_joker |> Deck.shuffle |> List.ofSeq
        let hand, deck = Hand.createBy deck |> Option.get
        loop hand deck
        0
//*)

動かしてみる

遊んでみます。「input change cards」の次の行が、キーボードからの入力です。

-----------------
your hand: [a]:Club12/[b]:Spade10/[c]:Club7/[d]:Heart7/[e]:Club1
input change cards
bd
your hand: Diamond13 / Spade1 / Club12 / Club7 / Club1
rank: Some OnePair
-----------------
your hand: [a]:Diamond13/[b]:Spade1/[c]:Club12/[d]:Club7/[e]:Club1
input change cards
ab
your hand: Heart4 / Diamond10 / Club12 / Club7 / Club1
rank: <null>
-----------------
your hand: [a]:Heart4/[b]:Diamond10/[c]:Club12/[d]:Club7/[e]:Club1
input change cards
ab
your hand: Club5 / Club10 / Club12 / Club7 / Club1
rank: Some Flush
-----------------
your hand: [a]:Club5/[b]:Club10/[c]:Club12/[d]:Club7/[e]:Club1
input change cards
abcde
your hand: Club13 / Club6 / Diamond1 / Spade11 / Heart6
rank: Some OnePair
-----------------
your hand: [a]:Club13/[b]:Club6/[c]:Diamond1/[d]:Spade11/[e]:Heart6
input change cards
acd
your hand: Diamond4 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond4/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Club9 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Club9/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Heart11 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Heart11/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Spade12 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Spade12/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Diamond2 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond2/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Heart1 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Heart1/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Diamond11 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond11/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Spade2 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Spade2/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Club11 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Club11/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Heart3 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Heart3/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Spade3 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Spade3/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Club4 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Club4/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Diamond8 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond8/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Heart8 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Heart8/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Diamond12 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond12/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Diamond9 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Diamond9/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Club3 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some TwoPair
-----------------
your hand: [a]:Club3/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
a
your hand: Spade5 / Heart5 / Diamond5 / Club6 / Heart6
rank: Some FullHouse
-----------------
your hand: [a]:Spade5/[b]:Heart5/[c]:Diamond5/[d]:Club6/[e]:Heart6
input change cards
acd
your hand: Spade8 / Spade13 / Spade9 / Heart5 / Heart6
rank: <null>
-----------------
your hand: [a]:Spade8/[b]:Spade13/[c]:Spade9/[d]:Heart5/[e]:Heart6
input change cards
b
your hand: Heart9 / Spade8 / Spade9 / Heart5 / Heart6
rank: Some OnePair
-----------------
your hand: [a]:Heart9/[b]:Spade8/[c]:Spade9/[d]:Heart5/[e]:Heart6
input change cards
c
your hand: Diamond6 / Heart9 / Spade8 / Heart5 / Heart6
rank: Some OnePair
-----------------
your hand: [a]:Diamond6/[b]:Heart9/[c]:Spade8/[d]:Heart5/[e]:Heart6
input change cards
a
your hand: Heart12 / Heart9 / Spade8 / Heart5 / Heart6
rank: <null>
-----------------
your hand: [a]:Heart12/[b]:Heart9/[c]:Spade8/[d]:Heart5/[e]:Heart6
input change cards
a
your hand: Spade7 / Heart9 / Spade8 / Heart5 / Heart6
rank: Some Straight
-----------------
your hand: [a]:Spade7/[b]:Heart9/[c]:Spade8/[d]:Heart5/[e]:Heart6
input change cards
abcde
your hand: Spade6 / Heart13 / Heart10 / Heart2 / Spade4
rank: <null>
-----------------
your hand: [a]:Spade6/[b]:Heart13/[c]:Heart10/[d]:Heart2/[e]:Spade4
input change cards
abcde
deck is empty. game end.

One Pair, Flush, Two Pair, Full House, Straightができました。対戦相手がいないのであまり面白くはありません。

最終的なモジュール構成

詳細を省略すると、以下のような形になりました。

(* システム系モジュール *)
[<AutoOpen>]
module Infrastructure =
    ...
    (* リスト処理関数 *)
    module List =
        ...

(* カード *)
module Card =
    (* 柄 *)
    module Suit =
        type T =
            ... 
    (* 番号 *)
    module Number=
        type T(value:int)=
            ...
    type NormalCard = { Number : Number.T; Suit : Suit.T }
        ...

(* デッキ=カードの1セット。山札 *)
module Deck =
    ...

(* 手札 *)
module Hand =
    type T =
        ...
    ...

(* 役 *)
module Rank =
    type T =
        ...

    (* 役判定関数モジュール。 *)
    module Evaluation =
        ...

    (* 判定処理の実装 *)
    module Evaluator=
        ...

(* メイン処理 *)
module Program =
    ...

ドメイン駆動設計的な多層アーキテクチャの観点で考えると、以下のような形でしょうか。

  • インフラストラクチャ層 ... Infrastructureモジュール。今回はリスト処理と、curry関数が含まれます。 DB等も使わないので、必要に迫られた分だけ追加しています。
  • ドメイン層 ... Card, Deck, Hand, Rankの各モジュールが当てはまりそうです。
  • アプリケーション層、ユーザーインターフェース層 ... 今回はUIは単純、単機能なので、Programモジュールでどちらもやっています。ToStringメソッドなど、一部はドメイン層のコードに依存したままになっています。

ポイント

  • Immutableなデータ構造を作り、「元の状態に副作用を起こさず、新しい状態を取得する」関数を作っていくこと。
  • パイプライン演算子 |> は便利。
    • F#!F#!
  • パターンマッチ(match hoge with | Some x -> ほにゃらら | None -> ほにゃらら みたいなやつ)は便利。
    • ML!ML!
  • 組み込みのデータ構造(コレクション)は積極的に使う(list, array, seq, Map, Set)。
    • 場面によってMutable/Immutableを使い分ける。DictionaryとMap, arrayとlistなど。
      • 原則Immutable, アクセスや生成・変更の速度が欲しい場合は、データ構造の特性を考えて局地的にMutableにしていくイメージです。
  • moduleは積極的に切っていく。1概念1module, 1データ1module.
    • ML的なFunctorはF#にはありませんが、それでも便利です。
  • 変数のシャドウイング(同名変数の再定義)は、F#では推奨だと思います。
  • データ型を作り、型検査を頼る。
    • 今回の場合だと、Rank.Evaluatorの判定処理を実行する部分は、型チェックにかなり助けられています。
  • 型検査を頼れないところはテストする。
    • 記事では省略しましたが、Rank.Evaluationに書いたそれぞれの役の判定がきちんと機能するかどうか、Rank.evaluate関数を呼び出す形でテストを記述しました。Interactiveに読み込ませるだけで動作チェックができるように書いておくとよいです。
  • 変数の命名規則が一部snake_caseとcamelCaseで入り混じってるのは、頭の中がまだどっちつかずになっているためなので、深い理由はありません。

明日のF# Advent Calendar

明日のF# Advent Calendarは、担当代わってbleisさんのクラスのコンストラクタ呼び出し時にプロパティも同時に初期化する - ぐるぐる~でした。

あわせて読みたい

bleisさんがすばらしい添削記事をアップしてくださいました: Re: FSharperならこう書く(A Poker Game Program) #FsAdvent - ぐるぐる~。必読です!