0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(DAY15) 俺と学ぶRust言語~BlackJack(1)~

Last updated at Posted at 2025-01-08

今日の内容

  • ゲーム作り前編:「BlackJack」

はじめに

前回は構造体について学びました。さて、ここまでを通して様々なことを学んできましたが、今日はその知識を生かして簡単な「BlackJack」ゲームを作成したいと思います。今日中に終わらなかったので、明日には完成させます。

BlackJackとは

BlackJack(ブラックジャック)はカードゲームで、ディーラーとプレイヤーの対戦型ゲームです。割と簡単なルールなので、プログラミング初心者でも簡単に作ることができます。

  • ルール

    • カードはトランプカード(♡、♢、♧、♤の四種類がそれぞれ1~13まである)
    • プレイヤーはディーラーよりカードの合計が「21」に近ければ勝ち
    • ただし、「21」を超えたらその時点で負け
    • 点数は、2~10はそのまま、「J・Q・K」は全て10点と数える
    • 「A」は1点か11点のどちらか、都合の良い方で数える
  • 進行

    • カードを2枚ずつディーラーとプレイヤーに配る
    • ディーラーは2枚のうち1枚を表向き(アップカードと呼ぶ)にする
    • プレイヤーのカード2枚が「A」と「10・J・Q・K」のどれか1つの組み合わせなら、「ナチュラルブラックジャック」となる -> もしディーラーも「ナチュラルブラックジャック」であれば引き分け、でなければプレイヤーの勝ち
    • 「ナチュラルブラックジャック」でないなら、プレイヤーは「Hit」と「Stand」の二択を選択する
    • 「Hit」->カードをもう1枚ひく
    • 「Stand」->その時点での合計点で勝負する
    • 「Hit」をしても21を超えなければ、何回でも「Hit」できる
    • 「Hit」をして21を超えたら、「bust」となり、その時点でプレイヤーの負けとなる
    • プレイヤーが行動を終えると、ディーラーは伏せられたカード「ホールカード」を表向きにする
    • ディーラーは合計が17点以上になるまで引き続ける(17点以上では引くことはできない)
    • ディーラーがbustしたら、bustしてないプレイヤーの勝ち

プログラムで書こうとしても、そんなに難しくないルールです。ディーラー(CPU)の選択肢もランダムで選んで支障ないので、さっそく書いていきます。

BlackJack!

ブラックジャックを作るために、新しいプロジェクトを作成します。

cmd
cargo new blackjack
cd blackjack

Cargo.toml内にrandクレートの説明を追加します。

Cargo.toml
[package]
name = "blackjack"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8.5"

cargo buildして以下のようになれば導入完了です。

cmd
cargo build
   Compiling cfg-if v1.0.0
   Compiling byteorder v1.5.0
   Compiling getrandom v0.2.15
   Compiling rand_core v0.6.4
   Compiling zerocopy v0.7.35
   Compiling ppv-lite86 v0.2.20
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling blackjack v0.1.0 (C:\Users\username\blackjack)
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.79s

ゲームの進行に沿ってプログラムを書いていきます。

まずは、カードを2枚ずつプレイヤーとディーラー(CPU)に配ります。
カードですが、今回は可読性重視のタプル型ベクタを用意しました。コピペしてください。
また、今回は2デッキ分使います。

fn main() {
   let mut cards: Vec<(char, u32)> = vec![
        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),

        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),
    ];
}

なぜベクタにしたかというと、要素数を動的に変更できるからです。当然ですが、現実世界でデッキを用意してカードを1枚引いたとき、デッキからは1枚減りますよね?それを忠実に再現することで、プログラムの動作を理解しやすいものにしました。
次に、randクレートを利用してベクタから1枚取る関数「rnd_card」を作成します。デッキからカードを取りたいとき、必要な枚数分この関数を呼び出せば良くなるわけです。

use rand::prelude::*;

fn rnd_card(cards: &mut Vec<(char, u32)>) -> (char, u32){
    //ベクタcardsから要素を1つ取得し、ベクタからそれを削除してから返す。
    let x = thread_rng().gen_range(0..cards.len());
    let card = cards[x];
    cards.remove(x);
    card
}

タプル(char, u32)型ベクタを引数とし、タプル(char, u32)を返す関数です。つまり、main関数内で、引数に「&mut cards」を渡して呼び出すと、その要素を返す関数にします。
中身では「0 < x < ベクタの長さ-1」の範囲で乱数を生成し、xに入れています。
変数cardにベクタのx番目を入れ、ベクタのx番目を消し、変数cardを返します。

main関数内で「player」という変数にcardsから1枚入れたければ、以下のようにすると良いわけです。

fn main() {
    let mut cards: Vec<(char, u32)> = vec![
        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),

        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),
    ];
    let player = rnd_card(&mut cards);
    println!("{:?}", player);
}

しかしこれでは、プレイヤーの手札が2枚、3枚も増えるたびに変数を用意しなくてはなりません。そこで、またベクタを使います。ディーラーの分もベクタを用意し、「プレイヤーのベクタの0番目=(♡のA)」というように実装します。

fn main() {
    let mut cards: Vec<(char, u32)> = vec![
        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),

        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),
    ];
    //playerとcpuの手札
    let mut player: Vec<(char, u32)> = Vec::new();
    let mut cpu: Vec<(char, u32)> = Vec::new();
}

そして、playerベクタとcpuベクタにrnd_cardの結果をそれぞれ2回追加します。

fn main() {
    ...
    //playerとcpuの手札
    let mut player: Vec<(char, u32)> = Vec::new();
    let mut cpu: Vec<(char, u32)> = Vec::new();
    //playerとcpuの初期のカードを手札に加える
    player.push(rnd_card(&mut cards));
    player.push(rnd_card(&mut cards));
    cpu.push(rnd_card(&mut cards));
    cpu.push(rnd_card(&mut cards));
}

次にこれを表示する関数を作ります。playerとcpuを渡すことで全部表示させたり、一部を渡すことでそこだけ表示させたりできるようにしました。

fn print_card(player: &Vec<(char, u32)>, cpu: &Vec<(char, u32)>) {
    print!("You: ");
    for i in 0..player.len() {
        let a =(player[i].1).to_string();
        print!("{}{}", player[i].0, match player[i].1 { 1=>"A", 11=>"J", 12=>"Q", 13=>"K", _=> a.as_str()});
        if i != player.len()-1 { print!(", ") }
    }
    print!("\nCPU: ");
    for i in 0..cpu.len() {
        let a =(cpu[i].1).to_string();
        print!("{}{}", cpu[i].0, match cpu[i].1 { 1=>"A", 11=>"J", 12=>"Q", 13=>"K", _=> a.as_str()});
        if i != cpu.len()-1 { print!(", ") }
    }
    println!("");
}

例えば、player全部を表示させたければplayerベクタの参照を、cpuの1つ目だけを表示させたければ引数に&vec![cpu[0]]を与えればよいです。
最初はプレイヤーのカード2枚全部と、CPUのカード1枚を表示させたいので、main関数で以下のように呼びます。

fn main() {
    ...
    print_card(&player, &vec![cpu[0]]);
}

さて、ここまででカードを2枚ずつプレイヤーとディーラー(CPU)に配ることができました。

次にプレイヤーのカードの合計点を計算し、もしナチュラルブラックジャックであればディーラーのホールカードを表にして比べるプログラムを実装します。

...
fn count_point(mut point: u32, vec: &Vec<(char, u32)>, player: bool) -> u32 {
    for i in vec {
        if player == true {
            if i.1 == 1 {
                println!("Which would you choose for 'A': 1 or 11?");
                loop{
                    let mut a = String::new();
                    print!("> ");
                    stdout().flush().unwrap();
                    stdin().read_line(&mut a).expect("Can't read it!");
                    let a:u32 = a.trim().parse().expect("Type a number!");
                    if a==1 || a==11 {
                        if a == 11 { point+=10; }
                        break;
                    }
                }
            }
        } else if player == false {
            if i.1 == 1 && point+11<=21{ point+=10;}
        }
        point += match i.1 { 11=>10, 12=>10, 13=>10, _=> i.1 };
    }
    point
}

fn main() {
    let point:(u32, u32) = (0, 0);
    ...
    //現在の合計点を表示
    point.0 = count_point(point.0, &player, true);
    point.1 = count_point(point.1, &cpu, false);
    println!("Current your point: {}", point.0);
}

変数pointは、「(プレイヤーのポイント, CPUのポイント)」になります。count_point関数は引数に応じてプレイヤーとCPUのどちらのポイントも計算することができます。
さらにプレイヤーの場合、3つめの引数はtrueとし、もし「A」であれば1点か11点かを選ぶことができます。もしCPUであれば、「A」は21を超えそうなら1点で、でなければ11点にするとしています。

おわりに

すみませんが、今日中に終わらなかったので、区切りの良いここまでとします。明日には完成させたいです..💦
次回もBlackJackを作っていきます。最近忙しいので毎日投稿大変ですが、これからも頑張っていきたいと思います。
ご清聴ありがとうございました。

追記

一応完成したものがこちらです。

use std::io::*;
use rand::prelude::*;

fn rnd_card(cards: &mut Vec<(char, u32)>) -> (char, u32){
    //ベクタcardsから要素を1つ取得し、ベクタからそれを削除してから返す。
    let x = thread_rng().gen_range(0..cards.len());
    let card = cards[x];
    cards.remove(x);
    card
}

fn print_card(player: &Vec<(char, u32)>, cpu: &Vec<(char, u32)>) {
    print!("You: ");
    for i in 0..player.len() {
        let a =(player[i].1).to_string();
        print!("{}{}", player[i].0, match player[i].1 { 1=>"A", 11=>"J", 12=>"Q", 13=>"K", _=> a.as_str()});
        if i != player.len()-1 { print!(", ") }
    }
    print!("\nCPU: ");
    for i in 0..cpu.len() {
        let a =(cpu[i].1).to_string();
        print!("{}{}", cpu[i].0, match cpu[i].1 { 1=>"A", 11=>"J", 12=>"Q", 13=>"K", _=> a.as_str()});
        if i != cpu.len()-1 { print!(", ") }
    }
    println!("");
}

fn count_point(mut point: u32, vec: &Vec<(char, u32)>, player: bool) -> u32 {
    for i in vec {
        if player == true {
            if i.1 == 1 {
                println!("Which would you choose for 'A': 1 or 11?");
                loop{
                    let mut a = String::new();
                    print!("> ");
                    stdout().flush().unwrap();
                    stdin().read_line(&mut a).expect("Failed!");
                    let a:u32 = a.trim().parse().expect("Type a number!");
                    if a==1 || a==11 {
                        if a == 11 { point+=10; }
                        break;
                    }
                }
            }
        } else if player == false {
            if i.1 == 1 && point+11<=21{ point+=10;}
        }
        point += match i.1 { 11=>10, 12=>10, 13=>10, _=> i.1 };
    }
    point
}

fn main() {
    //初期化
    let mut point: (u32, u32) = (0, 0);
    let mut cards: Vec<(char, u32)> = vec![
        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),

        ('♡',1),('♡',2),('♡',3),('♡',4),('♡',5),('♡',6),('♡',7),('♡',8),('♡',9),('♡',10),('♡',11),('♡',12),('♡',13),
        ('♢',1),('♢',2),('♢',3),('♢',4),('♢',5),('♢',6),('♢',7),('♢',8),('♢',9),('♢',10),('♢',11),('♢',12),('♢',13),
        ('♧',1),('♧',2),('♧',3),('♧',4),('♧',5),('♧',6),('♧',7),('♧',8),('♧',9),('♧',10),('♧',11),('♧',12),('♧',13),
        ('♤',1),('♤',2),('♤',3),('♤',4),('♤',5),('♤',6),('♤',7),('♤',8),('♤',9),('♤',10),('♤',11),('♤',12),('♤',13),
    ];
    //playerとcpuの手札
    let mut player: Vec<(char, u32)> = Vec::new();
    let mut cpu: Vec<(char, u32)> = Vec::new();
    //playerとcpuの初期のカードを手札に加える
    player.push(rnd_card(&mut cards));
    player.push(rnd_card(&mut cards));
    cpu.push(rnd_card(&mut cards));
    cpu.push(rnd_card(&mut cards));
    
    print_card(&player, &vec![cpu[0]]);
    //現在の合計点を表示
    point.0 = count_point(point.0, &player, true);
    point.1 = count_point(point.1, &cpu, false);
    //ナチュラルブラックジャックであれば、引き分けか勝ちか判定する。
    if point.0 == 21 {
        println!("BlackJack!");
        print_card(&player, &cpu);
        if point.1  == 21 {
            println!("draw!");
        } else {
            println!("You win!");
        }
        return;
    }
    //HitかStandかを繰り返す
    println!("Current your point: {}", point.0);
    loop {
        let mut a = String::new();
        println!("Hit or Stand");
        print!("> ");
        stdout().flush().unwrap();
        stdin().read_line(&mut a).expect("Failed");
        if a.trim() == "Hit" || a.trim() == "Stand" {
            if a.trim() == "Hit" {
                player.push(rnd_card(&mut cards));
                print_card(&player, &vec![cpu[0]]);
            } else {
                break;
            }
        } else {
            continue;
        }
        point.0 = count_point(point.0, &vec![player[player.len()-1]], true);
        println!("Current your point: {}", point.0);
        if point.0 > 21 {
            println!("You lose..");
            return;
        }
    }
    //プレイヤー行動終了。
    //CPUがホールカードを公開する。
    print_card(&player, &cpu);
    println!("Current cpu's point: {}", point.1);
    //CPUが17点以上になるまで引き続ける。
    loop {
        print!("CPU: ");
        if point.1 < 17 {
            println!("Hit");
            cpu.push(rnd_card(&mut cards));
            point.1 = count_point(point.1, &vec![cpu[cpu.len()-1]], true);
            print_card(&player, &cpu);
            println!("Current cpu's point: {}", point.1);
            if point.1 >21 {
                println!("You win!");
                return;
            }
        } else {
            println!("Stand");
            break;
        }
    }
    //CPU行動終了。
    //ポイントを比較し、高い方の勝ち。
    if point.1 > point.0 {
        println!("You lose..");
    } else if point.1 < point.0 {
        println!("You win!")
    } else {
        println!("draw!");
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?