今日の内容
- ゲーム作り前編:「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!
ブラックジャックを作るために、新しいプロジェクトを作成します。
cargo new blackjack
cd blackjack
Cargo.toml内にrandクレートの説明を追加します。
[package]
name = "blackjack"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
cargo build
して以下のようになれば導入完了です。
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!");
}
}