Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

@tfujiwar

Reactの状態機械という概念から連想してライフゲームを再現してみた

作ったもの

入門Reactという本でReact.jsについて学んでいたところ、「Reactのコンポーネントは状態機械として振る舞う」という表現が非常にしっくりきた。
https://www.oreilly.co.jp/books/9784873117195/

この表現を見たとき、Reactを使えばライフゲームのようなセルオートマトン(格子状に並んだ状態機械)をわかりやすく表現できるのでは?と思いついたので、Reactの練習も兼ねて実際にやってみた。

ライフゲームとは、格子状に並んだセルが「生」と「死」の状態を持ち、隣接するセルの生存数のよって次世代にそのセルが生き残るかどうかが決定する、生命シミュレーションのこと。詳しくはこちら。
https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B2%E3%83%BC%E3%83%A0

スクリーンショット 2016-09-30 21.48.02.png

実装例

それぞれのセルを表現するCellコンポーネントと、すべてのセルをまとめるBoardコンポーネントを作成する。

Boardコンポーネント

  • props
    • nx(横に並ぶセル数)
    • ny(縦に並ぶセル数)

今回はボードの状態を定義しなかったが、例えばライフゲームの進行の停止・再生を切り替えたりする場合は、それを状態として定義したら良さそう。

updateGenerationメソッドでは、全セルの隣接セル生存数を集計・更新したのち、各セルのupdateAliveメソッドを呼び出している。各セルのstateにアクセスするために、各セルにはその座標に対応したref属性を与えている。ここで上下端・左右端はそれぞれ隣接している周期境界となっている(そのために%による剰余演算を使用)。

getLinesメソッドは、複数行にわたってセル群を出力するためのもの(もっと簡潔にしたい)。またcomponentDidMountメソッドでは、一定間隔でボードを更新するためのタイマーを設定している。

// セル全体をまとめるボード
var Board = React.createClass({

  // ボードを1世代前進させるメソッド
  updateGeneration: function() {
    // すべてのセルについて、隣接セル生存数を集計・更新する
    for (var iy = 0; iy < this.props.ny; iy++) {
      for (var ix = 0; ix < this.props.ny; ix++) {
        var neighbor = 0;
        for (var py = -1; py <= 1; py++) {
          for (var px = -1; px <= 1; px++) {
            if (px != 0 || py != 0) {
              var x = (ix + px + Number(this.props.nx)) % Number(this.props.nx);
              var y = (iy + py + Number(this.props.ny)) % Number(this.props.ny);
              if (this.refs[x + '_' + y].state.alive) {
                neighbor++;
              }
            }
          }
        }
        this.refs[ix + '_' + iy].setState({ neighbor: neighbor });
      }
    }
    // すべてのセルのalive状態を更新する
    for (var iy = 0; iy < this.props.ny; iy++) {
      for (var ix = 0; ix < this.props.ny; ix++) {
        this.refs[ix + '_' + iy].updateAlive();
      }
    }
  },

  // 複数行にわたるセル群を取得
  getLines: function() {
    var lines = [];
    for (var iy = 0; iy < this.props.ny; iy++) {
      for (var ix = 0; ix < this.props.ny; ix++) {
        var ref = ix + '_' + iy;
        lines.push(<Cell ref={ref} defaultAlive={default_alive[iy][ix]} />)
      }
      lines.push(<br />)
    }
    return lines;
  },

  render: function() {
    return (
      <div>{this.getLines()}</div>
    );
  },

  // 一定間隔でボードを更新するためのタイマー設定
  componentDidMount: function() {
    setInterval(this.updateGeneration, 100);
  }
});

Cellコンポーネント

  • props

    • defaultAlive(セルの生死のデフォルト値)
  • state

    • alive(セルの生死)
    • neighbor(隣接する8セルの生存数)

updateAliveメソッドでは、aliveとneighborの値を参照して、自身の生死状態を更新している。世代交代のロジックがここにまとまっている。

// それぞれのセル
var Cell = React.createClass({
  getInitialState: function() {
    return {
      alive: this.props.defaultAlive,
      neighbor: 0,
    }
  },

  // 世代交代ロジック
  updateAlive: function() {
    if (this.state.alive) {
      this.setState({ alive: (this.state.neighbor == 2 || this.state.neighbor == 3) });
    } else {
      this.setState({ alive: (this.state.neighbor == 3) });
    }
  },

  clickHandler: function() {
    this.setState({ alive: !this.state.alive });
  },

  render: function() {
    return (
      <div className={this.state.alive ? 'cell alive' : 'cell'} onClick={this.clickHandler}></div>
    );
  }
});

コンポーネント以外のコード

セルの生死状態を表現するCSSの記述。あと全セルの初期状態をランダムに設定するロジックと、Boardコンポーネントのマウントを行っている。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Life Game</title>
    <script src="https://npmcdn.com/react@15.3.1/dist/react.js"></script>
    <script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.js"></script>
    <script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>

    <style type="text/css">
      body {
        width: 100000px;
      }
      .cell {
        display: inline-block;
        width: 30px;
        height: 30px;
        margin-right: 5px;
        background-color: #eee;
        border-radius: 5px;
      }
      .alive {
        background-color: #5b86ff;
      }
    </style>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel">

      // ボードの初期状態
      var nx = 20;
      var ny = 20;
      var default_alive = [];
      for (var iy = 0; iy < ny; iy++) {
        default_alive.push([]);
        for (var ix = 0; ix < nx; ix++) {
          default_alive[iy].push(Math.floor(Math.random()*2) == 1);
        }
      }

      ReactDOM.render(
        <Board nx={nx} ny={ny} />,
        document.getElementById('content')
      );

      // CellとBoardの定義...

    </script>
  </body>
</html>

感想

とりあえず状態機械をReactコンポーネントで表現できたので満足。Cellコンポーネントは結構簡潔に書けて、世代交代のロジックをわかりやすく記述できたと思う。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What are the problem?