【Rust】カードゲームにハマったので大富豪を作った話

## はじめに

最近、**Slay the Spire**や**ドミニオン**等のカードゲームにハマっています。
毎回手に入るカードが違うためプレイする毎に戦略が変わります。
空き時間にちょっと遊ぶつもりでも熱中してしまうくらい面白いです。

https://store.steampowered.com/app/646570/Slay_the_Spire/

https://store.steampowered.com/app/1131620/Dominion/

https://store.steampowered.com/app/1135810/Vault_of_the_Void/


デジタルカードゲームはデッキのシャッフルやカードの配布を自動で行ってくれるので便利ですね。
遊んでいて自分でもカードゲームを作りたくなりました。
といっても上記の様なゲームは複雑すぎるので、トランプの定番である大富豪を作ることにしました。

プログラミング言語は個人的に好きな**Rust**を使いました。🦀

## 成果物

ターミナル上で動作する大富豪を作りました。
場に出すカードの番号を入力します。
`カードの番号(XX):`の`XX`には場に出されたカードが表示されます。
パスする場合は何も入力せずEnterキーを押します。

画面上には以下の情報が表示されます。
`プレイヤー名 [手札の枚数]: 場に出したカード`

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2693350/662b2fd5-5ea5-5995-4e14-d9d295a6576a.gif)

https://github.com/yt2b/daifugo

## ルール

大富豪はローカルルールを含め様々なルールがあります。
今回は以下のルールに絞って実装しました。

* プレイヤーは4人で固定
* 2、8、ジョーカーで上がってはいけない(反則上がり)
* 8を出すと強制的に場が流れる(8切り)
* 同じスートのカードが2回連続で出た場合は、そのスートのカードしか出せなくなる(縛り)

## トランプ

### スートと数字

スートと数字は列挙体で定義します。
比較やコピーができるように各種アトリビュートを実装します。
また、各値は弱い順に並んでいます。

```rs
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Rank {
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King,
    Ace,
    Two,
}
```

### カードの定義

カードも列挙体で定義します。
カードには以下の２種類があります。
* 数字とスート持つ通常のカード
* 数字とスートを持たないジョーカーのカード

```rs
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Card {
    Normal(Suit, Rank),
    Joker,
}
```

Rustの列挙体は値によって異なるデータ型を持たせることができます。

### カードの比較関数

次に2つのカードの大きさを比較する関数を作ります。
Cardのベクタをソートしたり、カードを場に出せるかを判定する時に使います。
必要な比較関数は以下の通りです。

* 数字、スート順で比較する関数
* 数字のみで比較する関数
* 数字のみで比較する関数(革命版)

比較関数は2つの`Card`の参照を受け取り、以下の条件で`std::cmp::Ordering`列挙体を返します。
* カード1 < カード2 ⇒ `std::cmp::Ordering::Less`
* カード1 == カード2 ⇒ `std::cmp::Ordering::Equal`
* カード1 > カード2 ⇒ `std::cmp::Ordering::Greater`

#### 数字、スート順で比較

この関数は配られた手札をソートする際に使用します。

```rs
pub fn cmp_order(c1: &Card, c2: &Card) -> std::cmp::Ordering {
    match (c1, c2) {
        (Card::Normal(s1, r1), Card::Normal(s2, r2)) => r1.cmp(r2).then(s1.cmp(s2)),
        (_, _) => c1.cmp(c2),
    }
}
```

通常のカード同士の場合は数字、スート順で比較します。
少なくとも一方のカードがジョーカーの場合は、数字やスートを見ずに`Card`の値で比較します。

```rs
// 以下の様ににソートされる
// [♠7, Joker, ♣3, ♥7]
// ↓
// [♣3, ♥7, ♠7, Joker]
let mut cards = vec![Card::Normal(Suit::Spade, Rank::Seven)、Card::Joker, Card::Normal(Suit::Club, Rank::Three), Card::Normal(Suit::Heart, Rank::Seven)];
cards.sort_by(cmp_order);
assert_eq!(cards, vec![Card::Normal(Suit::Club, Rank::Three), Card::Normal(Suit::Heart, Rank::Seven), Card::Normal(Suit::Spade, Rank::Seven), Card::Joker]);
```

#### 数字のみ比較

上記の関数では同じ数字のカードを比較しても`♣ < ♦ < ♥ < ♠`の順で大小関係を判定します。
なので数字のみを比較する関数も定義します。
場に出ているカードと手札のカードの大小関係は主にこの関数で判定します。

```rs
pub fn cmp_rank(c1: &Card, c2: &Card) -> std::cmp::Ordering {
    match (c1, c2) {
        (Card::Normal(_, r1), Card::Normal(_, r2)) => r1.cmp(r2),
        (_, _) => c1.cmp(c2),
    }
}
```

#### 数字のみでソートする関数(革命版)

大富豪では同じ数字のカードを4枚以上出すとジョーカー以外のカードの強さが逆転します。(革命)
この「**ジョーカー以外の**」というのが意外と曲者です。
この例外のせいで`cmp_rank`関数の使いまわしができません。

```rs
// これだとジョーカーが最弱になってしまう
pub fn cmp_rank_reversely(c1: &Card, c2: &Card) -> std::cmp::Ordering {
    cmp_rank(c2, c1)
}
```

そのため、逆転した際のロジックを書く必要があります。
通常のカード同士の場合は逆順で比較し、どちらかがジョーカーの場合は`cmp_rank`と同じように比較します。

```rs
pub fn cmp_rank_reversely(c1: &Card, c2: &Card) -> std::cmp::Ordering {
    match (c1, c2) {
        (Card::Normal(_, r1), Card::Normal(_, r2)) => r2.cmp(r1),
        (_, _) => c1.cmp(c2),
    }
}
```

## 組み合わせ

場に出せるカードの組み合わせは大きく分けて以下の3通りです。
* カード1枚
* 数字が同じ2枚以上のカード
* 同じスートで数字が連続している3枚以上のカード(階段)

上記の組み合わせも列挙体で定義します。

```rs
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Comb {
    Single(Card), // カード1枚
    Multi(Vec<Card>), // 数字が同じ2枚以上のカード
    Seq(Vec<Card>), // 同じスートで数字が連続している3枚以上のカード
}
```

### Card ⇒ Comb::Singleへの変換

`Card`から`Comb::Single`への変換処理は簡単です。

```rs
// 1枚のカードからComb::Singleへ変換できるようにする ※例: ♥10
let cmb = Comb::Single(Card::Normal(Suit::Heart, Rank::Ten));
```


### Vec\<Card\> ⇒ Comb::Multiへの変換

`Comb::Multi`や`Comb::Seq`の場合は、変換処理を実装します。
また、枚数が複数枚の場合は無効な組み合わせが渡されることを考慮する必要があります。
そのため、変換に成功したら`Ok`、失敗したら`Err`を返す`TryFrom`トレイトを実装します。

```rs
impl TryFrom<Vec<Card>> for Comb {
    type Error = ();

    fn try_from(mut cards: Vec<Card>) -> Result<Self, Self::Error> {
        let len = cards.len();
        if len <= 1 {
            return Err(());
        }
        if is_same_ranks(&cards) {
            return Ok(Comb::Multi(cards));
        }
        Err(())
    }
}
```

全てのカードが同じ数字か判定する`is_same_ranks`関数にカードのベクタの参照を渡して`true`ならば`Ok(Comb::Multi)`を返します。
カードが1枚の場合や判定が`false`の場合は`Err`を返します。

#### is_same_ranks関数

`is_same_ranks`関数の判定ロジックは以下の通りです。

1. ジョーカーを削除する
2. 各カードの数字を抽出する
3. 隣り合う数字同士を比較して全て同じか調べる

:::note
例えば`is_same_ranks`関数に`♠6、Joker、♥6、♦6`を渡すと
1. `♠6、♥6、♦6`
2. `6、6、6`
3. `6 == 6 == 6`

となり`true`を返します。
:::

コードは以下の様になります。
ジョーカーの削除と数字の抽出は`filter_map`関数で一度に行っています。
`tuple_windows`関数で隣り合う要素のタプルを取得しています。
これは`itertools`という外部クレートの関数です。

```rs
// 全てのカードが同じ数字か判定する
fn is_same_ranks(cards: &[Card]) -> bool {
    cards
        .iter()
        .filter_map(|c| match c {
            Card::Normal(_, r) => Some(r),
            Card::Joker => None,
        })
        .tuple_windows()
        .all(|(r1, r2)| r1 == r2)
}
```

```rs
// 数字が同じ2枚以上のカードならComb::Multiを返す ※例[♦Q ♣Q]
let cards = vec![Card::Normal(Suit::Diamond, Rank::Queen), Card::Normal(Suit::Club, Rank::Queen)];
let cmb2 = Comb::try_from(cards);
assert_eq!(cmb2, Ok(Comb::Multi(...)));

// 無効な組み合わせならばエラーを返す ※例[♠3、♦5]
let cmb4 = Comb::try_from(vec![Card::Normal(Suit::Spade, Rank::Three), Card::Normal(Suit::Spade, Rank::Five)]);
assert_eq!(cmb3, Err());
```

### Vec\<Card\> ⇒ Comb::Seqへの変換

`Comb::Seq`への変換処理を上記の`try_from`関数内に追加します。
以下の条件を全て満たした場合に`Ok(Comb::Seq(..))`を返します。
* カードの枚数が3枚以上
* 全てのカードのスートが同じ(後述する`is_same_suits`関数で判定)
* カードが連続して並んでいる(後述する`is_seq`関数で判定)

```rs
impl TryFrom<Vec<Card>> for Comb {
    type Error = ();

    fn try_from(mut cards: Vec<Card>) -> Result<Self, Self::Error> {
        ...
        if len >= 3 && is_same_suits(&cards) && is_seq(&cards) {
            return Ok(Comb::Seq(cards));
        }
        Err(())
    }
}
```

#### is_same_suits関数

`is_same_suits`関数の判定ロジックは`is_same_ranks`とほぼ同じです。
抽出する値が数字からスートに変わっただけです。

```rs
// 全てのカードが同じスートか判定する
fn is_same_suits(cards: &[Card]) -> bool {
    cards
        .iter()
        .filter_map(|c| match c {
            Card::Normal(s, _) => Some(s),
            Card::Joker => None,
        })
        .tuple_windows()
        .all(|(v1, v2)| v1 == v2)
}
```

#### is_seq関数

「カードが連続して並んでいるか」の判定は少し複雑です。
ジョーカーを含む場合と含まない場合でロジックが変わるので順番に説明します。

##### ジョーカーを含まない場合

まずは比較的簡単なジョーカーを含まない場合の判定ロジックです。

1. カードの数値を抽出する
2. 隣り合う数字同士の差分を計算する
3. 全ての差分が1または-1か調べる

:::note
例えば`♦3、♦4、♦5、♦6`の場合
1. `3、4、5、6`
2. `1、1、1`
3. `1 == 1 == 1`

となり`true`を返します。
:::

`Rank`は列挙体なので差分を求めることはできません。
なので`Rank`を整数型に変換するトレイトを実装します。

```rs
impl From<Rank> for i32 {
    fn from(r: Rank) -> Self {
        match r {
            Rank::Three => 0,
            Rank::Four => 1,
            ...
            Rank::Ace => 11,
            Rank::Two => 12,
        }
    }
}
```

Rustで上記のロジックを実装すると以下の様になります。
差分を重複を排除する`HashSet`に入れます。
その後、要素を取得しやすくするためベクタに変換します。
このベクタの要素が1でその値の絶対値が1の場合、カードが連続して並んでいることが分かります。

```rs
            let diffs = cards
                .iter()
                .filter_map(|c| match c {
                    // カードの数字をi32に変換
                    Card::Normal(_, r) => Some(i32::from(*r)),
                    _ => None,
                })
                .tuple_windows()
                .map(|(v1, v2)| v2 - v1) // 隣同士の数字の差分を計算する
                .collect::<HashSet<i32>>() // 差分の重複を排除する
                .into_iter()
                .collect::<Vec<i32>>();
            return (diffs.len() == 1) && (diffs[0].abs() == 1);
```

##### ジョーカーを含む場合

ジョーカーを含む場合、ジョーカーを適切な数字に変換する必要があります。
ジョーカーの位置によって見るべきカードが変わります。

先頭にある場合は、後続の2枚から適切な数字を判断します。
`Joker、♣7、♣8` ⇒ `6、7、8`
`Joker、♣7、♣6` ⇒ `8、7、6`
末尾にある場合は、その前の2枚から適切な数字を判断します。
`♥4、♥5、Joker` ⇒ `4、5、6`
`♥6、♥5、Joker` ⇒ `6、5、4`
それ以外の場合は、前後の2枚から適切な数字を判断します。
`♠Q、Joker、♠A` ⇒ `Q、K、A`
`♠A、Joker、♠Q` ⇒ `A、K、Q`

Rustで上記のロジックを実装すると以下の様になります。
ジョーカーの置き換え処理が追加されている以外は、含まない場合の処理とほとんど同じです。

```rs
            let mut nums: Vec<Option<i32>> = cards
                .iter()
                .map(|c| match c {
                    // カードの数字をi32に変換
                    Card::Normal(_, r) => Some(i32::from(*r)),
                    Card::Joker => None,
                })
                .collect();
            // ジョーカーを数字に置き換える
            match idx {
                _ if idx == 0 => {
                    let x = *nums[idx + 1].as_ref().unwrap();
                    let y = *nums[idx + 2].as_ref().unwrap();
                    nums[idx] = Some(2 * x - y);
                }
                _ if idx == nums.len() - 1 => {
                    let x = *nums[idx - 2].as_ref().unwrap();
                    let y = *nums[idx - 1].as_ref().unwrap();
                    nums[idx] = Some(2 * y - x);
                }
                _ => {
                    let v1 = *nums[idx - 1].as_ref().unwrap();
                    let v2 = *nums[idx + 1].as_ref().unwrap();
                    nums[idx] = Some(((v1 + v2) / 2) as i32)
                }
            };
            let diffs = nums
                .into_iter()
                .filter_map(|v| v)
                .tuple_windows()
                .map(|(v1, v2)| v2 - v1) // 隣同士の数字の差分を計算する
                .collect::<HashSet<i32>>() // 差分の重複を排除する
                .into_iter()
                .collect::<Vec<i32>>();
            return diffs.len() == 1 && (diffs[0].abs() == 1);
```


以下に`is_seq`関数全体のコードを載せます。

<details><summary>is_seq関数</summary>

```rs
// カードの数字が連続しているか判定する
fn is_seq(cards: &[Card]) -> bool {
    if cards.len() <= 2 {
        return false;
    }
    let joker_idx = cards.iter().position(|c| matches!(*c, Card::Joker));
    match joker_idx {
        // ジョーカーを含む
        Some(idx) => {
            let mut nums: Vec<Option<i32>> = cards
                .iter()
                .map(|c| match c {
                    // カードの数字をi32に変換
                    Card::Normal(_, r) => Some(i32::from(*r)),
                    Card::Joker => None,
                })
                .collect();
            // ジョーカーを数字に置き換える
            match idx {
                _ if idx == 0 => {
                    let x = *nums[idx + 1].as_ref().unwrap();
                    let y = *nums[idx + 2].as_ref().unwrap();
                    nums[idx] = Some(2 * x - y);
                }
                _ if idx == nums.len() - 1 => {
                    let x = *nums[idx - 2].as_ref().unwrap();
                    let y = *nums[idx - 1].as_ref().unwrap();
                    nums[idx] = Some(2 * y - x);
                }
                _ => {
                    let v1 = *nums[idx - 1].as_ref().unwrap();
                    let v2 = *nums[idx + 1].as_ref().unwrap();
                    nums[idx] = Some(((v1 + v2) / 2) as i32)
                }
            };
            let diffs = nums
                .into_iter()
                .filter_map(|v| v)
                .tuple_windows()
                .map(|(v1, v2)| v1 - v2) // 隣同士の数値の差分を計算する
                .collect::<HashSet<i32>>() // 差分の重複を排除する
                .into_iter()
                .collect::<Vec<i32>>();
            return diffs.len() == 1 && (diffs[0].abs() == 1);
        }
        // ジョーカーなし
        None => {
            // カードから数字を抽出する
            let diffs = cards
                .iter()
                .filter_map(|c| match c {
                    // カードの数値をi32に変換
                    Card::Normal(_, r) => Some(i32::from(*r)),
                    Card::Joker => None,
                })
                .tuple_windows()
                .map(|(v1, v2)| v1 - v2) // 隣同士の数値の差分を計算する
                .collect::<HashSet<i32>>() // 差分の重複を排除する
                .into_iter()
                .collect::<Vec<i32>>();
            return diffs.len() == 1 && (diffs[0].abs() == 1);
        }
    }
}
```

</details>

## Combの比較

`Comb`同士を比較する`is_greater`関数を実装します。
これは比較対象の`Comb`より数字が強ければ`true`を返す関数です。
引数に比較対象の`Comb`の参照と比較関数を取ります。

この比較関数を通常時と革命時で変えることで、カードの強さが変わります。


```rs
// ♣5、♥5、♠5
let comb1 = Comb::try_from(vec![
    Card::Normal(Suit::Club, Rank::Five),
    Card::Normal(Suit::Heart, Rank::Five),
    Card::Normal(Suit::Spade, Rank::Five)
]).unwrap();
// ♣3、♥3、♠3
let comb2 = Comb::try_from(vec![
    Card::Normal(Suit::Club, Rank::Three),
    Card::Normal(Suit::Heart, Rank::Three),
    Card::Normal(Suit::Spade, Rank::Three)
]).unwrap();
// 通常時は5の方が強い
assert_eq!(comb1.is_greater(&comb2, cmp_rank), true);
// 革命時は3の方が強い
assert_eq!(comb1.is_greater(&comb2, cmp_rank_reversely), false);
```

`is_greater`関数内では`Comb`が持つ`Card`を順番に比較して全て大きいかを調べています。
カードのどちらかがジョーカーの場合は比較をせず`true`を返しています。

```rs
impl Comb {
    pub fn is_greater<F>(&self, comb: &Comb, comparator: F) -> bool
    where
        F: Fn(&Card, &Card) -> Ordering,
    {
        match (self, comb) {
            (Comb::Single(card1), Comb::Single(card2)) => {
                comparator(card1, card2) == Ordering::Greater
            }
            (Comb::Multi(cards1), Comb::Multi(cards2)) | (Comb::Seq(cards1), Comb::Seq(cards2)) => {
                // カードの枚数が同じか
                if cards1.len() != cards2.len() {
                    return false;
                }
                // cards1の全てのカードがcards2のカードより大きいか
                cards1
                    .iter()
                    .zip(cards2.iter())
                    .all(|(c1, c2)| match (c1, c2) {
                        (Card::Normal(_, _), Card::Normal(_, _)) => {
                            comparator(c1, c2) == Ordering::Greater
                        }
                        // どちらかのカードがジョーカーならtrue
                        (_, _) => true,
                    })
            }
            (_, _) => false,
        }
    }
}
```

## Validatorトレイト

「カードを場に出せるか」を判定する処理を実装します。
大富豪にはスート縛りや階段しばりといったルールがあるため、数字が強いだけで場に出せるとは限りません。
そういった制約を内部で管理して場に出せるかの判定をするのが`Validator`トレイトです。

```rs
pub trait Validator {
    fn get_prev_comb(&self) -> Option<&Comb>;
    fn is_valid(&self, comb: &Comb) -> bool;
}
```

`Validator`トレイには以下の2つの関数があります。
* **get_prev_cob**
直前に場に出た組み合わせを返す関数です。
* **is_valid**
組み合わせが場に出せるかを判定する関数です。

大富豪のプレーヤーは、`Validator`トレイトで場に出せる組み合わせを見つけます。

## フィールド

上記の`Validator`トレイトを実装しているのが、場の状態を管理する`Field`構造体です。
`Field`は主に以下の状態を管理しています。

* 直前に場に出されたカード
* パスの回数
* プレイヤーの順番や順位
* 縛り状況

`Indexer`はプレイヤーの順番と順位を管理し、`SuitBinder`は縛りの状態を管理する構造体です。
`Field`だけで全てを管理するのは責任過剰なので、別の構造体に機能を委譲しています。

```rs
pub struct Field {
    prev_comb: Option<Comb>,
    indexer: Indexer,
    binder: SuitBinder,
    pass_counter: usize,
    is_rev: bool,
}
```

### put関数

カードを場に出す処理を担うのがput関数です。
引数に場に出す組み合わせと残りの手札枚数を渡します。
手札の枚数が0になったらそのプレイヤーは上がりとなります。
パスをする場合は`None`を渡します。

場に出されたカードよって様々なイベントが発生します。(革命、8切りなど)
返り値に発生したイベントのフラグを返します。

```rs
impl Field {
    ...
    pub fn put(&mut self, new_comb: Option<Comb>, hands_count: usize) -> Flags {
    }
```

#### 革命

`is_rev_comb`関数は4枚のカードを持つ`Comb::Multi`か判定する関数です。
返り値のフラグに`Flags::REV`を付与します。

```rs
...
    pub fn put(&mut self, new_comb: Option<Comb>, hands_count: usize) -> Flags {
        let mut flags = Flags::empty();
        match new_comb {
            Some(comb) => {
                ...
                if is_rev_comb(&comb) {
                    // カードの強さが逆転する
                    self.is_rev = !self.is_rev;
                    flags.insert(Flags::REV);
                }
```

#### 8切り

`contains_eight`は組み合わせに8のカードが含まれているか判定する関数です。
8のカードが含まれている場合は、次のプレイヤーにターンを進めず場を初期化します。
`self.prev_comb`に`None`を設定することで場が流れ、同じプレイヤーが続けて任意のカードを出せます。


```rs
    pub fn put(&mut self, new_comb: Option<Comb>, hands_count: usize) -> Flags {
        let mut flags = Flags::empty();
        match new_comb {
            Some(comb) => {
                ...
                let eight_flag = contains_eight(&comb);
                if hands_count > 0 {
                    if eight_flag {
                        // 8切り
                        flags.insert(Flags::EIGHT);
                        self.binder.clear();
                    } else {
                        // 次のプレイヤーのターンに移る
                        self.indexer.next();
                    }
                }
                ...
                // 8を含むなら場を流す
                self.prev_comb = if eight_flag { None } else { Some(comb) }
```

#### 反則上がり

`contains_especial_card`関数は反則上がりになるカードが含まれているか判定する関数です。
反則上がりの場合、現在のプレイヤーを下位の順位に設定し、フラグに`Flags::LOSE`を付与します。

```rs
...
    pub fn put(&mut self, new_comb: Option<Comb>, hands_count: usize) -> Flags {
        ...
            Some(comb) => {
                ...
                } else if contains_especial_card(&comb, self.is_rev) {
                    // 禁止上がり
                    self.indexer.set_rank_back();
                    flags.insert(Flags::LOSE);
                }
                ...
```

### Validatorトレイトの実装

`Validator`トレイトを`Field`に実装します。

適切なスートでかつ場に出ているカードより強い場合に`true`を返します。
カードが場にない場合は常に`true`を返します。

```rs
impl Validator for Field {
    fn get_prev_comb(&self) -> Option<&Comb> {
        self.prev_comb.as_ref()
    }

    fn is_valid(&self, comb: &Comb) -> bool {
        match &self.prev_comb {
            Some(prev_comb) => {
                let comparator = match self.is_rev {
                    true => cmp_rank_reversely,
                    false => cmp_rank,
                };
                self.binder.is_valid(comb) && comb.is_greater(prev_comb, comparator)
            }
            None => true,
        }
    }
}
```

## プレイヤー

大富豪のプレイヤーが実装する`Player`トレイトを定義します。

```rs
pub trait Player {
    fn init(&mut self, hands: Vec<Card>);
    fn count_hands(&self) -> usize;
    fn get_name(&self) -> &str;
    fn get_hands(&mut self) -> &mut Vec<Card>;
    fn play(&mut self, validator: &dyn Validator) -> Option<Comb>;
    fn get_needless_cards(&mut self, cards_count: usize) -> Vec<Card>;
}
```

`Player`トレイトの関数は以下の通りです。

* **init**
手札を初期化する関数です。
引数にカードのベクタを取ります。

* **count_hands**
現在の手札の枚数を返す関数です。

* **get_name**
プレイヤーの名前を返す関数です。

* **get_hands**
手札への可変参照を返す関数です。

* **play**
`Validator`トレイトを実装したオブジェクトを受け取り、場に出す組み合わせを返す関数です。
パスする場合は`None`を返します。

* **get_needless_cards**
カード交換時に不要なカードのベクタを返す関数です。
引数に交換するカードの枚数を渡します。

大富豪に参加するプレイヤーはコンピュータの場合も人間の場合も
上記の`Player`トレイトを必ず実装します。

### NPC

まずNPC(Non Playable Character)を実装します。
出せる組み合わせの中から最弱のものを場に出す単純なプレイヤーです。
最弱の組み合わせを見つける方法は`Comb`の種類によって異なります。

#### `Comb::Single`が場に出ている場合

手札のカードを弱い順に調べていき、場のカードより強いものがあればそれを返します。

```rs
impl Player for MinNpc {
    ...
    fn play(&mut self, validator: &dyn Validator) -> Option<Comb> {
        match validator.get_prev_comb() {
            Some(comb) => match comb {
                Comb::Single(_) => {
                    // 場に出せる最小のカードのインデックスを探す
                    (0..self.hands.len()).find_map(|i| {
                        let new_comb = Comb::Single(self.hands[i]);
                        validator.is_valid(&new_comb).then(|| {
                            self.hands.remove(i);
                            new_comb
                        })
                    })
                }
                ...
```

#### `Comb::Multi`が場に出ている場合

カードを数字ごとにグループ分けして、その中で場に出せる最弱の組み合わせを調べます。

:::note
例えば場に`♦5 ♠5`があり、手札が`♥6 ♣7 ♥7 ♦8 ♥8 ♠8`である場合
数字ごとにグループ分けすると`[♥6] [♣7 ♥7] [♦8 ♥8 ♠8]`となり
場に出せる最弱の組み合わせである`♣7 ♥7`が見つかります。
:::

実際は`get_indices_grouped_by_rank`関数で数字ごとにグループ分けしたカードのインデックスを取得します。

```rs
impl Player for MinNpc {
    ...
    fn play(&mut self, validator: &dyn Validator) -> Option<Comb> {
        match validator.get_prev_comb() {
            Some(comb) => match comb {
                ...
                Comb::Multi(cards) => {
                    let len = cards.len();
                    get_indices_grouped_by_rank(&self.hands, len)
                        .into_iter()
                        .find_map(|indices| {
                            // 場に出せる最小のカードの組み合わせを探す
                            let cards = get_cards(&self.hands, &indices[0..len]);
                            let new_comb = Comb::try_from(cards).ok()?;
                            validator.is_valid(&new_comb).then(|| {
                                self.remove_hands(&indices[0..len]);
                                new_comb
                            })
                        })
                }
                ...
```

#### `Comb::Seq`が場に出ている場合

カードをスートごとにグループ分けして、その中で場に出せる最弱の組み合わせを調べます。

:::note
例えば場に`♣4 ♣5 ♣6`があり、手札が`♥4 ♣7 ♥7 ♦8 ♥8 ♣8 ♥9`である場合
数字ごとにグループ分けすると`[♣7 ♣8] [♦8] [♥4 ♥7 ♥8 ♥9]`となり
場に出せる最弱の組み合わせである`♥7 ♥8 ♥9`が見つかります。
:::

実際は`get_indices_grouped_by_suit`関数でスートごとにグループ分けした
カードのインデックスを取得して、`find_seq`関数でグループ分けしたカードの中から
階段となっているカードの組み合わせを見つけます。

```rs
impl Player for MinNpc {
    ...
    fn play(&mut self, validator: &dyn Validator) -> Option<Comb> {
        match validator.get_prev_comb() {
            Some(comb) => match comb {
                ...
                Comb::Seq(cards) => {
                    let len = cards.len();
                    get_indices_grouped_by_suit(&self.hands, len)
                        .into_iter()
                        .find_map(|indices| {
                            // 場に出せる最小のカードの組み合わせを探す
                            let (new_comb, indices) = find_seq(&self.hands, &indices, len)?;
                            validator.is_valid(&new_comb).then(|| {
                                self.remove_hands(&indices[0..len]);
                                new_comb
                            })
                        })
                }
                ...
```

#### 場が流れた場合

場が流れて新たにカード出す際は、複数枚→階段→1枚の順で出せる組み合わせを探します。
それぞれの組み合わせの見つけ方は上記の方法とほぼ同じです。

また、カード交換の際は最弱のカードを不要なカードとして出します。

```rs
impl Player for MinNpc {
    ...
    fn get_needless_cards(&mut self, cards_count: usize) -> Vec<Card> {
        (0..cards_count).map(|_| self.hands.remove(0)).collect()
    }
    ...
```

### PC

次に人間のプレイヤーであるPC(Playable Character)を実装します。
キーボードで出すカードのインデックスを入力します。
なので、入力された文字列をインデックスに変換する処理が必要です。

```rs
fn parse_idx(input: &str) -> Result<Vec<usize>, ()> {
    let results: Vec<_> = input.split(' ').map(|s| s.parse::<usize>()).collect();
    match results.iter().all(|r| r.is_ok()) {
        true => Ok(results.into_iter().map(|r| r.unwrap()).sorted().collect()),
        false => Err(()),
    }
}
```

以下の条件を全て満たした場合に手札のカードを場に出せます。
* 入力したインデックスが全て範囲内
* 選択したカードの組み合わせが有効
* カードの組み合わせが場に出せる

```rs
impl Player for Pc {
    ...
    fn play(&mut self, validator: &dyn Validator) -> Option<Comb> {
        ...
        println!("{}", get_cards_with_indices(&self.hands));
        loop {
            let input = get_input(format!("カードの番号{}: ", comb_str));
            if input.is_empty() && prev_comb.is_some() {
                return None;
            }
            let result = parse_idx(&input);
            if result.is_err() {
                println!("無効な入力");
                continue;
            }
            let indices = result.unwrap();
            let cards: Vec<Option<&Card>> =
                indices.iter().map(|idx| self.hands.get(*idx)).collect();
            if cards.iter().any(|card| card.is_none()) {
                println!("無効な入力");
                continue;
            }
            let cards: Vec<Card> = cards.iter().map(|card| *card.unwrap()).collect();
            match conver_to_comb(cards) {
                Ok(comb) if validator.is_valid(&comb) => {
                    // 手札からカードを除く
                    for i in indices.iter().rev() {
                        self.hands.remove(*i);
                    }
                    return Some(comb);
                }
                _ => {
                    println!("無効な組み合わせ");
                }
            }
        }
    }
    ...
```

以下の様に手札のカードの一覧が表示されるので次に出すカードの番号を入力します。
パスする場合は何も入力せずにEnterキーを押します。

```
 0:♣️6 
 1:♥6 
 2:♠️7 
 3:♣️8 
 4:♦︎8 
 5:♣️9 
 6:♥10
 7:♠️Q
 8:♦︎K
 9:♠️K
10:♦︎2
11:♠️2
カードの番号(♦︎A) : 10
```


カード交換の際も同様にカードのインデックスを入力します。

```rs
impl Player for Pc {
    ...
    fn get_needless_cards(&mut self, cards_count: usize) -> Vec<Card> {
        println!("{}", get_cards_with_indices(&self.hands));
        loop {
            let input = get_input(format!("不要なカードを{}枚選択: ", cards_count));
            let result = parse_idx(&input);
            if result.is_err() {
                continue;
            }
            let indices = result.unwrap();
            if indices.len() != cards_count {
                continue;
            }
            return indices
                .into_iter()
                .rev()
                .map(|idx| self.hands.remove(idx))
                .collect();
        }
    }
    ...
```

## おわりに

大富豪もプログラムに落とし込むと結構複雑なルールだと感じました。
ジョーカーという例外があるため、カードを扱う処理が思ってた以上に難しかったです。

また、Rustを使ったことでイテレータやトレイトについての理解も深めることができました。

## 参考文献

https://ja.wikipedia.org/wiki/%E5%A4%A7%E5%AF%8C%E8%B1%AA#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%83%AB%E3%83%BC%E3%83%AB

https://docs.rs/itertools/latest/itertools/

https://docs.rs/bitflags/latest/bitflags/