目次


Rust 入門ガイド

Rust 言語でのコーディングを開始する

Rust のスキルを実践するために、単純な三目並べゲームを作成する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Rust 入門ガイド

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Rust 入門ガイド

このシリーズの続きに乞うご期待。

このシリーズの第 1 回で公言したように、私は本当に Rust が大好きです。この静的なコンパイル言語では、メモリー安全性が確保されます。また、オペレーティング・システムに依存しないため、どのコンピューター上でも Rust を実行できます。Rust を使用すれば、C# や Java などの言語のように煩わしいガーベッジ・コレクションを行うことなく、システム言語の処理速度と低レベルでのメリットをすべて手に入れることができます。

言語を習得するには、実際に使ってみることが最も効果的な方法です。この記事では、Rust を使って単純な三目並べゲームを作成する手順を通して、この言語を習得するお手伝いをします。説明する手順に従って、独自のゲームを作成してください。

前提条件

まず、シリーズ「Rust 入門ガイド」の第 1 回を読んでください。第 1 回で、Rust をインストールして実行する方法、Rust のコア機能、そして Rust を使い始めるために必要な概念について説明しています。今回の記事では Rust のあらゆる面を説明することはしないので、手順を開始する前に、この言語の基礎を把握しておく必要があります。

プロジェクトを開始する

最初に必要な作業は、プロジェクトをセットアップすることです。Cargo を使用すると、端末から新しい実行可能バイナリー・プログラムを作成できます。

$ cd ~/Documents
$ cargo new tic_tac_toe –bin

tree プログラム内に新しく作成された tic_tac_toe ディレクトリーは、以下のようになっています。

$ cd tic_tac_toe
$ tree .
.
??? Cargo.toml
??? src
    ??? main.rs

main.rs ファイルは以下の行で構成されているはずです。

fn main() {
    println!("Hello, world!");
}

リスト 1 に示すように、プログラムを実行するのは、プログラムを作成するのと同じく簡単です。

リスト 1. 「Hello, World!」を実行する
$ cargo build
    Compiling …
     Finished …
$ cargo run
     Finished …
      Running …
Hello, world!

ゲーム・モジュールのファイルも必要なので、以下のコマンド・ラインを実行して、そのためのファイルを作成します。

$ touch ./src/game.rs

これで、プロジェクトとディレクトリーがセットアップされました。次は、ゲームの枠組みに取り掛かります。

ゲームで使用する型と構造体の計画を練る

伝統的な三目並べゲームは、ゲームのボードとプレイヤーの順番という、2 つの主要な要素で構成されます。ボードは基本的に、空の 3x3 の配列です。順番は、どちらのプレイヤーがアクションを取る番であるのかを意味します。この 2 つの要素をコードに変換するには、前のセクションで作成した game.rs ファイルを編集する必要があります (リスト 2 を参照)。

リスト 2. ボードとプレイヤーの順番を反映して変更された game.rs
type Board = Vec<Vec<String>>;

enum Turn {
    Player,
    Bot,
}

pub struct Game {
    board: Board,
    current_turn: Turn,
}

奇妙な構文が使われていることにお気付きかもしれませんが、気にしないでください。手順を進めていく中で説明します。

ボード

ゲームのボードをコードに変換するには、type キーワードを使用して、Board という別名を設定し、これを型 Vec<Vec<String>> の同義語にします。これで、Board は文字列からなる 2 次元のベクトルを表す単純な型になりました。ここで文字列を使用している理由は、配列に含まれる値は xo、または空いているマスを示す番号のいずれかに限られるためです。

順番

順番は、アクションを取るプレイヤーがどちらであるかを示すだけなので、enum 構造体で完全に対応できます。順番を交代するたびに、Turn バリアントを照合して該当するメソッドを呼び出せばよいだけです。

ゲーム

最後に、ボードと現在のプレイ順を保持する Game オブジェクトを作成する必要があります。ですが、ちょっと待ってください!Game 構造体を処理するメソッドは一体どこにあるのでしょうか?ご心配なく。次のステップで説明します。

ゲームを実装する

三目並べゲームには、どのようなメソッドが必要になるでしょうか?まず、先行か後攻かの順番があります。順番を交代する時点でボードを表示し、そのプレイヤーがアクションを取ったらボードを再度表示して勝利条件をチェックします。ゲームに勝った場合、どちらのプレイヤーが勝ったのかを発表し、もう一度プレイするかどうか尋ねます。どちらのプレイヤーもゲームに勝っていない場合は、現在のプレイヤーを切り替えて、ゲームを進めます。プレイヤーによる各アクション内で細かな問題に対処しなければならないことは確かですが、以上の流れを出発点として作業を進めることができます。

まず、impl ブロック内にネストする構造を作成します (リスト 3 を参照)。

リスト 3. ゲームの構造
impl Game {
    pub fn new() -> Game {
        let first_row = vec![
            String::from("1"), String::from("2"), 
            String::from("3")];

        let second_row = vec![
            String::from("4"), String::from("5"), 
            String::from("6")];

        let third_row = vec![
            String::from("7"), String::from("8"), 
            String::from("9")];

        Game {
            board: vec![first_row, second_row, third_row],
            current_turn: Turn::Player,
        }
    }
}

静的メソッド new では、Game 構造体を作成して返します。Rust では、オブジェクト・コンストラクターの標準的な名前は new となっています。

board メンバー変数を String オブジェクトからなる 2 次元ベクトルにバインドする必要があります。ボード上の各マスは空白にするのではなく数字を 1 つ入れることにより、アクションの際に選択可能な位置を示すようにしてあります。次に、current_turn メンバー変数を Turn::Player の値にバインドします。この行は、どのゲームもプレイヤーのアクションで始まることを意味します。

このゲームの遊び方

このプログラムの案内図としての役割を果たす、最初のメソッドを、impl Grid ブロック内に追加します (このセクションで取り上げる残りのメソッドも、このブロック内に追加します)。リスト 4 に、この最初のメソッドを記載します。

リスト 4. ゲーム・プログラムの案内図
pub fn play_game(&mut self) {
    let mut finished = false;

    while !finished {
        self.play_turn();

        if self.game_is_won() {
            self.print_board();

            match self.current_turn {
                Turn::Player => println!("You won!"),
                Turn::Bot => println!("You lost!"),
            };

            finished = Self::player_is_finished();

            self.reset();
        }

        self.current_turn = self.get_next_turn();
    }
}

上記のコードから、ゲームのフローは簡単に理解できます。つまり、無限ループを使用して、先行後攻の順番を交代して current_turn の値を入れ替えていくというフローです。このフローでは、順番が交代するたびにゲームの内部状態が変わるため、self に対する可変の借用を使用します。

ここですでに、enum 構造体が効果を発揮しています。ゲームに勝った場合、勝者に関する情報がこの構造体に格納されるためです。この情報を使用して、プレイヤーに勝ったか負けたかを知らせます。さらに、ボードを元の状態にリセットします。こうすれば、ユーザーがもう一度プレイしたい場合に役立ちます。

new を除いては、パブリック・メソッドはこのメソッドだけであることに注意してください。つまり、Game オブジェクトを使用するときに別のライブラリーがアクセスできるのは、play_gamenew だけということになります。他のすべてのメソッドは、静的であるかどうかにかかわらずプライベート・メソッドです。

順番交代

play_game メソッド内で使用する最初のヘルパー関数は play_turn です。リスト 5 に、この小さい効果的な関数を記載します。

リスト 5.play_turn 関数
fn play_turn(&mut self) {
    self.print_board();

    let (token, valid_move) = match self.current_turn {
        Turn::Player => (
            String::from("X"), self.get_player_move()),
        Turn::Bot => (
            String::from("O"), self.get_bot_move()),
    };

    let (row, col) = Self::to_board_location(valid_move);

    self.board[row][col] = token;
}

この関数は巧妙にできています。まず、ボードを出力して、どのマスが空いているかをユーザーがわかるようにします (ボットがプレイする順番のときでも、こうしてあると役立ちます)。次に、タプル構造体の分解と match を使用して、token 変数と valid_move 変数に、current_turn のバリアントに応じた値を代入します。

token は、プレイヤーであるかボットであるかによって、それぞれ String 型の X または O が代入されます。ボード上のまだ使われていないマスを表す valid_move には整数 1 から 9 が代入されています。この変数をボードの該当する行と列に変換するために使用するのが、to_board_location 静的メソッドです (大文字「S」で始まる Self は、self の型 (この例では Game) を返します)。

ボードの状態を見てみましょう

play_turn をセットアップしたので、次は出力するためのメソッドが必要です。リスト 6 に、そのためのメソッドを記載します。

リスト 6. ゲームのボードを出力する
fn print_board(&self) {
    let separator = "+---+---+---+";

    println!("\n{}", separator);

    for row in &self.board {
        println!("| {} |\n{}", row.join(" | "), separator);
    }

    print!("\n");
}

上記のメソッド内では for ループを使用して、ボード上の行を表す ASCII 表現を出力します。一時変数 row は、ボード内の各ベクトルへの参照です。join メソッドを使えば、rowString 型に変換して、その新しい値の末尾にセパレーター String を追加して出力できます。

ボードを出力できるようになったので、ここからはいよいよ、プレイヤーとボットの有効なアクションを追加する作業に取り掛かります。

プレイヤー、次はあなたの番です

ここまでのところ、このプログラムは一連の値を返すコードでハードコーディングされていて、プレイヤーによる入力はありません。この状況を一転させるのが、リスト 7 です。

リスト 7. 順番交代をセットアップする
fn get_player_move(&self) -> u32 {
    loop {
        let mut player_input = String::new();

        println!(
            "\nPlease enter your move (an integer between \
            1 and 9): ");

        match io::stdin().read_line(&mut player_input) {
            Err(_) => println!(
                "Error reading input, try again!"),
            Ok(_) => match self.validate(&player_input) {
                Err(err) => println!("{}", err),
                Ok(num) => return num,
            },
        }
    }
}

このメソッドの要点は、プレイヤーがゲーム上有効なアクションを取るまで、無限にループすることです。

ユーザー・プロンプトに続く最初のユーザーの match 式で、String 型の入力 (player_input) の読み取りを試み、入力時にエラーが発生していないかどうかをチェックします。この機能は、io モジュール (game.rs ファイルの先頭でインポートする必要があります) の stdin().read_line メソッドによって提供します (stdin() は、ハンドル・オブジェクトを現在の標準入力に返します)。私は io モジュールを以下のようにインポートしています。

use std::io;

同じく注目すべき点は、read_line メソッドは特定の String を変化させながら、Result という名前の enum 型も返すことです。入門記事では Result について説明しなかったので、ここで触れておきます。

Result という enum 型

Result は、代数的データ型として知られるものです。この enum 型には、OkErr の 2 つのバリアントがあります。各バリアントは、String 型または i32 数値型などのデータを保持することができます。

read_line の場合、返される Resultio モジュールからの特殊なバージョンであるため、Err は特殊な io::Error バリアントです。それとは対照的に、Ok は元の Result バリアントと同じであり、この場合は読み取ったバイト数を表す整数を保持します。Result は、考えられるすべてのエラーを実行時ではなくコンパイル時に処理できるようにする、有用な enum 型です。

Rust で広く使用されている別の同種の enum 型としては、Option があります。Option のバリアントは OkErr ではなく、None (データは保持されません) と Some (データを保持します) です。Option は、C++nullptr、Python の None と同じ用途で役立ちます。

OptionResult の違いは何でしょう?また、どのような場合に使用すべきなのでしょうか?私のお決まりの回答としては、まず、何も返さないはずの関数には Option を使用します。常に成功すると見込まれるものの、失敗することもある関数には Result を使用します。つまり、エラーをキャッチしなければならない場合は、Result を使用すべきです。理解できましたか?それでは、get_player_move メソッドの話題に戻りましょう。

ゲームの再開

プレイヤーからの入力を読み取るところで説明を中断しました。ユーザーの入力を読み取り中にエラーが発生した場合、プログラムはユーザーにそれを通知し、別の入力を求めます。エラーが発生しなければ、プログラムは 2 番目の match 式に到達します。下線 (_) を使用していることに注目してください。下線は Rust に対し、ResultOk または Err バリアント内のデータがバインドされていないことを通知します (データのバインドは、2 番目の match 式の中で行います)。

この match 式では、player_input 変数が有効であるかどうかをチェックします。有効でなければ、コードはエラーを返し (ゲームがプレイヤーにアラートを発します)、プレイヤーに有効な入力を求めます。player_input が有効であれば、validate メソッドを使用して入力が整数に変換されて、その結果が返されます。

コードを検証する

ゲームのコードを完成した後は、validate 関数を作成するのが賢明です。リスト 8 にコードを記載します。

リスト 8. validate 関数
fn validate(&self, input: &str) -> Result<u32, String> {
    match input.trim().parse::<u32>() {
        Err(_) => Err(
            String::from(
                "Please input a valid unsigned integer!")),
        Ok(number) => {
            if self.is_valid_move(number) {
                Ok(number)
            } else {
                Err(
                    String::from(
                        "Please input a number, between \
                        1 and 9, not already chosen!"))
            }
        }
    }
}

出力を 1 行ずつ辿りながら、メソッドの要点を説明します。

まず、プログラムから Result enum 型が返されています。まだ型のテンプレートについては説明していませんが、基本的にここで宣言しているのは、ResultOk バリアントは u32 整数型を保持し、Err バリリアントは String 型を保持するということです。どうしてここで Result を返すのでしょうか?それは、このメソッドでは検証が合格すると想定されていて、入力が以下の場合にだけエラーをスローするためです。

  • 整数ではない
  • 該当するマスは埋まっているため、有効ではない
  • 整数が 1 から 9 ではないため、有効なマスではない

次に、プログラムは入力に対して parse メソッドを使用して、入力を u32 型に変換しようとします。turbofish としての ::<型> は、戻り値の型を自身で指定する関数の特殊な側面です。この例の場合は parse に対し、入力を u32 型に変換するように指示すると同時に、ResultOk バリアントは u32 型を保持するように設定しています。入力を変換できない場合、このコードは入力が符号なし整数ではないことを通知するエラーを返します。一方、入力が正常に変換された場合、コードはその入力を別のヘルパー関数 is_valid_move に渡します。

検証に別のヘルパー関数が必要なのは、どうしてでしょうか?上記の考えられるエラーのリストを踏まえると、数値 1 は特定のユーザーに固有の値です。ボットは常に整数を入力するので、validate を使用してプレイヤーの応答だけを検証し、is_valid_move を使用して残りの 2 つの考えられるエラーをチェックするというわけです。

リスト 9 に、検証コードの残りの部分を記載します。

リスト 9. 追加の検証
fn is_valid_move(&self, unchecked_move: u32) -> bool {
    match unchecked_move {
        1...9 => {
            let (row, col) = Self::to_board_location(
                unchecked_move);

            match self.board[row][col].as_str() {
                "X" | "O" => false,
                 _ => true,
            }
        }
        _ => false,
    }
}

上記の内容は簡単に理解できるはずです。指定された unchecked_move が 1 から 9 までの数値でなければ、それは無効なアクションです。この範囲の数値であれば、コードで、そのアクションがすでに取られているかどうかをチェックしなければなりません。そこで、play_turn で行ったように、unchecked_move をボード上の該当する行と列に変換します。変換後は、そのマスがボード上にあるかどうかをチェックできます。マスが X または O となっている場合、それは無効なアクションということになります。

ボットに取り掛かる

ボットにアクションを取らせるメソッドを作成する前に、リスト 10 に記載する to_board_location 静的メソッドを作成してください。

リスト 10. to_board_location メソッド
fn to_board_location(game_move: u32) -> (usize, usize) {
    let row = (game_move - 1) / 3;
    let col = (game_move - 1) % 3;

    (row as usize, col as usize)
}

このメソッドは、少々ズルをして作成されています。それは、validate 内と play_turn 内で to_board_location が呼び出されるときの引数は 1 から 9 までの整数であることがわかっているからです。また、計算は Game オブジェクトと何の関係もないため、このメソッドを静的として設定しています。三目並べのボードは常に 3x3 のマス目です。

おしゃべりなボット

プレイヤーのアクションを取得できるコードにはなっていますが、ボットについて考えてください。第一に、ボットのアクションは乱数となっていなければなりません。つまり、サード・パーティー製クレイトの rand をインポートする必要があるということです。第二に、このランダムなアクションは、is_valid_move メソッドを使用して有効なマスであることが検証されるまで生成し続けます。有効なマスであることが検証されたら、ボットが取ったアクションをゲームからプレイヤーに通知して、そのアクションを返す必要があります。

rand クレイトを Cargo.toml という名前のファイルにインポートして、rand を依存関係としてインストールします。リスト 11 にこのファイルを記載します。

リスト 11. Cargo.toml
[package]
name = "tic_tac_toe"
version = "0.1.0"
authors = ["Dylan Hicks <dirtgrub.dylanhicks@gmail.com>"]

[dependencies]
rand = "0.4"

main.js ファイルで Cargo に対し、この依存関係を使用することを指定します。それには、ファイルの先頭に以下のコマンドを追加します。

extern crate rand;

game.rs ファイルの先頭の io インポートの上に以下のコマンドを追加します。

use rand;

rand クレイトを使用して乱数を生成するには、ボットからアクションを取得するメソッドが必要です。リスト 12 に、そのためのメソッドを記載します。

リスト 12. bot_move メソッド
fn get_bot_move(&self) -> u32 {
    let mut bot_move: u32 = rand::random::<u32>() % 9 + 1;

    while !self.is_valid_move(bot_move) {
        bot_move = rand::random::<u32>() % 9 + 1;
    }

    println!("Bot played moved at: {}", bot_move);

    bot_move
}

骨の折れる作業ではありませんでしたよね?

上記のメソッドでは、play_turn メソッドの依存関係を終わりにします。次は、ゲームに勝ったかどうかをチェックするメソッドを作成する必要があります。

私たちが勝者です

ここで、拙速にプレイをして、ゲームの負けをブール代数で返します (リスト 13)。

リスト 13. ブール代数の小さなコード
fn game_is_won(&self) -> bool {
    let mut all_same_row = false;
    let mut all_same_col = false;

    for index in 0..3 {
        all_same_row |= 
            self.board[index][0] == self.board[index][1]
            && self.board[index][1] == self.board[index][2];
        all_same_col |= 
            self.board[0][index] == self.board[1][index]
            && self.board[1][index] == self.board[2][index];
    }

    let all_same_diag_1 =
        self.board[0][0] == self.board[1][1] 
        && self.board[1][1] == self.board[2][2];
    let all_same_diag_2 =
        self.board[0][2] == self.board[1][1] 
        && self.board[1][1] == self.board[2][0];

        (all_same_row || all_same_col || all_same_diag_1 || 
         all_same_diag_2)
}

for ループの処理中に、行と列を同時にチェックして、三目並べの勝利条件 (つまり、3 つの X または O が 1 行に並んでいること) が満たされているかどうかを調べます。それには |= を使用します。この演算子は += に似ていますが、加算演算子ではなく or 演算子を使用します。続いて、2 つの斜め方向がすべて同じ文字になっているかどうかをチェックします。最後に、ブール代数を使用して、勝利条件のいずれかが満たされているかどうかを返します。あと 3 つのメソッドを追加すれば、完了です。

もう一度プレイしますか?

リスト 4play_game メソッドをもう一度見ると、finishedtrue になるまでコードがループし続けることがわかります。この条件が満たされるのは、method player_is_finishedtrue の場合のみです。このメソッドは、プレイヤーの応答 (yes または no) に基づいて処理する必要があります (リスト 14)。

リスト 14. player_is_finished メソッド
fn player_is_finished() -> bool {
    let mut player_input = String::new();

    println!("Are you finished playing (y/n)?:");

    match io::stdin().read_line(&mut player_input) {
        Ok(_) => {
            let temp = player_input.to_lowercase();

            temp.trim() == "y" || temp.trim() == "yes"
        }
            Err(_) => false,
    }
}

最初にこのメソッドを作成したとき、プレイヤーの入力が「yes」の場合だけを処理するのが一番だと考えました。つまり、その他すべての入力では false を返すということです。self にデータを持たせても無意味であるため、この場合も静的メソッドになります。

ハード・リセットですべてを元の状態に戻す

play_game 内で使用する終了メソッドのうちの 1 つは、リスト 15 に記載する reset です。

リスト 15. reset メソッド
fn reset(&mut self) {
    self.current_turn = Turn::Player;
    self.board = vec![
        vec![
            String::from("1"), String::from("2"),  
            String::from("3")],
        vec![
            String::from("4"), String::from("5"), 
            String::from("6")],
        vec![
            String::from("7"), String::from("8"), 
            String::from("9")],
    ];
}

このメソッドはゲームのメンバー変数をそれぞれのデフォルトにリセットするだけに過ぎません。

ゲームを完成するために必要な最後のメソッドは、リスト 16 に記載する get_next_turn です。

リスト 16. get_next_turn メソッド
fn get_next_turn(&self) -> Turn {
    match self.current_turn {
        Turn::Player => Turn::Bot,
        Turn::Bot => Turn::Player,
    }
}

このメソッドは、self がどちらの順番になっていているかをチェックして、その反対の順番を返すだけです。

ゲームをコンパイルして実行する

game.rs モジュールが完成したので、この時点で、main.rs をコンパイルしてゲームをプレイできます (リスト 17)。

リスト 17. ゲームをコンパイルする
extern crate rand;

mod game;

use game::Game;

fn main() {
    println!("Welcome to Tic-Tac-Toe!");

    let mut game = Game::new();

    game.play_game();
}

このコードは単純です。まず、mod を使って、ゲーム・モジュールがこのプロジェクト内に存在することを宣言し、use を使って Game オブジェクトをスコープ内に取り込みます。次に、Game::new() を使って game オブジェクトを作成し、そのオブジェクトにゲームをするよう指示します。これで、Cargo を使用してゲームを実行できる状態になりました (リスト 18)

リスト 18. ゲームを実行する
$ cargo run
   Compiling tic_tac_toe v0.1.0 …
    Finished dev [unoptimized + debuginfo] …
     Running …
Welcome to Tic-Tac-Toe!

+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 4 | 5 | 6 |
+---+---+---+
| 7 | 8 | 9 |
+---+---+---+


Please enter your move (an integer between 1 and 9):
…

最後のまとめ

このチュートリアル全体を通して学んだように、Rust は Java、C#、または Python の使いやすさと、C または C++ の処理速度と力を併せ持つ万能の言語です。コンパイルされたこのコードは処理速度に優れているだけでなく、エラーに関する懸念事項は実行時ではなくコンパイル時に処理されるため、コード内の人為的なエラーが削減されます。

次のステップ

  • 私がこの記事のために作成したコードを確認するには、このリンク先の GitHub リポジトリーにアクセスしてください。

ダウンロード可能なリソース


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=1063525
ArticleTitle=Rust 入門ガイド: Rust 言語でのコーディングを開始する
publish-date=11222018