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で表示する文字を変更したり、セルの色を変更したりしてみると、雰囲気が変わるので試してみると良いでしょう。