今日の内容
- クレートってなに
- ランダム
- 迷路を作ろう
- 数当てゲーム
はじめに
前回はRustでの「所有権」について学びましたね。今回は「クレート」について学んで、ちょっとしたゲームを作ろうと思います。
クレートとは
Rustの「クレート」は、関連した機能をまとめたコードのパッケージです。クレートを使用するには、Rustプロジェクトを作成し、Cargo.tomlファイルに使用するクレートの設定を追記します。このクレートは、だれでも作成して共有することができます。
crates.io はRust公式のクレートレジストリです。
今回はこちらから「rand」クレートを導入し、ランダム性をもったプログラムを作ります。
クレートには、「ライブラリクレート」と「バイナリクレート」がありますが、ここまで私たちが作成してきたものは「バイナリクレート」になり、これは実行可能ファイルになります。これに対して、今から使うrandクレートは「ライブラリクレート」にあたります。ライブラリクレート自体を実行することは出来ません。そのためエントリーポイントもありませんが、そのクレートの外部から関数などにアクセスすることができます。
つまり、「rand」という名前のライブラリクレートを用意して、中身に「ランダムな値を返す」関数を用意すれば、外部からその関数を呼び出すことができ、結果的にランダムな値を得ることができます。
randクレートの導入
今回しようするrandクレートには、基本的に以下のような機能があります。
- 様々な乱数を生成する
- 配列をシャッフルできる
- 乱数ジェネレーターの選択
- ランダムな要素の選択
- 再現性のある乱数
- 分布に基づく乱数
それでは、randクレートをプロジェクトに導入しましょう。外部クレートを導入するには、プロジェクトファイル内の「Cargo.toml」に依存関係を記入します。(またはCargoコマンドを利用してもできます。)
新規プロジェクト「try_rand」を作成します。以下のコマンドをコマンドプロンプトで実行してください。
cargo new try_rand
cd try_rand
エクスプローラーからもtry_randフォルダを開き、その中のCargo.tomlをVSCodeで開きます。[dependencies]を以下のように変更します。
[package]
name = "try_rand"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
先ほどのコマンドプロンプトから以下のコマンドを実行します。
cargo build
すると、以下のような出力が得られます。
Compiling byteorder v1.5.0
Compiling cfg-if v1.0.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 try_rand v0.1.0 (C:\Users\username\try_rand)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.48s
さて、randクレートをプロジェクト「try_rand」に導入できました!
まずは乱数を生成して、表示するプログラムを作成しましょう。ランダム性を確かめるため、for文を利用して10回表示させます。
use rand::prelude::*;
fn main() {
let mut rnd = thread_rng();
for _ in 0..10 {
let x:i32 = rnd.gen();
println!("{}", x);
}
}
/******** 実行結果 *******
-777436863
-869087421
1545601977
-651608241
1683010864
-776972198
-2105388010
1001321443
1150887708
-1458854957
************************/
rand内のpreludeから全てをインポートしていますね。これは最も一般的なものだけをインポートしています。
「thread_rng」は、ジェネレーターへのハンドルを提供します。Randクレートには「random」関数が用意されていますが、これは単純な乱数生成に限ります。これに対して「thread_rng」は単純な乱数生成以外にも多様な操作が可能です。さらに、「random」では内部的に「thread_rng」を利用しており、同じスレッド内で複数回乱数を生成する場合パフォーマンスがやや低下します。
少しコード量は増えますが、可読性のためにも、パフォーマンス向上のためにも、「thread_rng」を利用することを推奨します。
また、すべてのジェネレータはRng特性を備えていて、gen、gen_range、sampleメソッドを提供します。「gen」は標準な分布からの乱数の生成、「gen_range」は指定された範囲内でランダムな値を生成します。「sample」は複数の値をランダムに選び取れます。
実行結果を見てみると、それぞれの値が異なることから、ランダムな値を得られていることがわかります。
次に、配列をランダムに並び替えてみましょう。
use rand::prelude::*;
fn main() {
let mut rnd = thread_rng();
let mut a = [1,2,3,4,5,6,7,8,9,10];
for _ in 0..3 {
a.shuffle(&mut rnd);
println!("{:?}", a);
}
}
/******** 実行結果 *******
[7, 10, 2, 9, 1, 8, 5, 3, 6, 4]
[8, 3, 1, 4, 9, 10, 7, 2, 6, 5]
[10, 2, 7, 4, 9, 5, 3, 6, 8, 1]
************************/
配列をランダムに並び替えるには、「shuffle」を使います。実行結果を見ると、確かに配列がランダムに並び替えられています。
迷路を作ろう
さて、ランダムな値を得ることができるようになったので、迷路を作ってみようかと思います。
実は、迷路の生成にはいくつかのアルゴリズムがあります。
今回は、「棒倒し法」を用いて迷路を作成してみたいと思います。
棒倒し法
ここで、迷路は0と1のみの二次元配列で表され、壁を1、道を0とする
- 2次元配列を、幅高さ5以上の奇数で生成する
- 初めに、迷路の外周を壁(1)とし、それ以外を通路(0)とする
- 外周の内側に棒(壁)をx,yともに偶数の座標に配置する
- 上下左右ランダムな方向に棒を倒すように、壁をおく
ここで、上向きに棒を倒せるのは、1行目のみ
また、すでに棒が倒され、壁になっているところには棒を倒せない
これに従うだけで、迷路が作れてしまいます。
以下は、棒倒し法に則って迷路を作成するプログラムです。
use rand::prelude::*;
const FIELD_MAX: usize = 15;
fn main() {
let mut rnd = thread_rng();
let mut field: [[i32; FIELD_MAX]; FIELD_MAX] = [[0; FIELD_MAX]; FIELD_MAX];
for y in 0..FIELD_MAX {
for x in 0..FIELD_MAX {
//1. 迷路の外周を壁(1)とし、それ以外を通路(0)とする
if y == 0 || y == FIELD_MAX - 1 {
field[y][x] = 1;
} else if x == 0 || x == FIELD_MAX - 1 {
field[y][x] = 1;
}
//2.外周の内側に棒(壁)をx,yともに偶数の座標に配置する
if x % 2 == 0 && y % 2 == 0 {
field[y][x] = 1;
//上下左右ランダムな方向に棒を倒すように、壁をおく
// ここで、上向きに棒を倒せるのは、1行目のみ
// また、すでに棒が倒され、壁になっているところには棒を倒せない
if y != 0 && x != 0 && y != FIELD_MAX - 1 && x != FIELD_MAX - 1 {
loop {
let dir = if y == 2 {rnd.gen_range(0..4)} else {rnd.gen_range(0..3)};
match dir {
0 => {
if field[y][x - 1] == 1 {continue;}
field[y][x - 1] = 1;
break;
},
1 => {
if field[y - 1][x] == 1 {continue;}
field[y - 1][x] = 1;
break;
},
2 => {
if field[y][x + 1] == 1 {continue;}
field[y][x + 1] = 1;
break;
},
_ => {
if field[y + 1][x] == 1 {continue;}
field[y + 1][x] = 1;
break;
}
}
}
}
}
}
}
for y in 0..FIELD_MAX {
for x in 0..FIELD_MAX {
print!("{}", if field[y][x]==0 {" "} else {"XX"});
}
println!("");
}
}
/******** 実行結果 *******
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX XX XX XX XX XX
XX XX XX XXXXXXXXXX XX XX
XX XX XX XX XX XX
XX XXXXXXXXXX XX XX XX XX
XX XX XX XX XX
XXXXXX XXXXXX XXXXXX XX XX
XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXX XX
XX XX XX XX XX
XXXXXX XXXXXX XX XX XXXXXX
XX XX XX XX XX XX
XX XXXXXX XXXXXX XX XX XX
XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
************************/
これまでの知識を駆使して作成したので、解説はいらないと思います。しかし、一点だけ説明事項があります。
以前、「print!マクロは、すぐに表示されない」と説明しました。そのためにflushを使ってすぐに表示させるようにしていました。しかし、今回はすぐに表示させる必要はありません。print!マクロは、表示する一歩手前で止まり、println!などで改行が入るとそこまで溜まっていたものを全て表示させてから改行をします。そのため、今回はstd::io::*;
をインクルードしなくてよいのです。
数当てゲーム
コンピュータが生成したランダムな数字をプレイヤーが当てるゲームを作ります。
今回は、1~100の範囲内の整数を当てることにします。また、もし間違えたら、予想した数字が、正解より大きいか小さいかのみヒントとして出力します。
use rand::prelude::*;
use std::io::*;
fn main() {
let mut rnd = thread_rng();
let ans: u32 = rnd.gen_range(1..=100);
let mut count: u32 = 0;
loop{
let mut guess = String::new();
print!("input a number: ");
stdout().flush().unwrap();
stdin().read_line(&mut guess).expect("Failed to read line!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue
};
count += 1;
if guess < ans {
println!("Too small!");
} else if guess > ans {
println!("Too big!");
} else {
println!("You win!");
println!("ANS: {}, COUNT: {}", ans, count);
break;
}
}
}
/******** 実行結果 *******
input a number: 50
Too big!
input a number: 30
Too big!
input a number: 10
Too small!
input a number: 20
Too small!
input a number: 25
Too small!
input a number: 27
Too small!
input a number: 28
Too small!
input a number: 29
You win!
ANS: 29, COUNT: 8
************************/
入力が入っただけで、さっきよりもむしろ簡単かもしれません。
おわりに
今回はクレートについて学びました。
ランダムな値を扱えるだけで、様々なゲームが作れるようになってしまいました!
次回はu128より大きい整数を扱ってみます。(そろそろネタ切れ..誰かこのあとの記事の案を考えてください(´;ω;`))
ご精読ありがとうございました。