C言語に代わってOS開発に採用されているRust。本連載では、Rustで有名アルゴリズムを実装して、Rustについての理解を深めています。今回扱うのは、生物の栄枯盛衰をシミュレーションするライフゲームです。
コンウェイのライフゲームとは?
「ライフゲーム(Life Game)」は、イギリスの数学者コンウェイによって考案されたもので、簡単な配列操作によって実装できる簡単な生物のシミュレーションです。次のように動きます。
見た目が面白いのに加えて、プログラミング言語の性質や特徴を知るのにもってこいの題材であるため、次の姉妹連載でも何度か紹介しています。ぜひ、今回のRust版と見比べてみてください。
- Python連載9回目(https://news.mynavi.jp/techplus/article/zeropython-9/)
- JavaScript連載25回目(https://news.mynavi.jp/techplus/article/zerojavascript-25/)
- 日本語プログラミング言語「なでしこ」連載25回目(https://news.mynavi.jp/techplus/article/nadeshiko-25/)
実際にプログラムを紹介する前に、簡単にライフゲームのルールを紹介します。ライフゲームは「生物集団は過疎でも過密でも生きてはいけない」という基本的な原則に沿ったシミュレーションが行われます。
二次元のグリッドに生物(セル)を配置して、移りゆく世代ごとに生物の生死を決定します。そのため各グリッドの周囲8方向を確認して生きているセルの個数を調べます。そして、個数により生死を判定します。
- 死んでいるセルの周囲に、生きているセルが3つあれば、次の世代に生物が誕生する
- 生きているセルの周囲に、生きているセルが…
-- 1つ以下ならば、過疎なので生物は死滅する
-- 2つか3つあれば、次の世代も生存する
-- 4つ以上ならば、過密なので死滅する
このルールに沿ってシミュレーションが行われます。
プロジェクトを作成しよう
それでは、Rustのパッケージマネージャーを兼ねたcargoコマンドを使ってプロジェクトを作成しましょう。また、本連載13回目で迷路ゲームを作るのにも使ったターミナル制御ライブラリの「crossterm」を使う事にします。
ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、次のコマンドを実行しましょう。
# プロジェクトの作成
mkdir program
cd program
cargo init
# 乱数クレートとcrosstermのインストール
cargo add lazyrand
cargo add crossterm
なお、Rustでは乱数を利用するには何かしらの乱数ライブラリをインストールする必要があります。ここでは、Pythonライクに乱数を生成できるlazyrandを利用することにします。
上記のコマンドを実行すると、次のようなディレクトリ構成のプロジェクトが生成されます。
.
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
メインプログラムを記述しよう
それでは、メインプログラムを、src/main.rsに記述しましょう。88行と少し長いのでプログラム全体をこちらにアップしています。実行する際は、プログラム全体をmain.rsに記述してください。
プログラムを少しずつ見ていきましょう。まずは、利用するクレートと定数の宣言を行います。
use lazyrand::rand_usize;
use std::thread;
use std::io::{stdout, Result};
use crossterm::{cursor, execute, terminal,
style::{Color, Print, SetBackgroundColor, SetForegroundColor}};
// 定数の宣言
const WIDTH: usize = 80; // グリッドの列数
const HEIGHT: usize = 35; // グリッドの行数
const MAX_TERN: usize = 1000; // 最大世代数
そして、メイン関数を記述します。Rustでは、C言語と同じように最初にmain関数が実行されます。
fn main() -> Result<()> {
// 画面をクリアしてカーソルを(0, 0)に移動 --- (*1)
execute!(stdout(),
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0))?;
let mut cells = init_cells(); // ランダムに初期セルを初期化 --- (*2)
for i in 0..MAX_TERN { // 繰り返し世代を勧める --- (*3)
// セルを描画
draw_cells(&cells)?;
println!("{}/{}", i, MAX_TERN);
// 300ミリ秒待つ
thread::sleep(std::time::Duration::from_millis(300));
// 次の世代のセルを計算 --- (*4)
cells = next_generation(&cells);
}
Ok(())
}
プログラムの(*1)では、先ほどインストールしたcrosstermを使って画面をクリアします。そして、(*2)ではセルの初期配置をランダムに決定します。関数init_cellsなどはこの後、紹介します。
(*3)のfor文では繰り返し生物のシミュレーションを行います。セルの状態を描画して、300ミリ秒待ちます。そして、(*4)で次世代のセルの状態を計算するという処理をMAX_TERN回繰り返します。
次に、init_cells関数の定義を見てみましょう。このプログラムでは、生物の状態を管理するために、二次元のベクタ配列を使っています。関数の戻り値を見ると分かるように、真偽型boolを持つベクタ配列Vec<bool>を二次元に重ねた、Vec<Vec<bool>>という型を利用します。
// ランダムにセルの初期化を行う関数
fn init_cells() -> Vec<Vec<bool>> {
// 2次元のベクタ配列を作成してfalseで初期化 --- (*5)
let mut cells = vec![vec![false; WIDTH]; HEIGHT];
// 適当にライフゲームの初期状態を作成
for _ in 0..(WIDTH * HEIGHT / 13) {
cells[rand_usize() % HEIGHT][rand_usize() % WIDTH] = true;
}
cells
}
プログラムの(*5)の部分では、二次元のベクタ配列を列数WIDTH、行数HEIGHTを初期値falseで初期化します。vec!というのは、ベクタ配列を作成するためのマクロです。
例えば、以下のように記述してベクタを初期化できます。
// 複数の値を指定してベクタを初期化
let a = vec![0, 0 , 0];
// 特定の値を指定してベクタを一括初期化(値0を持つ3要素の配列)
let a = vec![0; 3];
}}}
続いて、セルをターミナルに描画する関数draw_cellsを見てみましょう。クレートcrosstermのexecute!マクロを利用してターミナルを操作しつつ色のついた文字を描画します。cursor::MoveToでカーソルを移動、SetForegroundColorで前景色、SetBackgroundColorで背景色を変更します。
// セルをターミナルに描画する
fn draw_cells(cells: &Vec<Vec<bool>>) -> Result<()> {
execute!(stdout(), cursor::MoveTo(0, 0))?;
for row in cells {
for &cell in row {
if cell {
execute!(stdout(),
SetForegroundColor(Color::Yellow),
SetBackgroundColor(Color::Red),
Print("+"))?;
} else {
execute!(stdout(),
SetForegroundColor(Color::Blue),
SetBackgroundColor(Color::Black),
Print("-"))?;
}
}
execute!(stdout(), Print("\n"))?;
}
// execute!(stdout(), ResetColor)?;
Ok(())
}
上記のPrintで表示する文字を変更したり、セルの色を変更したりしてみると、雰囲気が変わるので試してみると良いでしょう。
// 次世代のセルを計算する関数
fn next_generation(cells: &Vec<Vec<bool>>) -> Vec<Vec<bool>> {
// 次世代のセルを生成
let mut new_cells = vec![vec![false; WIDTH]; HEIGHT];
// 現在のセルについてルールを適用 ---- (*6)
for y in 0..HEIGHT {
for x in 0..WIDTH {
// 周囲の生存セルを数える --- (*7)
let mut count = 0;
for dy in -1..=1 {
for dx in -1..=1 {
if dy == 0 && dx == 0 { continue; }
let ny = (y as isize + dy + HEIGHT as isize) as usize % HEIGHT;
let nx = (x as isize + dx + WIDTH as isize) as usize % WIDTH;
if cells[ny][nx] { count += 1; } // 生存セルなら+1 --- (*8)
}
}
// ライフゲームのルールの沿って次世代の生死を判定 --- (*9)
new_cells[y][x] = match (cells[y][x], count) {
(true, 2) | (true, 3) => true,
(false, 3) => true,
_ => false,
};
}
}
new_cells
}
プログラム(*6)以降のfor文では現在のセルの状態を左上から右下に向かって走査します。 (*7)ではセル(x, y)の周囲にある8個のセルの状態を調べます。セル画生存してれば、生存数を表す変数countを1加算します。
そして、(*9)で次世代のセルの状態を判定します。ここでは、match文を使って次世代の生死を判定します。ここでmatch文に指定しているのは、(現在の状態, 周囲の生存数)を表すタプルです。例えば、(true, 2)という条件は、現在生存しており、周囲に2つ生存セルがあることを表しています。このように、match文を使うとスッキリと条件判定を記述できます。
プログラムを動かしてみよう
プログラムを動かすには、ターミナルで下記のコマンドを実行します。
cargo run
世代が進むにつれて、生物が増え広がったり、減少したりするので、とても面白いです。1000回世代が進むとプログラムは終了します。もし、途中でプログラムを終了したい時には、ターミナルを閉じるか、[Ctrl]+[c]キーを押します。
まとめ
以上、今回はRustを使ってライフゲームを作成してみました。他の言語と比べてみると、より楽しめるでしょう。Rustでポイントとなるのは、二次元ベクタ配列を使っているという点です。メモリ管理を簡単にするために、世代が進む毎に、次世代セルを生成し直しているのも、Rustらしいプログラムと言えるでしょう。本稿がRustに親しむ助けになれば幸いです。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「実践力をアップする Pythonによるアルゴリズムの教科書(マイナビ出版)」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。