CoffeeScript と Canvas で作成するコンウェイのライフ・ゲーム

CoffeeScript と HTML5 の canvas 要素を使用して簡単なゲームを作成する

コンウェイのライフ・ゲーム (Conway's Game of Life) は、プレイヤーのいないゲームです。このゲームは初期構成に依存し、その後は入力なしで動作します。この記事では、ライフ・ゲームの独自のバージョンを実装する手順を説明します。CoffeeScript の機能と HTML5 の canvas 要素を使用してゲームを作成する方法を学んでください。サンプル・コードも用意されています。

David Strauß, Programmer, NerdKitchen

David Strauß photoDavid Strauß は、想像力豊かなプログラマーとして、有益なソフトウェアの作成に取り組み、NerdKitchen を運営しています。彼は、木工に情熱を注ぐ父親の姿から、幼い頃に物作りの魅力を見出しました。彼は主に CoffeeScript と Ruby on Rails フレームワークを使用しています。彼の初期のプロジェクトの 1 つである Marble Run は、Fantasy Interactive や Google Chrome Team などのチームを打ち負かして、Mozilla の Game On 2010 Challenge を勝ち取りました。



2012年 9月 06日

はじめに

新しい技術を詳しく学ぶ方法としては、簡単なゲームを作成するのが楽しい手段になります。この記事では、CoffeeScript と HTML5 の canvas 要素を使用してコンウェイのライフ・ゲーム (Conway's Game of Life) を独自に実装する手順を説明します。コンウェイのライフ・ゲームは、厳密に言うとゲームではありませんが、非常に扱いやすく、技術を学ぶには最適で簡単なタスクです。

頻繁に使用される略語

  • CSS: Cascading Style Sheets
  • DOM: Document Object Model
  • HTML: HyperText Markup Language

技術に関して必要なものを以下に記載します。

  • HTML5 の canvas 要素を処理できる適切な Web ブラウザー
  • CoffeeScript: JavaScript にコンパイルされる簡単なプログラミング言語です。10 分の時間を割いて、CoffeeScript の Web サイトでこの言語の機能を調べるか、無料のオンライン書籍『The Little Book on CoffeeScript』(「参考文献」を参照) を読むことをお勧めします。
  • CoffeeScript コンパイラー: CoffeeScript をプログラミングするために必要です。Node.js をインストールしてから、NPM (Node Package Manager) を使って CoffeeScript コンパイラーをインストールすることをお勧めします (「参考文献」を参照)。この記事のサンプルでは、CoffeeScript コンパイラーのバージョン 1.3.3 を使用します。
  • お好みのテキスト・エディター

記事で使用するサンプル・プログラムのソース・コードは、「ダウンロード」セクションからダウンロードすることができます。


コンウェイのライフ・ゲーム

コンウェイのライフ・ゲームは、基本的に、正方形のセルからなる 2 次元の直交グリッド上で繰り広げられるシミュレーションです。各セルは、「生」か「死」のいずれかの状態になります。このゲームは、定期的な更新 (ティック (tick) とも呼ばれます) で進められ、ティックのたびにセルの現行の世代が次の世代へと進化します。ティックの際には、以下のルールが個々のすべてのセルに適用されます。

  • 生きているセルは、隣接するセルのうち生きているセルが 2 つ以上なければ、過疎により死滅する。
  • 生きているセルは、隣接するセルのうち生きているセルが 2 つまたは 3 つあれば、次の世代に生き延びる。
  • 生きているセルは、隣接するセルのうち生きているセルが 4 つ以上あると、過密により死滅する。
  • 死んでいるセルは、隣接するセルのうち生きているセルがちょうど 3 つあると、再生により生きたセルになる。

セルの最初の世代は、ランダムに作成されます。それから後は、すべてのセルが死滅するか、パターンが出現するまで、シミュレーションが続きます。さまざまなパターンについての詳細、そしてコンウェイのライフ・ゲームの歴史については、「参考文献」を参照してください。

図 1 に、この記事で行う演習の最終的な結果を示します。この最終的な結果はオンラインで見ることも、自分で試してみることもできます (「参考文献」で紹介している、著者が作成したライフ・ゲームを参照してください)。

図 1. コンウェイのライフ・ゲームを実装して実行した例
完成したコンウェイのライフ・ゲームの実装を実行した例

実装

この記事では、コンウェイのライフ・ゲームを 2 つのパーツに分けて実装します。最初のパーツは、HTML5 マークアップと CSS です。これが、2 つ目のパーツに必要な基礎となります。2 つ目のパーツは、ゲームの実際の CoffeeScript 実装です。


HTML5 マークアップと CSS

まず初めに、すべてのサンプル・ファイルを保存するための「game-of-life」という名前のディレクトリーを作成します。マークアップと CSS を記述する場所が必要なので、新しく作成した game-of-life ディレクトリー内に新規 index.html ファイルを作成してください。リスト 1 に、コンウェイのライフ・ゲームの実装に必要な HTML5 マークアップと CSS を記載します。

リスト 1. index.html ファイル内のマークアップと CSS
<html>
<head>
  <title>Game of Life</title>

  <script type="text/javascript" src="javascripts/game_of_life.js">
  </script>

  <style type="text/css">
    body {
      background-color: rgb(38, 38, 38);
    }

    canvas {
      border: 1px solid rgba(242, 198, 65, 0.1);
      margin: 50px auto 0 auto;
      display: block;
    }
  </style>
</head>
<body>
  <script type="text/javascript">
    new GameOfLife();
  </script>
</body>
</html>

リスト 1 では、ゲームのタイトルを記述した後、javascripts ディレクトリーに格納されている game_of_life.js という名前の JavaScript ファイルを読み込みます。ご心配には及びません。プログラミングに使用するのは、JavaScript ではなく CoffeeScript です。game_of_life.js ファイルはまだ存在していませんが、この行を index.html ファイルに含めてください。

このライフ・ゲームの見栄えを良くするために、ここでちょっとしたスタイルを追加します。body 要素に暗い背景色を指定して、canvas 要素に境界線を追加します。さらに、CSS 属性の margin と display を使用して、canvas 要素が画面中央に配置されるようにします。

ライフ・ゲームを開始するために、このコードはマークアップの body に小さな script ブロックを追加して、新しい GameOfLife インスタンスを作成します。


CoffeeScript によるライフ・ゲームの実装

皆さんは、リスト 1 で、なぜ CoffeeScript ファイルではなく、game_of_life.js という JavaScript ファイルを読み込んでいるのかを不思議に思っていることでしょう。その理由は、多くのブラウザーがまだ CoffeeScript に対応していないためです。したがって、CoffeeScript コードをJavaScript にコンパイルすることで、ブラウザーが解釈できるようにしなければなりません。同じ理由から、game-of-life ディレクトリー内には 2 つの新規ディレクトリーを作成する必要があります。一方の新規ディレクトリーには「coffeescripts」という名前を付け、ここにすべての CoffeeScript コードを格納します。コンパイル後の JavaScript コードを格納するもう一方の新規ディレクトリーには、「javascripts」という名前を付けてください。

このサンプル実装に含まれるクラスは 1 つしかないので、作成する必要がある CoffeeScript ファイルは、このクラスに対応するファイルだけです。coffeescripts ディレクトリーに game_of_life.coffee というファイルを作成してください。ここに、すべての CoffeeScript コードを格納します。


CoffeeScript のコンパイル

ゲーム・ロジックの実装を開始するには、その前に、CoffeeScript コードを JavaScript にコンパイルする手段を見つけなければなりません。ありがたいことに、CoffeeScript コンパイラーには、コンパイルを実行する際に指定できるオプションがいくつか用意されています。例えば、以下のコマンドを実行すると、CoffeeScript コードが自動的に JavaScript にコンパイルされます。

coffee --output javascripts/ --watch --compile coffeescripts/

このコマンドを実行するには、任意のコマンドライン・ツールで game-of-life ディレクトリーにナビゲートします。その上で、coffee コマンドを使用して CoffeeScript コードを JavaScript にコンパイルします。上記のコマンド例では、最初に --output フラグで出力フォルダーを指定し、続いて --watch および --compile フラグを使用して、coffeescripts ディレクトリーを指定しています。

この指定内容が何を意味するのかと言うと、coffeescripts ディレクトリー内のファイルが変更されたときには必ず coffee コマンドがその変更を取得してコンパイルし、コンパイル後の JavaScript ファイルを javascripts ディレクトリーに保存するということです。これで、game_of_life.js ファイルを作成してもいないのに、リスト 1 でこのファイルを読み込んだ理由がおわかりでしょう。game_of_life.coffee ファイルに CoffeeScript コードを書き込めば、そのコードがコンパイルされて、生成された JavaScript コードが自動的に game_of_life.js ファイルに保存されるというわけです。


ゲームの初期化

コンパイルの問題に対処したので、今度はライフ・ゲームのサンプル版のプログラミングに取り掛かります。任意のテキスト・エディターで game_of_life.coffee ファイルを開いてください。リスト 2 を見るとわかるように、このファイル内にあるのは 1 つの GameOfLife クラスと、このクラスの複数の属性だけです。

リスト 2. GameOfLife クラスとその属性
class GameOfLife
  currentCellGeneration: null
  cellSize: 7
  numberOfRows: 50
  numberOfColumns: 50
  seedProbability: 0.5
  tickLength: 100
  canvas: null
  drawingContext: null

変数とメソッドには一目瞭然の名前が付けられているので、ソース・コードの内容は簡単に理解できるはずです。リスト 2 の内容を以下にまとめます。

  • コンウェイのライフ・ゲームは 2 次元のセルのグリッドで構成されるため、サンプルでも同じようなものが必要です。そこで、currentCellGeneration 属性にすべてのセルを 2 次元配列で格納します。
  • cellSize は、単一のセルの幅と高さを指定します。この例では、7 ピクセルに指定しています。
  • numberOfRows 属性と numberOfColumns 属性は、グリッドのサイズを決定します。
  • コンウェイのライフ・ゲームには、セルの初期パターンが必要です。この初期パターンは、シードとも呼ばれます。シードを作成するときには、seedProbability 属性を使用して、セルが死んでいるか、生きているかを判別します。
  • tickLength 属性は、ゲームの更新間隔を指定します。この例では、ゲームは 100 ミリ秒ごとに更新されます。
  • canvas 属性は、この後作成する canvas 要素を保存します。
  • Canvas 上にグラフィックスを描画するには、描画コンテキストが必要です。このコンテキストは、drawingContext 属性に格納されます。

ゲームをセットアップする役目を果たすのは、GameOfLife クラスのコンストラクターです。リスト 3 に示されているように、まず Canvas を作成してから、Canvas を正しいサイズに変更します。Canvas を作成してそのサイズを変更した後は、新しく作成した Canvas を使って描画コンテキストを作成します。その後、初期シード・パターンを作成して、最初のティックを開始することで、ゲーム・ループを開始することができます。けれどもその前に、まずは Canvas を作成しましょう。

リスト 3. GameOfLife クラスのコンストラクター
  constructor: ->
    @createCanvas()
    @resizeCanvas()
    @createDrawingContext()

    @seed()

    @tick()

最近のブラウザーには、DOM (Document Object Model) を操作するための優れた API がすでに用意されているため、手の込んだものを作成する必要はなく、jQuery などの外部フレームワークを使う必要もありません。document.createElement メソッドを使用して、新しい canvas 要素を作成し、その要素を同じ名前の属性に格納した上で、新しく作成した要素をページの body の最後に追加します。このすべての処理を createCanvas メソッド内で行います (リスト 4 を参照)。

リスト 4. canvas 要素のセットアップ
  createCanvas: ->
    @canvas = document.createElement 'canvas'
    document.body.appendChild @canvas

  resizeCanvas: ->
    @canvas.height = @cellSize * @numberOfRows
    @canvas.width = @cellSize * @numberOfColumns

  createDrawingContext: ->
    @drawingContext = @canvas.getContext '2d'

resizeCanvas メソッドは、cellSizenumberOfRows、および numberOfColumns を使用して、canvas 要素の幅と高さを計算します。リスト 4 に記載されている 3 番目のメソッド createDrawingContext は、Canvas から 2 次元 (2d) コンテキストを取得して、後で使用するために格納します。

以上で説明した 3 つのメソッドの他に、リスト 4 のコンストラクターが呼び出すメソッドには、seedtick の 2 つがあります。これらのメソッドは、このコードのかなり大きな部分を占めるため、以降のセクションで説明します。


初期シード・パターンの作成

コンウェイのライフ・ゲームには、初期シード・パターンが必要です。グリッド上のセルは、初期シード・パターンからティックごとに次の世代へと進化していきます。シードを作成するには、seed メソッドを使用して、グリッド上の各セルの生死をランダムに決定します (リスト 5 を参照)。ネストされた 2 つの for ループによって、グリッド上のすべてのセルにアクセスすることができます。

外側のループは、すべての行をループ処理します。そのために使用するのは、範囲指定と呼ばれる CoffeeScript の機能です。for row in [0...@numberOfRows] に含まれる 3 つのピリオド (.) は、範囲に最後の値が含まれないことを意味します。つまり、例えば numberOfRows の値が 3 だとすると、イテレーター (この例では、row 変数) の範囲は 0 から 2 になります。これにより、2 次元配列 currentCellGeneration を作成することができます。

内側のループは、すべての列をループ処理して、セルごとに新しい seedCell を作成します。それにはまず、現在の行と列を指定して createSeedCell メソッドを呼び出します。このメソッドによって作成されたシード・セルを、currentCellGeneration 内の正しい位置に格納します。

リスト 5. 初期シード・パターン
  seed: ->
    @currentCellGeneration = []

    for row in [0...@numberOfRows]
      @currentCellGeneration[row] = []

      for column in [0...@numberOfColumns]
        seedCell = @createSeedCell row, column
        @currentCellGeneration[row][column] = seedCell

  createSeedCell: (row, column) ->
    isAlive: Math.random() < @seedProbability
    row: row
    column: column

新しいシード・セルを作成するのは簡単です。セルは単純なオブジェクトで、3 つの属性で構成されます。リスト 5createSeedCell メソッドは、引数として渡される row 引数と column 属性を単純にセル・オブジェクトに渡します。すると、isAlive 属性によってセルの生死が決定されます。死んでいるセルまたは生きているセルをランダムに作成するには、Math.random メソッドと seedProbability 属性を利用します。お気付きかもしれませんが、return キーワードを使用する必要はありません。CoffeeScript のメソッドは自動的に最終的な値を返します。


ゲーム・ループ

初期シード・パターンの作成は完了したので、次は、ライフ・ゲームに命を吹き込みます。ここで必要となるのは、現行世代のセルを Canvas に描画して、次の世代に進化させることです。しかも、このすべての処理が一定の間隔で行われるようにしなければなりません。リスト 6 に示すように、この間隔を開始するには、 tick メソッドを呼び出します。リスト 6tick メソッドは、次の 3 つの処理を行います。

  • drawGrid メソッドを呼び出して、現行世代のセルを描画します。
  • 現行世代のセルを次の世代に進化させます。
  • ゲームのループを実行し続けるためのタイムアウトを設定します。

setTimeout メソッドには、2 つの引数を指定します。最初の引数には、呼び出しの対象とするメソッドを指定します。この例では、tick メソッド自体がそのメソッドに該当します。2 番目の引数には、そのメソッドを呼び出すまで待機するミリ秒数を指定します。ゲームのループの速さは、tickLength 属性を使って制御することができます。

tick メソッドとその他すべてのメソッドとの違いにお気付きでしょうか。tick メソッドでは、CoffeeScript の太い矢印 (=>) を使用します。この太い矢印によって、メソッドは現在のコンテキストにバインドされるため、メソッドのコンテキストは常に正しいものとなります。このようにしなければ、タイムアウトは機能しません。

リスト 6. ゲームのループの tick メソッド
  tick: =>
    @drawGrid()
    @evolveCellGeneration()

    setTimeout @tick, @tickLength

グリッドは簡単に描画することができます。リスト 7drawGrid メソッドは、ネストされた 2 つのループを使用してグリッド上の各セルにアクセスし、そのセルを drawCell メソッドに渡します。drawCell メソッドは、cellSize 属性ならびにセルの row 属性と column 属性を使用して、グリッド上でのセルの x と y の位置を計算します。次に、isAlive 属性の値に応じて、セルの塗りつぶしスタイルを設定します。そして最後に Canvas の strokeStyle 属性および fillStyle 属性を設定してから、Canvas の strokeRect メソッドおよび fillRect メソッドを使用してセルを描画します。

リスト 7. グリッドの描画
  drawGrid: ->
    for row in [0...@numberOfRows]
      for column in [0...@numberOfColumns]
        @drawCell @currentCellGeneration[row][column]

  drawCell: (cell) ->
    x = cell.column * @cellSize
    y = cell.row * @cellSize

    if cell.isAlive
      fillStyle = 'rgb(242, 198, 65)'
    else
      fillStyle = 'rgb(38, 38, 38)'

    @drawingContext.strokeStyle = 'rgba(242, 198, 65, 0.1)'
    @drawingContext.strokeRect x, y, @cellSize, @cellSize

    @drawingContext.fillStyle = fillStyle
    @drawingContext.fillRect x, y, @cellSize, @cellSize

現行世代のセルを次の世代に進化させるには、3 つのメソッドを使用します。そのうちの 1 つ、evolveCellGeneration メソッドをリスト 8 に記載します。このメソッドは、seed メソッドと同じように、2 つのネストされたループを使用して、進化後のセルの世代を格納する newCellGeneration という名前の 2 次元配列を作成します。内側のループが evolveCell セルをメソッドに渡すと、次の世代に進化したセルが返されます。次の世代のセルは、newCellGeneration 配列の正しい位置に格納されます。現行世代のすべてのセルの進化が完了した時点で、currentCellGeneration 属性を更新することができます。

リスト 8. 現行世代のセルの進化
evolveCellGeneration: ->
    newCellGeneration = []

    for row in [0...@numberOfRows]
      newCellGeneration[row] = []

      for column in [0...@numberOfColumns]
        evolvedCell = @evolveCell @currentCellGeneration[row][column]
        newCellGeneration[row][column] = evolvedCell

    @currentCellGeneration = newCellGeneration

リスト 9 に記載する evolveCell メソッドは、まず初めに、渡されたセルと同じ属性を持つ evolveCell 変数を作成します。セルが死滅するか、再生するか、または生き延びるかを決定するには、隣接するセルのうち、いくつのセルが生きているかを知らなければなりません。この数を取得するために、当該セルを指定して countAliveNeighbors メソッドを呼び出します。すると、このメソッドが、隣接するセルのうち生きているセルの数をカウントして、その結果を返します。

生きている隣接セルの数がわかったら、後はライフ・ゲームのルールを適用して、次の世代のセルの isAlive 属性を更新することができます。この属性を更新した後は、単に evolvedCell オブジェクトを返すだけです。

リスト 9. 単一のセルの進化
  evolveCell: (cell) ->
    evolvedCell =
      row: cell.row
      column: cell.column
      isAlive: cell.isAlive

    numberOfAliveNeighbors = @countAliveNeighbors cell

    if cell.isAlive or numberOfAliveNeighbors is 3
      evolvedCell.isAlive = 1 < numberOfAliveNeighbors < 4

    evolvedCell

続いてリスト 10countAliveNeighbors メソッドが、単一のセルを引数として取り、生きているセルの数を返します。一般に、グリッド上の各セルには 8 つの隣接するセルがあります。ただし、セルがグリッドの端に位置する場合、隣接するセルの数はそれよりも少なくなります。したがって、生きている隣接セルをカウントするタスクは、少し複雑になります。

この問題に簡潔で読みやすいコードで対処するためには、生きている隣接セルを検索する範囲を計算しなければなりません。グリッドの中央にあるセルの場合、検索する範囲を計算するのは簡単です。例えば、行 4、列 5 に位置するセルには、行 3、4、5 の列 4、5、6 にあるセルが隣接します。

行 0、列 0 に位置するセルの場合は別です。この場合、隣接するセルの範囲は、行 0 から 1 の列 0 から 1 になります。行の下限は、セルの行番号から 1 を引いた値になりますが、最小値はゼロです。この計算を実装するには、リスト 10 に記載するように、Math.max メソッドを使用することができます。列の下限を計算する場合も、同じようにします。

上限を計算するには、Math.min メソッドを使用します。セルの行に 1 を足した値が、最後の行インデックスより大きくならないようにしてください。行と列の上限と下限を計算した後は、2 つのネストされたループで該当する範囲内の行と列をループ処理することができます。このサンプルでは、CoffeeScript の暗黙的な範囲演算子を使用して、upperRowBound およびupperColumnBound の値も使用されるようにしています。

対象のセル自体をカウントしないようにするには、内側のループに continue 文を含める必要があります。ループの変数 row および column がセルの属性と一致すると、この文が実行されます。この文に続き、現在アクセスしているセルが生きている場合には、numberOfAliveNeighbors カウンターの値を 1 だけインクリメントします。最後にこのカウンターを返せば、生きている隣接セルのカウントは完了です。

リスト 10. 隣接するセルのうち生きているセルのカウント
  countAliveNeighbors: (cell) ->
    lowerRowBound = Math.max cell.row - 1, 0
    upperRowBound = Math.min cell.row + 1, @numberOfRows - 1
    lowerColumnBound = Math.max cell.column - 1, 0
    upperColumnBound = Math.min cell.column + 1, @numberOfColumns - 1
    numberOfAliveNeighbors = 0

    for row in [lowerRowBound..upperRowBound]
      for column in [lowerColumnBound..upperColumnBound]
        continue if row is cell.row and column is cell.column

        if @currentCellGeneration[row][column].isAlive
          numberOfAliveNeighbors++

    numberOfAliveNeighbors

CoffeeScript はすべてのファイルを独自のクロージャーにラップするため、GameOfLife クラスをエクスポートすれば、このクラスを自身のファイルの外部でも使用することができます。window.GameOfLife = GameOfLife と設定して、GameOfLife 属性を window オブジェクトに追加してください。

以上で終了です!コンウェイのライフ・ゲームのサンプル実装は、これで完成しました。ブラウザーで index.html ファイルを開くと、図 1 に示した独自のライフ・ゲームのバージョンが表示されるはずです。上手く表示されない場合には、自分で作成したバージョンを、著者が作成した完全なソース・コード (「参考文献」を参照) と比較してください。


まとめ

コンウェイのライフ・ゲームは単純なルールを使用した簡単なゲームですが、解決しなければならない微妙な問題がいくつかあります。このことから、新しいプログラミング言語を学ぶため、あるいはスキルを磨くためには、このゲームは最適な例となります。

簡単なメソッドと、一目瞭然の名前が付けられた変数は、コードを読みやすくするだけでなく、ソース・コードを適切な方法で構造化するのにも役立ちます。その点、JavaScript の構文の煩雑さの多くを省略できる CoffeeScript は打って付けの言語です。また、CoffeeScript には生産性を向上させることのできる便利な機能が用意されています。


ダウンロード

内容ファイル名サイズ
Article example source codegame-of-life.zip4KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=832941
ArticleTitle=CoffeeScript と Canvas で作成するコンウェイのライフ・ゲーム
publish-date=09062012