はじめに
最近、Slay the Spireやドミニオン等のカードゲームにハマっています。
毎回手に入るカードが違うためプレイする毎に戦略が変わります。
空き時間にちょっと遊ぶつもりでも熱中してしまうくらい面白いです。
デジタルカードゲームはデッキのシャッフルやカードの配布を自動で行ってくれるので便利ですね。
遊んでいて自分でもカードゲームを作りたくなりました。
といっても上記の様なゲームは複雑すぎるので、トランプの定番である大富豪を作ることにしました。
プログラミング言語は個人的に好きなRustを使いました。🦀
成果物
ターミナル上で動作する大富豪を作りました。
場に出すカードの番号を入力します。
カードの番号(XX):
のXX
には場に出されたカードが表示されます。
パスする場合は何も入力せずEnterキーを押します。
画面上には以下の情報が表示されます。
プレイヤー名 [手札の枚数]: 場に出したカード
ルール
大富豪はローカルルールを含め様々なルールがあります。
今回は以下のルールに絞って実装しました。
- プレイヤーは4人で固定
- 2、8、ジョーカーで上がってはいけない(反則上がり)
- 8を出すと強制的に場が流れる(8切り)
- 同じスートのカードが2回連続で出た場合は、そのスートのカードしか出せなくなる(縛り)
トランプ
スートと数字
スートと数字は列挙体で定義します。
比較やコピーができるように各種アトリビュートを実装します。
また、各値は弱い順に並んでいます。
#[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,
}
カードの定義
カードも列挙体で定義します。
カードには以下の2種類があります。
- 数字とスート持つ通常のカード
- 数字とスートを持たないジョーカーのカード
#[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
数字、スート順で比較
この関数は配られた手札をソートする際に使用します。
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
の値で比較します。
// 以下の様ににソートされる
// [♠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]);
数字のみ比較
上記の関数では同じ数字のカードを比較しても♣ < ♦ < ♥ < ♠
の順で大小関係を判定します。
なので数字のみを比較する関数も定義します。
場に出ているカードと手札のカードの大小関係は主にこの関数で判定します。
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
関数の使いまわしができません。
// これだとジョーカーが最弱になってしまう
pub fn cmp_rank_reversely(c1: &Card, c2: &Card) -> std::cmp::Ordering {
cmp_rank(c2, c1)
}
そのため、逆転した際のロジックを書く必要があります。
通常のカード同士の場合は逆順で比較し、どちらかがジョーカーの場合はcmp_rank
と同じように比較します。
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枚以上のカード(階段)
上記の組み合わせも列挙体で定義します。
#[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
への変換処理は簡単です。
// 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
トレイトを実装します。
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
関数の判定ロジックは以下の通りです。
- ジョーカーを削除する
- 各カードの数字を抽出する
- 隣り合う数字同士を比較して全て同じか調べる
例えばis_same_ranks
関数に♠6、Joker、♥6、♦6
を渡すと
♠6、♥6、♦6
6、6、6
6 == 6 == 6
となりtrue
を返します。
コードは以下の様になります。
ジョーカーの削除と数字の抽出はfilter_map
関数で一度に行っています。
tuple_windows
関数で隣り合う要素のタプルを取得しています。
これはitertools
という外部クレートの関数です。
// 全てのカードが同じ数字か判定する
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)
}
// 数字が同じ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
関数で判定)
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
とほぼ同じです。
抽出する値が数字からスートに変わっただけです。
// 全てのカードが同じスートか判定する
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または-1か調べる
例えば♦3、♦4、♦5、♦6
の場合
3、4、5、6
1、1、1
1 == 1 == 1
となりtrue
を返します。
Rank
は列挙体なので差分を求めることはできません。
なのでRank
を整数型に変換するトレイトを実装します。
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の場合、カードが連続して並んでいることが分かります。
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で上記のロジックを実装すると以下の様になります。
ジョーカーの置き換え処理が追加されている以外は、含まない場合の処理とほとんど同じです。
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
関数全体のコードを載せます。
is_seq関数
// カードの数字が連続しているか判定する
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);
}
}
}
Combの比較
Comb
同士を比較するis_greater
関数を実装します。
これは比較対象のComb
より数字が強ければtrue
を返す関数です。
引数に比較対象のComb
の参照と比較関数を取ります。
この比較関数を通常時と革命時で変えることで、カードの強さが変わります。
// ♣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
を返しています。
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
トレイトです。
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
だけで全てを管理するのは責任過剰なので、別の構造体に機能を委譲しています。
pub struct Field {
prev_comb: Option<Comb>,
indexer: Indexer,
binder: SuitBinder,
pass_counter: usize,
is_rev: bool,
}
put関数
カードを場に出す処理を担うのがput関数です。
引数に場に出す組み合わせと残りの手札枚数を渡します。
手札の枚数が0になったらそのプレイヤーは上がりとなります。
パスをする場合はNone
を渡します。
場に出されたカードよって様々なイベントが発生します。(革命、8切りなど)
返り値に発生したイベントのフラグを返します。
impl Field {
...
pub fn put(&mut self, new_comb: Option<Comb>, hands_count: usize) -> Flags {
}
革命
is_rev_comb
関数は4枚のカードを持つComb::Multi
か判定する関数です。
返り値のフラグにFlags::REV
を付与します。
...
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
を設定することで場が流れ、同じプレイヤーが続けて任意のカードを出せます。
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
を付与します。
...
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
を返します。
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
トレイトを定義します。
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
が場に出ている場合
手札のカードを弱い順に調べていき、場のカードより強いものがあればそれを返します。
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
が場に出ている場合
カードを数字ごとにグループ分けして、その中で場に出せる最弱の組み合わせを調べます。
例えば場に♦5 ♠5
があり、手札が♥6 ♣7 ♥7 ♦8 ♥8 ♠8
である場合
数字ごとにグループ分けすると[♥6] [♣7 ♥7] [♦8 ♥8 ♠8]
となり
場に出せる最弱の組み合わせである♣7 ♥7
が見つかります。
実際はget_indices_grouped_by_rank
関数で数字ごとにグループ分けしたカードのインデックスを取得します。
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
が場に出ている場合
カードをスートごとにグループ分けして、その中で場に出せる最弱の組み合わせを調べます。
例えば場に♣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
関数でグループ分けしたカードの中から
階段となっているカードの組み合わせを見つけます。
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枚の順で出せる組み合わせを探します。
それぞれの組み合わせの見つけ方は上記の方法とほぼ同じです。
また、カード交換の際は最弱のカードを不要なカードとして出します。
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)を実装します。
キーボードで出すカードのインデックスを入力します。
なので、入力された文字列をインデックスに変換する処理が必要です。
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(()),
}
}
以下の条件を全て満たした場合に手札のカードを場に出せます。
- 入力したインデックスが全て範囲内
- 選択したカードの組み合わせが有効
- カードの組み合わせが場に出せる
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
カード交換の際も同様にカードのインデックスを入力します。
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を使ったことでイテレータやトレイトについての理解も深めることができました。
参考文献