コンテンツにスキップ

Rustで作る数当てゲーム

いよいよ最初の本格的なプロジェクトです。コンピュータが1から100までの中から秘密の数を1つ選びます。あなたはその数を予想します。当たるまで「小さすぎます」「大きすぎます」とヒントが出て、当たったら何回で当てられたかを教えてくれます。

小さなプログラムですが、ここまでに学んだことをたくさん使います。変数、match、ループ、そして Result です。一歩ずつ作っていきましょう。どのステップも、それ単体で動くようになっています。

完成すると、自分のパソコンで遊ぶゲームはこんな感じになります。

数を当ててください!(1〜100)
予想を入力してください:
50
大きすぎます!
予想を入力してください:
こんにちは
それは整数ではありません。もう一度どうぞ!
予想を入力してください:
25
小さすぎます!
予想を入力してください:
37
正解!3回で当たりました!

ここに、やさしい工夫が2つあります。プレイヤーが こんにちは と入力しても、ゲームは落ちずにもう一度聞き直します。そして、その間違った入力は1回分の予想として数えません。

ステップ1:プロジェクトを作る

Section titled “ステップ1:プロジェクトを作る”

ターミナルを開いて、Cargoで新しいプロジェクトを作ります。

Terminal window
cargo new guessing_game
cd guessing_game

このページのコードは、すべて src/main.rs に書きます。いつでも次のコマンドで実行できます。

Terminal window
cargo run

まだRustをインストールしていない場合は、先にRustを始めるを見てください。

ステップ2:キーボードから予想を読み込む

Section titled “ステップ2:キーボードから予想を読み込む”

プレイヤーが入力した内容を読むには、std::io::stdin() とその read_line メソッドを使います。

use std::io;
fn main() {
println!("予想を入力してください:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("行の読み込みに失敗しました");
println!("あなたの予想: {guess}");
}

このプログラムは、あなたが何かを入力してEnterキーを押すまで待ちます。なので、自分のパソコンで cargo run を使って実行してください。(Rust Playgroundではキーボード入力ができないため、ここには「試してみる」ボタンがありません。)

ここでは、いくつかのことが起きています。

  • String::new() は、テキストを入れるための空の、伸び縮みできる String を作ります。
  • read_line(&mut guess) は、入力された行を guess の末尾に追加します。文字列を書き換えるので &mut が必要です。&mut が初めての人は借用のページを見てください。
  • 入力の読み込みは失敗することがあるので、read_line は Result を返します。.expect(...) は「もし失敗したら、このメッセージを出してプログラムを止める」という意味です。今はこれで大丈夫です。あとで、もっとやさしく扱う方法に変えます。

1つ意外な点があります。読み込んだテキストには、Enterキーの改行も含まれます。42 と入力すると、guess の中身は "42\n" になります。次のステップのために覚えておいてください。

ステップ3:テキストを数値に変換する

Section titled “ステップ3:テキストを数値に変換する”

プレイヤーが入力したのはテキストですが、比べるためには数値が必要です。.trim() で両端の空白と改行を取り除き、.parse() でテキストを数値に変換してみます。

変換は失敗することがあります。"なな" は数値ではありませんよね。そのため parse() は Result を返します。match を使って、成功と失敗の両方に対応しましょう。

fn main() {
// プレイヤーがこれらの行を入力したことにします(それぞれEnterで終わります)。
let inputs = ["42\n", " 7 \n", "なな\n", "-3\n"];
for input in inputs {
match input.trim().parse::<u32>() {
Ok(num) => println!("{:?} -> 数値 {num}", input),
Err(e) => println!("{:?} -> 有効な予想ではありません({e})", input),
}
}
}

実行結果:

"42\n" -> 数値 42
" 7 \n" -> 数値 7
"なな\n" -> 有効な予想ではありません(invalid digit found in string)
"-3\n" -> 有効な予想ではありません(invalid digit found in string)
▶ 実際に試してみよう

parse::<u32>() は、どの型に変換するかをRustに伝えています。u32 は符号なし(決してマイナスにならない)の整数です。だから -3 は受け付けられません。これはちょうど望んでいる動きです。秘密の数はいつも1から100の間だからです。

かっこの中のメッセージ(invalid digit found in string、「文字列の中に数字として無効な文字がある」という意味)は、Rustの標準ライブラリが出しているものなので英語のままです。

実際のゲームでは、同じ考え方をこう書きます。

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("それは整数ではありません。もう一度どうぞ!");
continue;
}
};

ここでは let guess: u32 から型がわかるので、::<u32> は要りません。変換がうまくいけば、match は数値を返します。失敗したら、やさしいメッセージを表示して continue でもう一度聞き直します。(continue はループの中でしか使えません。ループはステップ5で追加します。)

なぜ .expect() を使わないのでしょうか。それだと、入力ミスひとつでゲーム全体が落ちてしまうからです。Err の場合をきちんと扱うことが、使っていて気持ちのいいプログラムを作るコツです。

ステップ4:予想と秘密の数を比べる

Section titled “ステップ4:予想と秘密の数を比べる”

数値にはすべて .cmp() メソッドがあります。2つの値を比べて、std::cmp::Ordering を返します。これは Less(小さい)、Greater(大きい)、Equal(等しい)の3つのバリアントを持つ列挙型(enum)です。これに match を使うと、ほとんど文章のように読めます。

use std::cmp::Ordering;
fn main() {
let secret: u32 = 42;
for guess in [10, 42, 99] {
match guess.cmp(&secret) {
Ordering::Less => println!("{guess}: 小さすぎます!"),
Ordering::Greater => println!("{guess}: 大きすぎます!"),
Ordering::Equal => println!("{guess}: 当たり!"),
}
}
}

実行結果:

10: 小さすぎます!
42: 当たり!
99: 大きすぎます!
▶ 実際に試してみよう

同じことは if guess < secret ... else if ... でも書けます。match 版のよいところは、3つすべての場合を扱ったかどうかをコンパイラがチェックしてくれる点です。1つでも忘れると、コンパイルが通りません。

ステップ5:当たるまで聞き続ける

Section titled “ステップ5:当たるまで聞き続ける”

ここで全体を loop で包みます。loop は、break で抜けるまでずっと繰り返します。あわせて attempts というカウンターを用意し、予想がちゃんとした数値だったときだけ1を足します。

本物のゲームでは、入力はキーボードから来ます。Playgroundで動く版を作るために、秘密の数を固定し、「入力された」行も固定のリストにします。inputs.next() は次の行を返し、リストが終わると None を返します。

use std::cmp::Ordering;
fn main() {
let secret: u32 = 37;
// プレイヤーがこれらを1行ずつ入力したことにします。
let mut inputs = ["50", "こんにちは", "25", "37"].into_iter();
let mut attempts = 0;
println!("数を当ててください!(1〜100)");
loop {
let input = match inputs.next() {
Some(line) => line,
None => {
println!("もう予想がありません。正解は{secret}でした。");
break;
}
};
println!("> {input}");
let guess: u32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("それは整数ではありません。もう一度どうぞ!");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("小さすぎます!"),
Ordering::Greater => println!("大きすぎます!"),
Ordering::Equal => {
println!("正解!{attempts}回で当たりました!");
break;
}
}
}
}

実行結果:

数を当ててください!(1〜100)
> 50
大きすぎます!
> こんにちは
それは整数ではありません。もう一度どうぞ!
> 25
小さすぎます!
> 37
正解!3回で当たりました!
▶ 実際に試してみよう

秘密の数や予想のリストを変えて、もう一度実行してみてください。正解を最初に置いたり、リストから外したりすると、どうなるでしょうか。

None の場合はとても大事です。これはPlaygroundにおける「プレイヤーの入力がもうない」状態で、本物のゲームにも同じ考え方が必要になります(ステップ7を参照)。

ステップ6:秘密の数をランダムに選ぶ

Section titled “ステップ6:秘密の数をランダムに選ぶ”

毎回同じ数が秘密の数では、あまり楽しくありませんよね。実際のプロジェクトでは、rand(英語)クレートを使います。Rustで乱数を扱うときの定番です。使い方は乱数のレッスンで紹介しています。次のコマンドで追加できます。

Terminal window
cargo add rand

このプロジェクトでは標準ライブラリだけを使うので、準備は何も要りません。ちょっとした工夫をします。時計から現在の時刻を取り出し、そのナノ秒の部分(0から999,999,999までの、どんどん変わる数)を使います。それを %(割り算の余り)で1から100の範囲に収めます。

use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("時計が1970年より前になっています")
.subsec_nanos();
nanos % 100 + 1
}
fn main() {
let secret = secret_number();
let in_range = (1..=100).contains(&secret);
println!("秘密の数を選びました。1〜100の範囲内ですか? {in_range}");
}

実行結果:

秘密の数を選びました。1〜100の範囲内ですか? true
▶ 実際に試してみよう

nanos % 100 は必ず0〜99になるので、1を足すと1〜100になります。ここでは範囲内かどうかだけを表示しているので、実行結果は毎回同じです。中身をのぞきたければ、secret そのものを表示してみてください。

これが何なのかは、正直に押さえておきましょう。これは「ゲームには十分なくらい予測しにくい」というだけで、本物の乱数ではありません。パスワードやセキュリティ用のトークンなど、誰かが値を推測しようとするかもしれないものには、絶対にこの方法を使わないでください。そういう用途には、ちゃんとしたクレートを使いましょう。

ここまでの内容をすべてまとめました。これを src/main.rs に書いて、cargo run で遊んでください。

1つだけ新しい点があります。.expect() を呼ぶ代わりに、read_line の結果に match を使っています。read_line は、入力がもうないときに Ok(0) を返します。たとえば、プレイヤーがCtrl+D(WindowsではCtrl+Zを押してからEnter)を押したときや、ファイルから入力をパイプで流し込んだときです。このチェックがないと、空の入力に対してループが永遠に回り続けてしまいます。

use std::cmp::Ordering;
use std::io;
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("時計が1970年より前になっています")
.subsec_nanos();
nanos % 100 + 1
}
fn main() {
println!("数を当ててください!(1〜100)");
let secret = secret_number();
let mut attempts = 0;
loop {
println!("予想を入力してください:");
let mut guess = String::new();
match io::stdin().read_line(&mut guess) {
Ok(0) => {
// 入力の終わり:もう読むものがありません。
println!("入力がありません。正解は{secret}でした。さようなら!");
break;
}
Ok(_) => {}
Err(e) => {
println!("入力を読み込めませんでした: {e}");
break;
}
}
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("それは整数ではありません。もう一度どうぞ!");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("小さすぎます!"),
Ordering::Greater => println!("大きすぎます!"),
Ordering::Equal => {
println!("正解!{attempts}回で当たりました!");
break;
}
}
}
}

ブラウザで試したい場合は、固定の予想リストで遊ぶステップ5の版を使ってください。🦀

文字列に mut を付け忘れる。 read_line は文字列を書き換える必要があるので、可変(mutable)にしておかなければなりません。

use std::io;
fn main() {
let guess = String::new();
io::stdin().read_line(&mut guess).expect("行の読み込みに失敗しました");
}
error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable
--> src/main.rs:5:27
|
5 | io::stdin().read_line(&mut guess).expect("行の読み込みに失敗しました");
| ^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
4 | let mut guess = String::new();
| +++

コンパイラは「guess は可変として宣言されていないので、可変として借用できません」と言っています。直し方は、コンパイラの提案どおり let mut guess = String::new(); です。

テキストと数値を比べてしまう。 変換を忘れると、guess は String のままです。String と u32 は比べられません。

use std::cmp::Ordering;
fn main() {
let secret: u32 = 42;
let guess = String::from("50");
match guess.cmp(&secret) {
Ordering::Less => println!("小さすぎます!"),
Ordering::Greater => println!("大きすぎます!"),
Ordering::Equal => println!("当たり!"),
}
}
error[E0308]: mismatched types
--> src/main.rs:7:21
|
7 | match guess.cmp(&secret) {
| --- ^^^^^^^ expected `&String`, found `&u32`
| |
| arguments to this method are incorrect

「mismatched types」は「型が一致しない」という意味です。Rustは、テキストと数値の間で勝手に変換してはくれません。まず変換して、それから比べましょう。

.trim() を忘れる。 これはコンパイルは通りますが、改行は数字ではないので、どの予想も受け付けられなくなります。

fn main() {
let typed = "42\n";
println!("{:?}", typed.parse::<u32>());
println!("{:?}", typed.trim().parse::<u32>());
}

実行結果:

Err(ParseIntError { kind: InvalidDigit })
Ok(42)
▶ 実際に試してみよう

ゲームが動いたら、次のアイデアに自分で挑戦してみてください。どれも小さな変更です。

  • 範囲をチェックする。 予想が0や100より大きい数なら「1〜100の数を選んでください」と表示し、回数には数えないようにします。
  • 回数を制限する。 プレイヤーに7回のチャンスを与えます。使い切ったら秘密の数を見せて、ゲームを終わりにします。
  • もう一度遊ぶ。 プレイヤーが当てたら「もう一度遊びますか?(y/n)」と聞き、新しい秘密の数で次のラウンドを始めます。
  • ベストスコアを記録する。 ラウンドをまたいで最も少ない回数を覚えておき、最後に表示します。
  • もっと親切なヒントを出す。 予想と秘密の数の差が3以内なら「おしい!」と表示します。guess.abs_diff(secret) で2つの数の差がわかります。
  • 本物の rand クレートを使う。 cargo add rand を実行し、docs.rs(英語)のドキュメントに従って secret_number() を置き換えます。
  • ロジックを関数に分ける。 fn check_guess(guess: u32, secret: u32) -> Ordering を書き、そのテストを追加します。
  • io::stdin().read_line(&mut s) は1行を改行ごと読み込み、Result を返します。Ok(0) は入力がもうないことを意味します。
  • .trim().parse() はテキストを数値に変換します。戻り値は Result なので、match を使って間違った入力をやさしく扱いましょう。
  • a.cmp(&b) は Ordering::Less、Greater、Equal のどれかを返します。match を使えば、3つすべてを扱ったかどうかを確かめられます。
  • continue と break を組み合わせた loop で、プレイヤーが当てるまでゲームを続けられます。
  • 時計を使う工夫はゲームには十分ですが、実際のプロジェクトでは rand クレートを使います。

理解度チェック

1. プレイヤーが 42 と入力してEnterを押しました。read_line で読み込んだ文字列の中身はどれですか?

2. このゲームで parse() に .expect() ではなく match を使うのはなぜですか?

3. read_line が Ok(0) を返したとき、それは何を意味しますか?

4. guess.cmp(&secret) が返す可能性のある値はどれですか?

スコア: 0 / 4