今回は、シンプルにトランプゲームの「ババ抜き」を作って、Rustのプログラミングの面白さを味わいましょう。特に、カードゲームを作るのには、高度な配列データの操作が必要となるため、Rustの配列(Vec型)の操作にも慣れることができます。
「ババ抜き」を作ろう
トランプを使ったゲームの中でも「ババ抜き」は、最も有名なものの一つです。誰しも遊んだことがあるので、Rustのプログラムを作るのに良い題材と言えます。
ここでは、プログラムを短くするために、「ババ抜き」のゲームのルールをシンプルにすることにしましょう。それで、参加者をプレイヤーとコンピューターの2人に限定します。
それでは、プログラムを作るのにあたって、ゲームの手順を箇条書きにしてみましょう。
1.ジョーカーを加えたカードをシャッフルして二人の参加者に均等に配ります。
2.それぞれの手札のカードの中で、同じ番号のカードがあれば、ペアにして捨て札にします。
3.プレイヤーがコンピューターの手札から1枚を引いて、同じ数字があれば捨て札にします。
4.コンピューターがランダムにプレイヤーの手札から1枚引いて、同じ数字があれば捨て札にします。
5.どちらかの手札がなくなるまで、3-4を繰り返し行います。
プロジェクトを作成しよう
それでは、cargoコマンドを利用してプロジェクトを作成しましょう。Rustをインストールすると、cargoコマンドが使えるようになります。cargoコマンドは、プロジェクトのひな形を作成したり、ライブラリをプロジェクトに追加したり、コンパイルしたりと、Rustプログラミングには欠かせないツールです。
ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、下記のコマンドを実行しましょう。
# プロジェクトフォルダを作成
mkdir old_maid
cd old_maid
cargo init
# 乱数のため「lazyrand」クレートを追加
cargo add lazyrand
以上の手順により、次のようなファイルが作成されます。
.
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
この中の、src/main.rsがメインファイルです。このファイルを編集しましょう。今回作成するババ抜きのプログラムの全体は、こちらのGist( https://gist.github.com/kujirahand/4cdc61414178d4044cce8cf724f45317 )にアップロードしています。
プログラムを実行するには、ターミナで下記のコマンドを実行します。
cargo run
実行すると、下記のようにババ抜きが始まります。プレイヤーは指示に従って、相手の何枚目のカードを引くのかを数字を入力して[Enter]キーを押します。するとゲームが進んでいきます。
トランプカードを表現しよう
今回の最大のポイントとなるのが、トランプをどのように表現するかという点にあります。トランプの表現方法はいろいろありますが、今回はトランプの各カードを0から52のカード番号で表現することにします。これなら、Rustのu8型で表現できます。
そして、次の画像のように、カード番号とカードの意味を対応させることにします。
このような並びにしておけば、次の計算式で、カード番号cnoから、マークとカードの数字を取得できます。
カードのマーク: cno / 13
カードの番号: cno % 13 + 1
それで、次のようなプログラムを作りました。関数get_card_labelで、手軽にマークと数字を文字列で取得できます。
const JORKER: u8 = 52; // ジョーカーは52番
const CARD_SUIT: [&str; 5] = ["♠", "♥", "♣", "♦", ""]; // カードのマーク
const CARD_NUMS: [&str; 14] = [ // カードの数字ラベル
"Jok", "A", "2", "3", "4", "5", "6",
"7", "8", "9", "10", "J", "Q", "K"];
// カード番号を与えてカードの数字を取得する
fn get_card_num(cno: u8) -> u8 {
if cno == JORKER { 0 } else { cno % 13 + 1 }
}
// カード番号を与えてカードのラベルを取得する
fn get_card_label(cno: u8) -> String {
format!("[{}{:>2}]",
CARD_SUIT[(cno / 13) as usize],
CARD_NUMS[get_card_num(cno) as usize])
}
// --- 関数をテストするコード ---
mod tests {
use super::*;
#[test]
fn test_get_card_label() {
assert_eq!(get_card_label(52), "[Jok]"); // cnoが52がジョーカー
assert_eq!(get_card_label(0), "[♠ A]"); // cnoの0が[♠ A]
assert_eq!(get_card_label(12), "[♠ K]");
assert_eq!(get_card_label(13), "[♥ A]");
}
}
ペアのカードを見つけて削除しよう
次に、同じ数字のカードを探して、ペアがあれば削除する関数remove_pairを作ってみましょう。ここでは、ペアの検出と削除を効率よく行うために、インデックス記録用の配列 exists_nums を使って、一度見つけたカードの情報を覚えておき、同じ数字がもう一度出てきたときにその2枚を削除対象とします。
// ペアがあれば削除して捨てた組数を返す
fn remove_pair(cards: &mut Vec<u8>) -> usize {
let mut remove_cards = vec![]; // 削除用にカード番号を記録
let mut exists_nums = [-1; 14]; // 存在する数字を記録(-1は未発見)
// すべてのカードを調べてペアを探す --- (※1)
for i in 0..cards.len() {
let num = get_card_num(cards[i]);
if exists_nums[num as usize] < 0 {
exists_nums[num as usize] = i as isize; // インデックスを記録
continue;
}
// すでに同じ数字のカードが存在する場合は削除マーク --- (※2)
remove_cards.push(i);
remove_cards.push(exists_nums[num as usize] as usize);
exists_nums[num as usize] = -1; // 同じ数字のカードは削除済み
}
let remove_count = remove_cards.len();
// 削除マークのカードを削除 --- (※3)
remove_cards.sort();
for &i in remove_cards.iter().rev() {
cards.remove(i);
}
lazyrand::shuffle(cards); // 削除後にシャッフル
return remove_count / 2;
}
具体的には、(※1)で手札のカード(cards)を先頭から1つずつ調べていって、変数exists_numsにカードのインデックスを記録していきます。もし、同じ数字があれば、(※2)で削除対象を記録する変数remove_cardsにカードのペアを追加します。
そして、最後(※3)にて、remove_cardsを元にカードを削除するという流れになっています。ここで注意が必要なのは、配列の末尾から要素を削除するという点です。これはインデックス番号が変わらないようにするために必要な処理です。
プレイヤーとコンピューターのターンを作ろう
上記で作った関数を利用して、ゲームを完成させましょう。いくつか上記で紹介していない関数もありますが、それはこちら( https://gist.github.com/kujirahand/4cdc61414178d4044cce8cf724f45317 )の方で確認してください。
fn main() {
// ジョーカーを加えたカード(53枚)を作成してシャッフル --- (※1)
let mut cards = (0..53).collect::<Vec<u8>>();
shuffle(&mut cards);
// それぞれの手札に分配
let mut user_hands = cards[0..26].to_vec();
let mut com_hands = cards[26..].to_vec();
print!("\x1b[2J\x1b[H"); // 画面をクリア
// それぞれの手札からペアを削除 --- (※2)
println!("貴方は{}組のカードを捨てました。", remove_pair(&mut user_hands));
println!("相手は{}組のカードを捨てました。", remove_pair(&mut com_hands));
// どちらかの手札が0になるまで繰り返す --- (※3)
loop {
if check(&user_hands, &com_hands) { break; }
// ユーザーの入力を取得 --- (※4)
let index = input_number(
">>> 何枚目を引きますか", com_hands.len());
// ユーザーの手札にカードを追加
let card = com_hands.remove(index - 1);
user_hands.push(card);
println!(">>> 貴方は{}を引きました", get_card_label(card));
if check(&user_hands, &com_hands) { break; }
if remove_pair(&mut user_hands) > 0 {
println!(">>> 成功!! 貴方はカードを捨てました。");
if check(&user_hands, &com_hands) { break; }
}
input(">>> [Enter]を押してください");
// コンピューターの番 --- (※5)
let card = user_hands.remove(randint(0, user_hands.len() as i64 - 1) as usize);
com_hands.push(card);
println!("<<< 相手が貴方のカード{}を引きました。", get_card_label(card));
if check(&user_hands, &com_hands) { break; }
if remove_pair(&mut com_hands) > 0 {
println!("<<< 残念.. 相手はカードを捨てました。");
if check(&user_hands, &com_hands) { break; }
}
}
println!("ゲーム終了です。");
}
メインプログラムを確認してみましょう。(※1)では、ジョーカーを含む全部で53枚のカードを作成してシャッフルします。0から51が普通のカードで、52がジョーカーです。その後、ユーザーの手札(user_hands)と、コンピューターの手札(com_hands)に分割します。
(※2)では、それぞれの手札から同じ数字のカードがあれば削除します。
(※3)以降がゲームのメインループとなります。プレイヤーのターン(※4)とコンピューターのターン(※5)の処理を記述しています。
まとめ
以上、今回はババ抜きを実装してみました。分かりやすさを優先したコードにしてみました。このようなカードゲームを作成する場合、配列やVec型の基本的な操作が頻出します。自分で、ゲームを実装してみると、楽しくRustプログラミングになれることができるでしょう。本稿を参考にしつつ作ってみると良いでしょう。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。