自己紹介
出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。
Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。
環境
新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
RAM: 8.00GB
Rust: 1.37.0
Editor: Vim 8.1.1
Terminal: PowerShell
前回
前回はWebサーバを作りながらクレートの使い方やmatchを学びました。
Rust勉強中 - その4
何か作りたいメータが100%になりました
エンジニアの皆さんならわかっていただけると思います。学習しているうちに「もう何か作ってみたい!」や「ここまでのこと組み合わせたら、こういうの作れるかも!」と思う時がありますよね。久しぶりにその感覚がやってまいりました。それだけRustが魅力的で楽しいということだと思います。Rustのイロハもまだわかっておりませんが、こうなると作って痛い目をみるしかないんです。
で、作るしかありませんねあれを。そう!「ブラックジャック」を!
ブラックジャックとは
トランプカードの数字を足し合わせて相手より21点に近づけば勝ちというゲームです。22点以上になるとバーストといい、無条件で負けとなります。10, J, Q, Kはどれも10として数えます。Aは1または11として有利な方に数えられます。Aと10,J,Q,Kの組み合わせはブラックジャックとなります。
今回作るのは、今まで学んだことでできる範囲にしたいので、ブラックジャックの掛け金やインシュランス、スプリットなどのアクションは無しにして、めちゃシンプルブラックジャックにします。
フロー
- プレイヤー、CPUに2枚ずつカードを配布
- プレイヤーはCPUの2枚のカードのうち1枚を見て、ヒット(一枚追加)かスタンド(勝負)を選択。スタンドを選ぶか、バーストするまでこれを続行。
- プレイヤーがバーストならCPUの勝ち。それ以外ならCPUもヒットとスタンドを選択し、スタンドかバーストするまでこれを続行。
色々難しそうです。カードも同じ数字は4枚までとか、CPUどないやって作るねんとか。
あと、main.rsに全部ぶっこみます。ファイルの分け方まだ学習していないので。
カード配布
まず、カードをプレイヤーとCPUに配布できなければいけません。
randクレート
何はともあれ、ランダムにカードを選択しないといけないなと思いました。
そこで、randクレートを使います。randは疑似乱数を生成してくれます。
crates.io - rand
rand Documentation
使い方は以下のような感じ。
use rand::Rng;
use rand::seq::SliceRandom;
fn main() {
let mut rng = rand::thread_rng();
let n: u8 = rng.gen();
println!("Random u8: {}", n);
println!("Random range: {}", rng.gen_range(0, 10));
let nums: Vec<u8> = (1..53).collect();
println!("Random choose: {}", nums.choose(&mut rng).unwrap());
}
$ cargo run
Compiling blackjack v0.1.0 (C:\Users\deta\hack\rust\blackjack)
Finished dev [unoptimized + debuginfo] target(s) in 0.88s
Running `target\debug\blackjack.exe`
Random u8: 101
Random range: 9
Random choose: 35
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target\debug\blackjack.exe`
Random u8: 205
Random range: 0
Random choose: 45
collectメソッドはVecに変換してくれるようです。Vecからランダムに要素を選び出すメソッドも用意されているのでこれが使えそうです。
カード作成
52枚のカードを作成します。
fn initialize_cards() -> Vec<u8> {
let mut cards: Vec<u8> = Vec::new();
for i in 1..14 {
cards.extend(vec![i; 4]);
}
cards
}
fn main() {
let mut cards: Vec<u8> = initialize_cards();
println!("cards: {:?}", cards);
}
extendメソッドは既存のVectorとほかのVectorを結合します。
また、vec![値; サイズ]
と指定することで、任意の値とサイズを持ったVectorが作成できます。
カード一枚選択
randクレートが使えるようになったので、カードを一枚選択する関数を作ってみます。
use rand::Rng;
fn initialize_cards() -> Vec<u8> {
let mut cards: Vec<u8> = Vec::new();
for i in 1..14 {
cards.extend(vec![i; 4]);
}
cards
}
fn hit<R: Rng>(cards: &mut Vec<u8>, rng: &mut R) -> Option<u8> {
if cards.is_empty() {
None
} else {
let index = rng.gen_range(0, cards.len());
Some(cards.swap_remove(index))
}
}
#[test]
fn hit_test() {
let mut rng = rand::thread_rng();
assert_eq!(hit(&mut Vec::new(), &mut rng), None);
assert_eq!(hit(&mut vec![], &mut rng), None);
assert_eq!(hit(&mut vec![0], &mut rng), Some(0));
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
println!("hit: {}", hit(&mut cards, &mut rng).unwrap());
println!("cards: {:?}", cards);
}
ブラックジャックではカードを一枚追加するのをヒットというらしいのでそういう名前にしました。
fn hit<R: Rng>(cards: &mut Vec<u8>, rng: &mut R) -> Option<u8> {
については、<R: Rng>
は「Rngを実装する任意の型Rに対して」と読むそうです。このRを型パラメータといいます。仮引数はどちらも可変参照しています。戻り値はOption型で返します。つまり、カードが引ければSome(u8)を、失敗すればNoneを返します。
ところで、さきほどの型パラメータ部分をなくして、最初はfn hit(cards: &mut Vec<u8>, rng: &mut Rng) -> Option<u8> {
としていました。これだとコンパイル時にエラーを吐いてしまいました。
$ cargo run
Compiling blackjack v0.1.0 (C:\Users\deta\hack\rust\blackjack)
warning: trait objects without an explicit `dyn` are deprecated
--> src\main.rs:5:39
|
5 | fn hit(cards: &mut Vec<u8>, rng: &mut Rng) -> Option<u8> {
| ^^^ help: use `dyn`: `dyn Rng`
|
= note: #[warn(bare_trait_objects)] on by default
error[E0038]: the trait `rand::Rng` cannot be made into an object
--> src\main.rs:5:1
|
5 | fn hit(cards: &mut Vec<u8>, rng: &mut Rng) -> Option<u8> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `rand::Rng` cannot be made into an object
|
= note: method `gen` has generic type parameters
= note: method `gen_range` has generic type parameters
= note: method `sample` has generic type parameters
= note: method `fill` has generic type parameters
= note: method `try_fill` has generic type parameters
error: aborting due to previous error
For more information about this error, try `rustc --explain E0038`.
error: Could not compile `blackjack`.
To learn more, run the command again with --verbose
私が考えているエラーの理由は、Rngは型ではなくトレイトだからだと思います。なので本来型を指定するところにトレイトを指定してしまったのでエラーを吐いたということなのではないでしょうか。
もう一つ、最初以下のような書き方をしていました。
extern crate rand;
use rand::Rng;
use crate::rand::prelude::SliceRandom;
fn hit<R: Rng>(cards: &mut Vec<u8>, rng: &mut R) -> Option<u8> {
let res = cards.choose(rng);
match res {
Some(n) => {
let index = cards.iter().position(|x| x==n).unwrap();
cards.remove(index);
Some(*n)
},
None => {return None}
}
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = (1..53).collect();
println!("Random choose: {}", hit(&mut cards, &mut rng).unwrap());
println!("cards: {:?}", cards);
}
こうすると以下のようなエラーが発生します。
$ error[E0502]: cannot borrow `*cards` as mutable because it is also borrowed as immutable
--> src/main.rs:10:13
|
6 | let res = cards.choose(rng);
| ----- immutable borrow occurs here
...
10 | cards.remove(index);
| ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
11 | Some(*n)
| -- immutable borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
これは、let res = cards.choose(rng);
がすでにイミュータブルな借用をしているのに、その中で、cards.remove(index);
のようなミュータブルな借用をしようとしているためエラーが発生しました。この場合、Some(n) => {
のところをSome(&n) => {
として、参照を外してあげる(と言っていいのかな?)ことで値を取り出してあげると解決します。
さらに、removeメソッドやswap_removeメソッドは削除した値を返すことから、indexをランダムにすればよいということで、コード例までいただきアドバイスいただきました。このへんのやり取りは、日本のRustのslackコミュニティで助けていただきました。(qnighyさん、S.Percentageさんありがとうございました)
予想通り痛い目をみながらも(だた楽しい)、カード一枚の選択はこれでできそうです。
プレイヤー、CPUへの初期カード配布
use rand::Rng;
fn initialize_cards() -> Vec<u8> {
let mut cards: Vec<u8> = Vec::new();
for i in 1..14 {
cards.extend(vec![i; 4]);
}
cards
}
fn initialize_player<R: Rng>(cards: &mut Vec<u8>, rng: &mut R) -> Option<Vec<u8>> {
let mut player: Vec<u8> = Vec::new();
for _i in 0..2 {
match hit(cards, rng) {
Some(card) => {
player.push(card);
},
None => {}
}
}
if player.len()==2 {
Some(player)
} else {
None
}
}
#[test]
fn initialize_player_test() {
let mut rng = rand::thread_rng();
assert_eq!(initialize_player(&mut vec![], &mut rng), None);
assert_eq!(initialize_player(&mut vec![1], &mut rng), None);
assert_eq!(initialize_player(&mut vec![1, 1], &mut rng), Some(vec![1, 1]));
}
fn hit<R: Rng>(cards: &mut Vec<u8>, rng: &mut R) -> Option<u8> {
if cards.is_empty() {
None
} else {
let index = rng.gen_range(0, cards.len());
Some(cards.swap_remove(index))
}
}
#[test]
fn hit_test() {
let mut rng = rand::thread_rng();
assert_eq!(hit(&mut Vec::new(), &mut rng), None);
assert_eq!(hit(&mut vec![], &mut rng), None);
assert_eq!(hit(&mut vec![0], &mut rng), Some(0));
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
println!("Your cards: {:?}", player);
println!("CPU card: {}", cpu[0]);
// println!("cards: {:?}", cards);
// println!("player: {:?}", player);
// println!("cpu: {:?}", cpu);
}
プレイヤーとCPUそれぞれにカードを2枚ずつ配ります。特に真新しいところはなさそうなので説明は割愛します。
カードの数値を計算
fn calculate(mut cards: Vec<u8>) -> u8 {
let mut result = 0;
cards.sort_unstable_by(|a, b| b.cmp(a));
for card in cards {
if card>=10 {
result+=10;
} else if card==1 {
if result+11>21 {
result+=1;
} else {
result+=11;
}
} else {
result+=card;
}
}
result
}
#[test]
fn calculate_test() {
assert_eq!(calculate(vec![]), 0);
assert_eq!(calculate(vec![2]), 2);
assert_eq!(calculate(vec![1]), 11);
assert_eq!(calculate(vec![2, 3]), 5);
assert_eq!(calculate(vec![1, 2]), 13);
assert_eq!(calculate(vec![13, 13]), 20);
assert_eq!(calculate(vec![13, 1]), 21);
assert_eq!(calculate(vec![13, 13, 1]), 21);
assert_eq!(calculate(vec![13, 13, 13]), 30);
assert_eq!(calculate(vec![13, 13, 1, 1]), 22);
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let player_result = calculate(player.clone());
println!("Your cards: {:?}({})", player, player_result);
println!("CPU card: {}", cpu[0]);
// println!("cards: {:?}", cards);
// println!("player: {:?}", player);
// println!("cpu: {:?}", cpu);
}
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target\debug\blackjack.exe`
Your cards: [8, 9](17)
CPU card: 4
まず、Vectorを降順に並べます。このときsort_unstable_byメソッドを使っています。sortとsort_unstableという2つのメソッドが用意されています。sortは等しい値を並べ替えません。sort_unstableは等しい値を並べ替えますが、高速です。そして、降順にするために引数に|a, b| b.cmp(a)
としています。この式はクロージャといいます。|引数| 式
のような文法になるようです。今回の場合cardsから要素2つを引数としてそれぞれをb.cmp(a)で比較し並べ替えるかどうかの判断をしています。
それ以降のfor文やif文は、ブラックジャックのルールに従って、合計値を求めています。
標準入力でHit or Stand
use std::io;
use std::io::Write;
use std::str::FromStr;
fn input<T: FromStr>(s: &str) -> T {
loop {
print!("{}", s);
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).expect("Reading error!");
match line.trim().parse::<T>() {
Ok(n) => {
return n;
},
Err(_) => {}
}
};
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let player_result = calculate(player.clone());
println!("Your cards: {:?}({})", player, player_result);
println!("CPU card: {}", cpu[0]);
println!("input: {}", input::<u8>("Hit:1 or Stand:0=>"));
// println!("cards: {:?}", cards);
// println!("player: {:?}", player);
// println!("cpu: {:?}", cpu);
}
$ cargo run
Compiling blackjack v0.1.0 (C:\Users\deta\hack\rust\blackjack)
Finished dev [unoptimized + debuginfo] target(s) in 1.04s
Running `target\debug\blackjack.exe`
Your cards: [7, 2](9)
CPU card: 3
Hit:1 or Stand:0=>0
input: 0
loop{...}
は無限ループを実現します。while true {...}
を使うよりも良いそうです。
print!
は改行しない標準出力ですが、バッファに持ち続けるため、フラッシュしてあげる必要があるみたいです。なので、io::stdout().flush().unwrap();
として、フラッシュしてあげています。
io::stdin().read_line(&mut line).expect("Reading error!");
で一行ごとの標準入力を変数lineに格納します。match文では改行が入っているためtrimメソッドで取り除きさらにparseメソッドでT型に変換します。パースエラーの場合はループの先頭に戻り再び質問します。パースに成功すれば値を返します。
プレイヤーのターン
fn process_player<R: Rng>(cards: &mut Vec<u8>, player: &mut Vec<u8>, cpu: Vec<u8>, rng: &mut R) {
println!("[Player turn]");
println!("Your cards: {:?}({})", player, calculate(player.clone()));
println!("CPU card: [{}, ?]", cpu[0]);
loop {
let n = input::<u8>("\n[1]: Hit or [0]: Stand: ");
if n==1 {
println!("Hit!");
match hit(cards, rng) {
Some(card) => {
player.push(card);
},
_ => {
println!("Cards is empty!");
std::process::exit(1);
}
}
let result = calculate(player.clone());
if result>21 {
println!("You are busted!: {:?}({})", player, result);
break;
}
println!("Your cards: {:?}({})", player, result);
println!("CPU card: [{}, ?]", cpu[0]);
} else if n>1 {
continue;
} else {
println!("Stand!");
break;
}
}
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let mut player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
process_player(&mut cards, &mut player, cpu.clone(), &mut rng);
println!("cards: {:?}", cards);
println!("player: {:?}", player);
println!("cpu: {:?}", cpu);
}
$ cargo run
Compiling blackjack v0.1.0 (C:\Users\deta\hack\rust\blackjack)
Finished dev [unoptimized + debuginfo] target(s) in 0.93s
Running `target\debug\blackjack.exe`
[Player turn]
Your cards: [12, 4](14)
CPU card: [12, ?]
[1]: Hit or [0]: Stand: 1
Hit!
You are busted!: [12, 4, 9](23)
cards: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 13, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 13, 8, 8, 8, 9, 9, 9, 13, 10, 10, 10, 10, 11, 11, 11, 11, 13, 12, 12]
player: [12, 4, 9]
cpu: [12, 8]
プレイヤーがHitまたはStandを選び、手札を整えていく処理です。すこし長くなってしまいました。特に真新しいところはないです。
一つだけメモしときます。
process_player(&mut cards, &mut player, cpu.clone(), &mut rng);
の部分でcpu.clone()
をcpu
とした場合以下のようなエラーを吐きます。
$ cargo run
Compiling blackjack v0.1.0 (C:\Users\deta\hack\rust\blackjack)
error[E0382]: borrow of moved value: `cpu`
--> src\main.rs:146:30
|
142 | let cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
| --- move occurs because `cpu` has type `std::vec::Vec<u8>`, which does not implement the `Copy` trait
143 | process_player(&mut cards, &mut player, cpu, &mut rng);
| --- value moved here
...
146 | println!("cpu: {:?}", cpu);
| ^^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
error: Could not compile `blackjack`.
To learn more, run the command again with --verbose.
これは、「既に関数process_playerで変数cpuを渡したときに権限が移譲されてしまっているので、その後の、println!("cpu: {:?}", cpu);
では貸しだせないですよ。」と言っています。なので、変数cpuをcloneメソッドで複製して、clone版cpuを関数process_playerに渡しています。
CPUのターン
fn process_cpu<R: Rng>(cards: &mut Vec<u8>, cpu: &mut Vec<u8>, rng: &mut R) {
let mut result = calculate(cpu.clone());
println!("[CPU turn]");
println!("CPU cards: {:?}({})", cpu, result);
while result<17 {
match hit(cards, rng) {
Some(card) => {
println!("Hit!");
cpu.push(card);
result = calculate(cpu.clone());
},
_ => {
println!("Cards is empty!");
std::process::exit(1);
}
}
}
if result>21 {
println!("CPU are busted!");
} else {
println!("Stand!");
}
}
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let mut player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let mut cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
process_player(&mut cards, &mut player, cpu.clone(), &mut rng);
if calculate(player.clone())>21 {
println!("You lose...");
} else {
process_cpu(&mut cards, &mut cpu, &mut rng);
let player_result = calculate(player.clone());
let cpu_result = calculate(cpu.clone());
println!("Your cards: {:?}({})", player, player_result);
println!("CPU card: {:?}({})", cpu, cpu_result);
}
}
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target\debug\blackjack.exe`
[Player turn]
Your cards: [6, 2](8)
CPU card: [2, ?]
[1]: Hit or [0]: Stand: 1
Hit!
Your cards: [6, 2, 6](14)
CPU card: [2, ?]
[1]: Hit or [0]: Stand: 1
Hit!
Your cards: [6, 2, 6, 1](15)
CPU card: [2, ?]
[1]: Hit or [0]: Stand: 1
Hit!
Your cards: [6, 2, 6, 1, 4](19)
CPU card: [2, ?]
[1]: Hit or [0]: Stand: 0
Stand!
[CPU turn]
CPU cards: [2, 5](7)
Hit!
Hit!
Hit!
CPU are busted!
Your cards: [6, 2, 6, 1, 4](19)
CPU card: [2, 5, 3, 3, 11](23)
CPUが手札を整える処理です。CPUの手札が16以上になるまでHitを続けることにしました。
勝負
fn main() {
let mut rng = rand::thread_rng();
let mut cards: Vec<u8> = initialize_cards();
let mut player = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
let mut cpu = initialize_player(&mut cards, &mut rng).expect("Hitting error!");
process_player(&mut cards, &mut player, cpu.clone(), &mut rng);
if calculate(player.clone())>21 {
println!("You lose...");
} else {
process_cpu(&mut cards, &mut cpu, &mut rng);
let player_result = calculate(player.clone());
let cpu_result = calculate(cpu.clone());
println!("Your cards: {:?}({})", player, player_result);
println!("CPU card: {:?}({})", cpu, cpu_result);
if cpu_result>21 {
println!("Player win!!!");
} else if player_result>cpu_result {
println!("Player win!!!");
} else if player_result==cpu_result {
println!("Draw!");
} else {
println!("You lose...");
}
}
}
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `target\debug\blackjack.exe`
[Player turn]
Your cards: [4, 7](11)
CPU card: [12, ?]
[1]: Hit or [0]: Stand: 1
Hit!
Your cards: [4, 7, 1](12)
CPU card: [12, ?]
[1]: Hit or [0]: Stand: 1
Hit!
Your cards: [4, 7, 1, 9](21)
CPU card: [12, ?]
[1]: Hit or [0]: Stand: 0
Stand!
[CPU turn]
CPU cards: [12, 8](18)
Stand!
Your cards: [4, 7, 1, 9](21)
CPU card: [12, 8](18)
Player win!!!
結果を判定します。私が勝った結果を載せるために、4回も勝負させられました。笑
今回はここまで!
痛い目をみながらも、めちゃくちゃ楽しかったです。これで発散できました。