rustでライフゲームをまったり作っていきます。
その第1回目です。
ライフゲームのについての解説から、フィールドを作って、セルの周りの状態を調べるあたりまでやろうと思います。
セルラーオートマトンなのかセルオートマトンなのか、まぁ英語ではcellular automatonなので、これをどう読むかの問題なので、そこらへんは、柔軟に。
ライフゲームとは?
ライフゲームは、2次元のセルラーオートマトン(セルオートマトン)です。
セルラーオートマトンは、空間があって、時間があって、あとは変化条件があって、変化していくっていうモデルです。
以前にnannou rustで1次元のセルラーオートマトンを作ったことがあるので、そちらも是非ご覧ください。
さて、2次元のセルラーオートマトンである、ライフゲーム、環境とルールはこんな感じ。
ライフゲームの環境
- 空間:2次元
- 状態:生と死の2パターン
- 時間:一斉更新(世代)
ライフゲームのルール
- 人口過剰
- 生きているセルの周りに4つ以上ならそのセルは死亡
- 均衡状態
- 生きているセルの周りのセルが2か3であれば生き延びる
- 人口過疎
- 生きているセルの周りのセルが1つの場合は、死亡
- 誕生
- 死んでいるセル周りのセルがちょうど3つの時に死んでるセルは生き返る
周りの生きているセルがちょうどいい数であれば、生存を続け、そして、誕生し、少なかったり多かったら死んでしまう。そんなルールです。
では、Rustで作っていきましょう。
必要な要素を考える。
まずは、ライフゲームに必要な要素を考えていきます。
ライフゲームの環境から
縦横の広がりを持つ2次元の世界があって、そこに生と死の2つの状態を持ったセルが敷き詰められてるイメージです。
整理すると
- 縦と横の長さを持つセルが敷き詰められた世界(field)
- 生と死の状態をもったセル(cell)
ってことになります。
環境の実装
では、早速それを実装していきます。
セルを定義する
まずは、セルを定義していきます。
セルは生と死の二つの状態
#[derive(Debug,Clone, Copy,PartialEq)]
enum Cell {
Dead = 0,
Alive = 1,
}
列挙型こんな感じですね。
この後、セルをコピーしたり比べたりするので、いくつかderiveしておきましょう。
Deadの状態であれば0をAliveの状態であれば1を返します。
世界(フィールド)を定義する
次にセルが広がる世界(Field)を定義していきます。
フィールドは縦と横の広がりがあって、そこにはセルがあるので
struct Field{
width:u32,
height:u32,
cells:Vec<Cell>
}
こんな構造体を定義します。
2次元のフィールドの考え方っていくつかあって、今回は1次元のベクタ型の配列を使ってます。
2次元だから、2次元配列を使う方法ももちろんありだと思います。
違いはいろいろあるようなので、気になる人は調べてみてください。
各種関数を作っていく
セルの状態とフィールドの定義ができたので、ここからは、各種関数を作っていきます。
フィールドを生成したり、セルの状態を求めたり、そういうものです
フィールドを生成する関数
まずは、フィールドを作る関数を作ります。
セルを出現させるにもまずは世界創造をしていかないとね。
よくあるnew()関数です
impl Field{
fn new()->Field{
let width = 5;
let height =5;
let cells = (0..width*height)
.map(|i|{
if i%2==0||i%11==0{
Cell::Alive
}else{
Cell::Dead
}
}).collect();
Field { width, height, cells}
}
}
width,heightで幅と高さを指定して、あとは、セルを埋めていきます。
このコードだと、初期のセルの状態は2の倍数と11の倍数のところをAlive、それ以外をDeadにしてます。
自分で任意の初期状態セル状態を作る場合はcellの部分を
let cells = [
1,0,0,0,0,
0,1,1,1,0,
0,0,0,1,0,
1,0,1,0,0,
0,0,1,1,0
].into_iter()
.map(|i|{
if i==1{
Cell::Alive
}else{
Cell::Dead
}
}).collect();
こんな感じで書けばできます。
配列の位置を調べる関数
世界を創造できたので、次にその世界への操作の関数を作っていきます。
まずは、この世界は2次元だけど、プログラム上は1次元配列なので、2次元の位置を1次元に変換する関数を作ります。
impl Fieldの中にこれを追加します。
fn get_index(&self,row:u32,column:u32)->usize{
((row-1)*self.width+column-1) as usize
}
行と列を受けて、どこにあるのかのインデックス番号を返す関数です。
こんな感じの作業をしてます。
このプログラムの場合、スタートはゼロから始まるので、ゼロ番目、ゼロ行目がある。
けど、直感的にわかりずらいので、関数に渡すときは0行目スタートじゃなくて、1行目スタートにしたいなってことで、コードの中でrow-1,column-1っていう計算をして、補正してます。
セルの状態を調べる関数
続いて、セルの状態を調べる関数を作ります。
そのセルが生きてるのか死んでるのかってのを調べる関数。
これは、大した難しいことではなくて、さっきのget_index()で取得したインデックス番号のセル状態を返すだけの関数です。
fn get_cell_info(&self,row:u32,column:u32)->usize{
let idx = self.get_index(row, column);
self.cells[idx] as usize
}
周囲の生きたセルを返す関数
ライフゲームでは、自身の周囲のセルの状態で自分の生き死にが決まります。
自分の周囲のセルの状態を調べる関数を作ります。
fn get_alive_cells(&self,row:u32,column:u32)->usize{
let mut count:usize=0;
for y in [-1,0,1]{
for x in [-1,0,1]{
let y_idx = row as i32 + y;
let x_idx = column as i32 +x;
if y_idx >0 && x_idx>0 &&
y_idx<=self.height as i32 && x_idx<=self.width as i32{
count += self.get_cell_info(y_idx as u32, x_idx as u32);
}
}
}
if self.get_cell_info(row, column)==1{
count -=1;
}
count
}
自分の上下左右、斜めすべての数を数えます。
自分を中心として、上下左右にインデックスが-1,0,+1変化させれば、上下左右斜めのセルを走査できます。
それを、 forループで回していきます。
たとえば、自分が端の方にいた場合、エラーが出るので、それを回避するために指定した範囲内(幅と高さ)に収まるように条件分岐で条件を与えてます。
そして、get_cell_info関数でセルの状態を調べて、それを足し合わせてます。
生きているなら1,死んでいるなら0なので、足し合わせた数字が、自分を含め周りのセルの生きている数になります。
そして、最後に、自分自身を除くために自分が生きている場合は、カウンターからー1して、自分の周りの生きているセルの数のみ返すようにしています。
盤面を描画する関数
今回の最後は、コンソール画面に盤面を描画する関数を作ります。
盤面描画といってもそんなかっこいいものじゃないけどね(笑)
impl std::fmt::Display for Field{
fn fmt(&self,f:&mut std::fmt::Formatter)->std::fmt::Result{
for line in self.cells.as_slice().chunks(self.width as usize){
for &cell in line{
let symbol = if cell == Cell::Dead {'・'} else {'〇'};
write!(f,"{}",symbol)?;
}
write!(f,"\n")?;
}
Ok(())
}
}
これで、指定した幅ごとに改行されて、生きているセルには〇を死んでいるセルには・を表示させることができます。
〇・〇・〇
・〇・〇・
〇〇〇・〇
・〇・〇・
〇・〇・〇
こんな感じね。
次回予告
というわけで、今回は、ライフゲームのフィールドの定義から始まって、セルの状態を調べたり、盤面描画をするプログラムを作りました。
次は、世代更新をしていこうと思います。
次回でライフゲームのアルゴリズム的には完成かな?
ではお楽しみに
参考情報
作るうえで参考にしたサイトや書籍、資料など
参考書籍
・岡瑞起、池上高志、ドミニク・チェン、青木竜太、丸山典宏『作って動かすALife』 オライリー・ジャパン(2018)
コメント