F#でDomemo実装を関数型っぽく

#趣旨
Domemoのプログラムを、CPUを強くするのも兼ねて書き直し。
前回はオブジェクト指向ベース(以後OOPと表現)っぽく書いてたので
今回は関数型ベース(以後FPと表現)っぽく実装する。

Domemoって何？→[ドメモ（Domemo）とかいうイタリアの伝説の確率ゲームが面白い。(ちもろぐ)](https://chimolog.co/domemo/)
OOP版は？→[Domemo F#実装のリファクタリング](https://qiita.com/aoi_erimiya/items/c96c5a2d848e9273cad9)

##成果物
BitBucketとgithubにも入れてみた。
MITライセンスの元でお好きにどうぞ!
https://aoi_erimiya@bitbucket.org/aoi_erimiya/domemo.git
https://github.com/Aoi-Erimiya/Domemo.git

##結果
・スーパースパゲッティモンスターの誕生。
→関数型実装の運命なのか。

・CPUは非常に強くなった。
→認識できる範囲の情報を元に、最も確率が高い結果を推測する。
→自分が宣言したものを記憶し、同じ失敗はしないようにした。

・ゲームとしての面白さは減った。
→Domemoはある程度解法が決まっている。
→ある程度の遊び(挙動の幅)が、ゲームとしては必要という事か。
→人的ミス含め、心理戦がDomemoの面白さの大事な要素と思われる。
→ブラフなどの駆け引きや、自プレイヤーや他プレイヤーが発言した数字を忘れたりとか。

##OOP版との比較
###１：カードのリストを一元管理する
それぞれに手札として分けるのではなく、誰がどのカードを所有しているかに考え方を変更。

【OOPでの挙動】
・必要なカード一覧を作成
・カード一覧をシャッフル
・プレイヤーの手札として、それぞれコピー
・場に出ている札リストにコピー
・余りは放置

【FPでの挙動】
・必要なカード一覧を作成。
・「誰がどのカードを所有しているか」をランダムに設定する。(シャッフルに相当)
→プレイヤーの手札(プレイヤー名)
→場に公開されたカード(open)
→除外されたカード(reject)の３種

####割り当て結果の例
```fsharp
[7:AKR; 7:HRT; 7:AKR; 7:open; 7:IZM; 7:HRT; 7:reject; 6:HRT; 6:reject; 6:HRT;
 6:IZM; 6:MZK; 6:reject; 5:AKR; 5:open; 5:open; 5:IZM; 5:AKR; 4:MZK; 4:MZK;
 4:IZM; 4:IZM; 3:MZK; 3:open; 3:MZK; 2:reject; 2:HRT; 1:AKR]
```

####割り当て処理
```fsharp:domemoFP.fs
// Hold the card authority.
type CardAuth(owner:string, cardNumber:int) = 
    let mutable owner:string = owner
    let cardNumber:int = cardNumber

    member x.Owner with get() = owner
    member x.Owner with set(value) = owner <- value
    member x.CardNumber with get() = cardNumber

    override x.ToString() = x.CardNumber.ToString()

// Assign ownership of the card.
let rec assignCards(cards:List<CardAuth>, assignName, assignCount) = 
    if assignCount = 0 then
        ()
    else
        let target = Math.Next(28)
        if cards.[target].Owner = "none" then
            cards.[target].Owner <- assignName
            assignCards(cards, assignName, assignCount - 1)
        else      
            assignCards(cards, assignName, assignCount)
```

###２：Playerクラスの廃止

プレイヤーとして必要な要素って、突き詰めると名前だけなんだなと。

```fsharp:domemoOOP.fs
type Player(name:string, cards:List<int>) = 
    let name:string = name
    let mutable cards:List<int> = cards

    member x.Name with get() = name

    member x.cardsToString(isMask:bool) = 
        let mutable cardsStr = ""
        if isMask then
            cards |> List.iter (fun x -> cardsStr <- cardsStr + "X")
        else
            cards |> List.iter (fun card -> cardsStr <- cardsStr + card.ToString())
        cardsStr

    member x.checkCard() = cards.Length = 0

    member x.guessCard(rand: Random) =
        rand.Next(1, 8)

    member x.matchCard(guessCard:int) = 
        if List.contains guessCard cards
        then
            let idx = List.findIndex (fun card -> card = guessCard) cards
            cards <- List.concat [ cards.[0..idx-1]; cards.[idx+1..cards.Length-1] ]
            true
        else
            false
```

```fsharp:domemoFP.fs
let playerNames = ["MZK";"IZM";"HRT";"AKR"]
```

###３：可能な限りiterとかfilterとか使う
FP版はスーパースパゲッティモンスターが誕生。
ソースコードの見通しの良さはきっとトレードオフ。
実装は長いものの、関数の意味さえ分かっていれば
何をしようとしているかわかるのが関数型の強さの一つかもしれない。

```fsharp:domemoOOP.fs
for player in players do

    if not isGameOver then
        Thread.Sleep(500)
    
        let guessCard =
            if List.last players = player then
                printf "Please input guess number! -> "
                Console.ReadLine() |> int
            else
                player.guessCard(rand)

        printfn "%s called -> %d" player.Name guessCard

        if player.matchCard(guessCard) then            
            printfn "jackpot!"
            gm.addOpenCards(guessCard)
        else
            printfn "miss!"

        if player.checkCard() then
            printfn "----------------------"
            printfn "%s is Win!" player.Name
            isGameOver <- true
        else
            printfn ""
```

```fsharp:domemoFP.fs
playerNames
|> List.iter(fun playerName ->
    if not isGameOver then
        printf "*%s " playerName
        // Extract cards visible from player's point of view.
        let showCards = cards |> List.filter (fun ca -> ca.Owner <> playerName && ca.Owner <> "reject")

        // Classify by card number and calculate the estimated remaining number.
        let mutable candidateCards = []
        cardNumbers
        |> List.iter(fun cardNumber ->
            let classifiedCards = showCards |> List.filter(fun ca -> ca.CardNumber = cardNumber)
            candidateCards <- List.append candidateCards [CardPair(cardNumber, cardNumber - classifiedCards.Length)]
        )

        // Estimate the card with the highest likelihood of hit (excluding items declared but not hit).
        let guessCard:CardPair =
            candidateCards
            |> List.filter(fun cp -> 
                missCards |> List.tryFind(fun cam -> cam.Owner = playerName && cam.CardNumber = cp.CardNumber) = None)
            |> List.maxBy(fun cp -> cp.CardCount)

        guessCard.CardNumber |> printfn "%A"
        Thread.Sleep(waitMilliseconds)

        if cards |> List.tryFind(fun ca -> ca.Owner = playerName && ca.CardNumber = guessCard.CardNumber) = None then
            printfn "Miss!"
            missCards <- List.append [CardAuth(playerName, guessCard.CardNumber)] missCards
        else
            printfn "Jackpot!"
            let hitCards = cards |> List.filter(fun ca -> ca.Owner = playerName && ca.CardNumber = guessCard.CardNumber)
            let otherCards = cards |> List.filter(fun ca -> ca.Owner = playerName && ca.CardNumber <> guessCard.CardNumber || ca.Owner <> playerName)
            cards <- List.append (List.append [CardAuth("open", hitCards.Head.CardNumber)] hitCards.Tail) otherCards
            
            // If there is no ownership card, the game ends.
            if cards |> List.tryFind(fun ca -> ca.Owner = playerName) = None then
                isGameOver <- true
                printfn "%s is Win!" playerName
)
```

###４：CPUの強化
自分がDomemoをプレーする時の思考をそのまま実装したら、超強いやつができた。
(OOP版は単純なランダム実装)

```fsharp:場に出ているカードと他プレイヤーのカードを抽出
// Extract cards visible from player's point of view.
let showCards = cards |> List.filter (fun ca -> ca.Owner <> playerName && ca.Owner <> "reject")
```
```fsharp:数字毎に枚数をカウント
// Classify by card number and calculate the estimated remaining number.
let mutable candidateCards = []
cardNumbers
|> List.iter(fun cardNumber ->
    let classifiedCards = showCards |> List.filter(fun ca -> ca.CardNumber = cardNumber)
    candidateCards <- List.append candidateCards [CardPair(cardNumber, cardNumber - classifiedCards.Length)]
)
```
```fsharp:一番ヒット率が高いカードを予測。過去に宣言したが、外れたカードは無視する。(同じ失敗をしない)
// Estimate the card with the highest likelihood of hit (excluding items declared but not hit).
let guessCard:CardPair =
    candidateCards
    |> List.filter(fun cp -> 
        missCards |> List.tryFind(fun cam -> cam.Owner = playerName && cam.CardNumber = cp.CardNumber) = None)
    |> List.maxBy(fun cp -> cp.CardCount)
```

##今後
###CPUに戦略の幅を持たせ、勝率の高い戦略を研究する。(AIとしての強さの追求)
・ヒット率が高いものから、合理的に選択(現状)
・ヒット率が少ないものから大穴狙いで選択
・ヒット率関係なく、可能性が1%でもあるものをランダムで選択
・基本は上記のいずれかに属するが、まれにブラフを言う

###心理戦に対応する。(ゲームとしての面白さの追求)
→他プレイヤーの宣言値から、自分の手札のカード有無を推測。
→ただし、鵜呑みにすると問題になるので、この辺りの閾値をどうするかが研究要素。

【ブラフの例】
前提：プレイヤーAKRを操作しているとする。

１：プレイヤーMZKが「1」を宣言。しかし、そのプレイヤーの手札に「1」はない。

```
*Field*
[1:0; 2:0; 3:2; 4:2; 5:3; 6:4; 7:3] //公開済カードの番号：枚数の形式で表示

*Players*
MZK [4; 3]
IZM [4; 7; 5]
HRT [7; 2]
AKR [X; X; X] //AKRは自分の手札はわからない。
```
２：「1」が他プレイヤーの手札と場に存在しなければ、「1」はゲーム開始時に除外されたという事が推測できる。

```
*Field*
[1:0; 2:0; 3:2; 4:2; 5:3; 6:4; 7:3]　//場にはない

*Players*
MZK [4; 3]   //他プレイヤーの手札にもない
IZM [4; 7; 5]
HRT [7; 2]
AKR [X; X; X]
```

３：しかしプレイヤーAKRを混乱させるために、あえて手番を消費して「1」を宣言している可能性もある。

```
*Field*
[1:0; 2:0; 3:2; 4:2; 5:3; 6:4; 7:3]

*Players*
MZK [4; 3] 
IZM [4; 7; 5]
HRT [7; 2]
AKR [5; 7; 1]
```