LoginSignup
8
12

More than 5 years have passed since last update.

React+Reduxでライフゲームを作ってみた

Posted at

今年の応用情報でライフゲームが出てきて
ライフゲーム作りたいなーって思ったので
React + Reduxの勉強ついでに作ってみました。
(完成と呼ぶにはあまりに拙い出来ですが・・・

ライフゲームとは

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

今回はwikiにもある基本的なルールで実装していきます

誕生
死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
生存
生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
過疎
生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
過密
生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

Directories

ディレクトリは以下のようになっています。

~/t/react-redux-lifegame ❯❯❯ tree src                                                        src
├── actions
│   └── index.js
├── components
│   ├── App.jsx
│   ├── Board.jsx
│   └── Cell.jsx
├── containers
│   └── LifeGame.js
├── reducers
│   ├── cells.js
│   ├── generation.js
│   └── index.js
├── app.js
└── index.html

action, component,container,reducerの4要素に分けるのが
コツっぽいのでそれっぽく分けたつもり。。。

ポイント

  • action: storeの状態遷移を指示する。
  • component: view(react)の部分。stateに合わせた見た目を定義する
  • container: react-reduxの結合部分。reactのstateとreduxのstore、dispatcher(actionを発行するやつ)をマッピングする
  • reducer: 発行されたactionを見てstoreの状態遷移を行う。副作用を持たない

componentとcontainerの切り分けが意外に困ったりする。。。

Main

app.js

app.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import lifegame from './reducers'
import App from './compoents/App'

const initialState = {
  cells: [
    [0,1,0,0,0],
    [1,1,0,1,0],
    [0,0,1,0,0],
    [0,0,0,0,0],
    [0,0,0,1,0]
  ],
  generation:0
}

const store = createStore(lifegame, initialState)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('container')
)

今回storeは以下の2つを定義

  • cells:各セルの状態を格納
  • generations: 現在の世代数を格納

ポイント

  • アプリケーションに必要な状態はここのstoreのみで管理する
  • createStoreでreducerとstoreを対応させる

Actions

index.js

index.js
export function nextGeneration(){
  return { type:'NEXT_GENERATION'}
}

ここでは次世代への遷移を指示するアクションのみ定義

ポイント

  • アクションでは状態遷移の種類(type)と状態遷移に必要な値(idとかformから渡された値とか)を渡す
  • ここではどんな状態遷移を行うかの指示のみでstoreの更新等は行わない

Components

App.jsx

App.jsx
import React from 'react'
import LifeGame from '../containers/LifeGame'

const App = () => (
    <LifeGame />
)

export default App

Root Component
ここではLifGame Container を呼び出している

Board.jsx

Board.jsx
import React from 'react'
import Cell from '../components/Cell'
import {nextGeneration} from '../actions'

class Board  extends React.Component{

  componentDidMount() {
    const {dispatch} = this.props
    this.timer = setInterval(() => dispatch(nextGeneration())
                              , 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  }

  render(){
    const {cells, generation} = this.props

    return (
      <div>
      <span>Generaion: {generation}</span>
      <table>
        <tbody>{
        cells.map((line, i) =>
          <tr key={i}>{
              line.map((cell,i) =>
                <Cell key={i} isLive={cell} />
              )
            }</tr>
         )
       }</tbody>
       </table>
       </div>
    )
  }
}

export default Board

ライフゲームの盤面を表示するコンポーネント

ポイント

  • componentDidMountはコンポーネントが描画されたあとに呼び出される。
    ここで1秒ごとにnextGenerationアクションを発行するようにしてる

  • componentWillUnMountはコンポーネントが破棄されるときに呼び出される。
    ここでタイマーを破棄しておかないと次のコンポーネント描画時のタイマーと被ってしまう?

  • this.propsに入ってくる値はcontainerで決める

  • 基本的にコンポーネントはRedux側に依存しない・・・??

世代数の表示もこのコンポーネント内でやってしまっているが、分けたほうがいい気がする。。

cell.jsx

cell.jsx
import React from 'react'

let Cell = ({isLive}) => (
    <td>{isLive?'':''}</td>
)

export default Cell

各セルの見た目。
もうちょっと綺麗にしたい。。。

Containers

LifeGame.js

LifeGame.js
import { connect } from 'react-redux'
import Board from '../components/Board'

function mapStateToProps(state){
  return{ cells:state.cells, generation:state.generation}
}

const LifeGame = connect(mapStateToProps)(Board)

export default LifeGame

reactとreduxの結合部分
mapStateToPropsでComponentに渡す状態を決めてあげる

ポイント

  • react-redux#connect でreactとreduxの結合を行う
  • connectの第1引数にはマッピングを定義した関数, 第2引数には結合するコンポーネントを渡す

Reducers

index.js

index.js
import { combineReducers } from 'redux'
import cells from './cells'
import generation from './generation'

const lifegame = combineReducers({
  cells
  ,generation
})

export default lifegame

Reducerを一つのファイルに書くと多くなってしまうので、
状態ごとに分けてここで一つのReducerとして結合する

cell.js

cell.js
function cells(state=[], action) {
  switch(action.type) {
  case 'NEXT_GENERATION':
    return _updateCells(state)
  default:
    return state
  }
}

function _updateCells(cells=[]){
  var cells_tmp = cells

  for(var y = 0; y < cells.length; y++){
    for(var x = 0; x < cells[y].length; x++){
      var cnt = _countLiveCell(cells, x, y)
      if(cnt < 2 || cnt >= 4){
        cells_tmp[y][x] = 0
      }else{
        cells_tmp[y][x] = 1
      }
    }
  }

  return cells_tmp
}

function _countLiveCell(cells, x, y){

  var cntLiveCell = 0
  for(var i = -1; i <= 1; i++){
    for(var j = -1; j <= 1; j++){

      if(i === 0 && j === 0 ){
        continue
      }

      var _y = _checkRange(y + i, 0, cells.length - 1)
      var _x = _checkRange(x + j, 0, cells[y].length - 1)

      if(cells[_y][_x] === 1){
        cntLiveCell++
      }

    }
  }

  return cntLiveCell

}

function _checkRange(num, min=0, max=0){

  if(num < min){
    return min
  }

  if(num > max){
    return max
  }

  return num

}

export default cells

セルの状態遷移を担うreducer
actionのtypeを見て行う処理をスイッチさせるように書くっぽい。。
原則渡ってきたaction.typeが不明な場合は元のstateを返すようにする
またreducerは副作用を持たない(=元のstoreの値を更新しない)ようにする
(ただ上のコードは元のstoreの値を更新してしまっているような気もする。。。)

ライフゲームのルール実装部分が汚いので、
もうちょっとES2015の書き方で簡潔に書きたい

generation.js

generation.js
function generation(state=[], action){
  switch(action.type) {
  case 'NEXT_GENERATION':
    return state + 1
  default:
    return state
  }
}

export default generation

世代数を表すreducer
セルの世代交代と完全に別物になっている。

最初はreducerとactionが分かれている理由がよくわかっていなかったが、
action ⇛ reducer とすることでreducerの責任が限定されるので
実装/テストしやすくしてるんではないかなー

成果物

ligame-demo.gif

感想

React も Redux も最初はすごいとっつきにくいですが、
実際にコード書いてみると小さい単位でコードを書いていけるので
よさ気な感じですね。

特にデータバインディングなのでロジック部分はデータをどのように
いじるかのみに集中すればよいので、すごく実装しやすいと思いました。

今回のコードはGitにあげています。
https://github.com/tamanugi/react-redux-lifegame

また実装の拙い部分や考え方でおかしい部分などあればご指摘もらえますと、
嬉しいです。

8
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
12