rustでライフゲームを作る#1

rust

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)

参考サイト

Rust and WebAssembly

コメント

タイトルとURLをコピーしました