Javaでライフゲームを作る

# Javaでライフゲームを作る
去年、大学の講義でライフゲームを作る課題がありました。
~~無事単位も出たので~~、ライフゲームについて記事にまとめようと思います。

## tl;dr
作ったもの https://github.com/natmark/LifeGame

![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/84888/3e0348aa-7f43-3725-3a51-67124ca1aa3b.gif)

## ライフゲームの作り方
### そもそもライフゲームとは
> ライフゲーム (Conway's Game of Life[1]) は1970年にイギリスの数学者ジョン・ホートン・コンウェイ (John Horton Conway) が考案した生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲームである。単純なルールでその模様の変化を楽しめるため、パズルの要素を持っている。 - Wikipediaより引用

https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0

ライフゲームは碁盤のようなマス目を使用して、シミュレーションを行うものです。
一つの格子を**セル**と呼び、セル自体は生か死かの状態しか持ちません。

### ライフゲームのルール
今回、下記のルールを用いました。
**誕生**
&nbsp; 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
**生存**
&nbsp; 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
**過疎**
&nbsp; 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
**過密**
&nbsp; 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

### セルの変化の例
<img width="801" alt="スクリーンショット 2017-04-07 14.29.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/84888/c6eb3e86-edc0-e2ff-fc5d-8a4c332a687a.png">
※生きているセルを黒、死んでいるセルを白とする。

赤枠のセルに着目したとき、世代nにおける赤枠セルの周囲8つのセルの状態から、世代n+1における赤枠のセルの状態が決定する。

## 実装
### メインクラス
```java:LifeGame.java
public class LifeGame {
	public static void main(String[] args){
		GameFrame frame = new GameFrame(Const.FRAME_SIZE.width,Const.FRAME_SIZE.height);
		frame.setTitle("LifeGame");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		try {
			frame.setCells(Const.CELL_SIZE);
		} catch (Exception e) {	e.printStackTrace(); }
		
		frame.setVisible(true);
		
	     Thread t = new Thread(frame);
	     t.start();
	}
}
```

### ゲームフレーム用クラス

```java:GameFrame.java
public class GameFrame extends JFrame implements Runnable{
	private int width;
	private int height;
	private boolean running = false;

	//盤面に配置されたセルの配列
	private ArrayList<LifeCell> cells;
	//プリセットパターンを生成する際に使用。cellsを二次元配列にしたもの。
	private LifeCell[][] cell_matrix;
	/**
	 * Constructors
	 */
	public GameFrame(int width, int height){
		super();

		this.width = width;
		this.height = height;
		setSize(width,height);	
		cells = new ArrayList<LifeCell>();
	}
	public GameFrame(String title){
		super(title);
		cells = new ArrayList<LifeCell>();
	}
	/**
	 * セルサイズの変更
	 * @param cellSize
	 * @throws Exception 
	 * @throws cellSizeがFrameSizeに合わない場合 
	 */
	public void setCells(int cellSize) throws Exception {	
		if((width % cellSize != 0) || (width < cellSize)){
			throw new Exception("セルのサイズがフレームの幅に合っていないか、セルサイズがフレームの幅より大きいです。");
		}
		if(height % cellSize != 0 || (height < cellSize)){
			throw new Exception("セルのサイズがフレームの高さに合っていません、セルサイズがフレームの高さより大きいです。");
		}		
		//縦の総セル数
		int v_cell_cnt = height / cellSize; 
		//横の総セル数
		int h_cell_cnt = width / cellSize; 
		
		//JFrameの高さには、タイトルバーが含まれてしまうため、heightを更新
		setVisible(true);
		//visible状態でないと、Insetsが取得不可
		Insets insets = getInsets(); 
		setVisible(false);			

		//セル用パネルの生成
		JPanel p = new JPanel();
		p.setLayout(null);
		p.setBounds(0, 0, width + h_cell_cnt, height + v_cell_cnt);
		p.setBackground(Color.black);

		//ツールパネルの生成
		JPanel toolPanel = new JPanel();
		toolPanel.setLayout(null);
		toolPanel.setBounds(0, p.getBounds().height, p.getBounds().width, 100);
		toolPanel.setBackground(Color.white);
		
		//パネルへのボタン追加
		addButtonsOnPanel(toolPanel);
		
		//フレームサイズの更新
		int insets_height = insets.top + insets.bottom;
		setResizable(true);
		setSize(width + h_cell_cnt,height + insets_height + v_cell_cnt + toolPanel.getBounds().height);
		setResizable(false);

		cell_matrix = new LifeCell[v_cell_cnt][h_cell_cnt];
		//セルの生成
		for(int y = 0; y < v_cell_cnt; y ++){
			for(int x = 0; x < h_cell_cnt; x++){
				LifeCell cell = new LifeCell(new Rectangle(x * (cellSize + 1),y * (cellSize + 1),cellSize,cellSize));
			    p.add(cell);
			    cell_matrix[y][x] = cell;
			    cells.add(cell);
			}			
		}
		
		//セルの外周をセット。
		for(int y = 0; y < v_cell_cnt ;y++){
			for(int x = 0; x < h_cell_cnt ;x++){
				LifeCell cell = cell_matrix[y][x];
				
				int dx_min = -1;
				int dy_min = -1;
				int dx_max = 1;
				int dy_max = 1;				
				//yが一番上
				if(y == 0){ dy_min = 0; }
				//xが一番左
				if(x == 0){ dx_min = 0; }
				//yが一番下
				if(y == v_cell_cnt - 1){ dy_max = 0; }
				//xが一番右
				if(x == h_cell_cnt - 1){ dx_max = 0; }
				
				for(int dx = dx_min; dx <= dx_max;dx++){
					for(int dy = dy_min; dy <= dy_max;dy++){
						if(!(dx == 0 && dy == 0)){
							cell.addSurroundings(cell_matrix[y+dy][x+dx]);
						}
					}					
				}
			}			
		}
		
		Container contentPane = getContentPane();
		contentPane.add(p);
		contentPane.add(toolPanel);
	}
	
	/**
	 * ツールパネルへのボタン追加 
	 */
	private void addButtonsOnPanel(JPanel panel){		
		//ボタンのアクションリスナー作成
		ActionListener RunBtnAction = new ActionListener(){
			public void actionPerformed(ActionEvent event){
				//動作状態を切り替え
				running = !running;
				JButton btn = (JButton)event.getSource();
				if(running){
					btn.setText("Stop");
				}else{
					btn.setText("Run");								
				}
			}
		};
		ActionListener ClearBtnAction = new ActionListener(){
			public void actionPerformed(ActionEvent event){
				//盤面全てを初期化
				LifeCell.forceKillAll(cells);
			}
		};
		ActionListener generateGliderBtnAction = new ActionListener(){
			public void actionPerformed(ActionEvent event){
				//グライダーのパターン生成
				CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.GLIDER);
			}
		};
		ActionListener generateSpaceShipBtnAction = new ActionListener(){
			public void actionPerformed(ActionEvent event){
				//宇宙船のパターン生成
				CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.SPACESHIP);
			}
		};
		ActionListener generateGalaxyBtnAction = new ActionListener(){
			public void actionPerformed(ActionEvent event){
				//銀河のパターン生成
				CellPattern.patternGenerator(cell_matrix, CellPattern.Pattern.GALAXY);
			}
		};
		//格納順を保持したいため、LinkedHashMapを使用
		LinkedHashMap<String,ActionListener> btnSources = new LinkedHashMap<String,ActionListener>();
		btnSources.put("Run", RunBtnAction);
		btnSources.put("Clear", ClearBtnAction);
		//スペーサー
		btnSources.put("", null);
		btnSources.put("グライダー", generateGliderBtnAction);
		btnSources.put("宇宙船", generateSpaceShipBtnAction);
		btnSources.put("銀河", generateGalaxyBtnAction);

		//ボタン生成
		int i = 0;
		for(Map.Entry<String, ActionListener>  btnSrc : btnSources.entrySet()){
			//空文字の場合、インクリメントしてスキップ
			if(btnSrc.getKey().equals("")) { i++; continue; }
			
			JButton button = new JButton(btnSrc.getKey());
			button.setBackground(Color.black);
			button.setBounds(10 + i * 80,panel.getBounds().y, 80, 50);
			button.addActionListener(btnSrc.getValue());
			panel.add(button);						
			i++;
		}
	}
	public void run(){
		while(true){
			//Startボタンが押されていない場合、処理を中断
			while(!running){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {	e.printStackTrace(); } 
			}
			
			try {
				//更新速度
				Thread.sleep(Const.SLEEP_TIME_MS);
			} catch (InterruptedException e) {	e.printStackTrace(); }
			
			for(LifeCell cell : cells){
				//周囲のセルの状況を確認
				cell.checkSurroundings();
			}
			for(LifeCell cell : cells){
				//世代交代(セルの塗り替え)
				cell.generationalChange();
			}
		}
	}
}
```
今回ゲームのフレームサイズやセルのサイズをユーザ定義で変更できるようにしました。
(その結果、少しソースが長くなってしまいましたが...)

`Runnable`を実装して、run()が実行されると、無限ループを発生させています。
スリープを挟みながら、毎フレームセルの更新処理をさせています。

```java
//周囲のセルの状況を確認
cell.checkSurroundings();
```
この部分で、セルの周囲の状態から、次の世代におけるセルの状態を確定し(世代 n)

```java
//世代交代(セルの塗り替え)
cell.generationalChange();
```
この部分で世代を交代させています(世代 n + 1)

### セル用クラス
最後にセル用のクラスですが、セルはJButtonを継承させています。
セルをマウスでクリックしたときに、セルの状態を切り替えることができるようになっています。

```java:LifeCell.java
class LifeCell extends JButton{
	//周囲のセル
	private ArrayList<LifeCell> surroundings;
	//現在の生存フラグ
	private boolean isLiving = false;
	//世代更新後の生存フラグ
	private boolean willLiving = false;
	/**
	 * Constructors
	 */
	public LifeCell(){
		super("");
		surroundings = new ArrayList<LifeCell>();
		setLayout();
	}

	public LifeCell(Rectangle rect) {
		super("");
		this.setBounds(rect);
		surroundings = new ArrayList<LifeCell>();
		setLayout();
	}
	/**
	 * レイアウトの設定
	 */
	private void setLayout(){
		this.setLayout(null);
		this.setBackground(Color.white);
        this.setContentAreaFilled(true);
        this.setOpaque(true);
        this.setBorderPainted(false);
		LifeCell button = this;
		
		this.addActionListener(
				new ActionListener(){
						public void actionPerformed(ActionEvent event){
							if(button.isLiving){
								button.forceKill();
							}else{
								button.forceSpawn();
							}
						}
					}
				);
	}
	/**
	 * 周囲のセルを追加
	 * @param cell:LifeCell
	 */
	public void addSurroundings(LifeCell cell){
		surroundings.add(cell);
	}
	/**
	 * 世代交代を行う
	 */
	public void generationalChange(){
		isLiving = willLiving;
		willLiving = false;
		if(isLiving){
			setBackground(Color.black);
		}else{
			setBackground(Color.white);			
		}
	}
	/**
	 * 周りのセルを調べて、世代交代の準備
	 */
	public void checkSurroundings(){
		int cnt = 0;
		for(LifeCell cell : surroundings){
			if(cell.isLiving) cnt++;
		}
		//誕生
		if(!isLiving){
			//生きているセルが3つ
			if(cnt == Const.BIRTH_CNT){
				this.willLiving = true;
			}
			return;
		}
		if(isLiving){
			//生存
			if(Const.LIVING_RANGE.includes(cnt)){
				this.willLiving = true;
			}
			//過疎
			if(Const.LIVING_RANGE.lowerBound > cnt){
				this.willLiving = false;
			}
			//過密
			if(Const.LIVING_RANGE.upperBound < cnt){
				this.willLiving = false;
			}
		}
	}
	/**
	 * ボタンを押して生成する場合
	 */
	public void forceSpawn(){
		isLiving = true;
		this.setBackground(Color.black);
	}
	/**
	 * ボタンを押してセルを消す場合
	 */
	public void forceKill(){
		isLiving = false;
		this.setBackground(Color.white);
	}
	/**
	 * 盤面初期化用
	 */
	public static void forceKillAll(ArrayList<LifeCell> cells){
		for(LifeCell cell : cells){
			cell.isLiving = false;
			cell.willLiving = false;
			cell.setBackground(Color.white);			
		}
	}
}
```
`GameFrame.java`の`setCells(int cellSize)`でセルを配置するときに、
`addSurroundings(LifeCell cell)`を用いて、周囲のセルのインスタンスを保持しています。
そのため、世代交代で周りのセルを調べる際は

```java
  /**
     * 周りのセルを調べて、世代交代の準備
     */
    public void checkSurroundings(){
        int cnt = 0;
        for(LifeCell cell : surroundings){
            if(cell.isLiving) cnt++;
        }
        //n世代で死
        if(!isLiving){
            //n + 1世代で誕生
            if(cnt == Const.BIRTH_CNT){
                this.willLiving = true;
            }
            return;
        }
        //n世代で生
        if(isLiving){
            //n + 1世代で生存
            if(Const.LIVING_RANGE.includes(cnt)){
                this.willLiving = true;
            }
            //n + 1世代で死(過疎)
            if(Const.LIVING_RANGE.lowerBound > cnt){
                this.willLiving = false;
            }
            //n + 1世代で死(過密)
            if(Const.LIVING_RANGE.upperBound < cnt){
                this.willLiving = false;
            }
        }
    }
```

周囲のセルが格納されている`surroundings`を用いて、生きているセルの数を数えるだけで済むようにしています。
これによって毎フレーム二重配列を走査させる必要がないので、処理を軽くすることができます。



## ライフゲームにおけるパターン
[ライフゲーム - Wikipedia]
(https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0)のパターンの例を見るとわかりやすいですが、
ライフゲームにおけるセルのパターンには、特殊な性質を持ったものが存在します。

講義の課題に、パターンを調べる課題が含まれていたので、作成したライフゲーム内で、下記のパターンを生成できるようにしました。

| パターン名 | 種類 | パターン|
|:---------:|:---------:|:--------:|
|グライダー | 移動物体 | <img src = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/Game_of_life_glider.svg/44px-Game_of_life_glider.svg.png"></img>|
|宇宙船 | 移動物体 | <img src = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Game_of_life_lwss.svg/72px-Game_of_life_lwss.svg.png"></img>|
| 銀河 | 振動子 | <img src = "https://upload.wikimedia.org/wikipedia/commons/2/2a/Oscilador8periodos.gif"></img>|


## まとめ
今回初めてライフゲームを作ってみました。
単純なルールに従って、画面が変わっていくだけの単純なものですが、眺めていると結構楽しいです。
今回紹介した3パターン以外にも、固定物体(世代が進んでも変化しない)パターンや、繁殖型(世代が進むと、別のパターンを新たに生成する)パターンなどいろいろあるので、新しいパターンを探して見るのも楽しいかもしれません。