読者です 読者をやめる 読者になる 読者になる

ux00ff

ビールとプログラミングと

Scalaでコンウェイのライフゲーム with Processing

Scala Processing LifeGame

定番ということで、書いてみました。ライフゲームについては Wikipedia の記事がわかりやすいので参照してください。本当に Better Java (というか普通に手続き的) な書き方ができてしまうところに面白みというか味を感じます。昔はライフゲーム書くときは二次元配列を多用していたけど、最近は一次元配列の方が自然に思える。ふしぎ。

f:id:ux00ff:20170212125812p:plain

フレームレートを明示的に指定することで世代交代をちゃんと眺めることができます。普通に実行すると一瞬で安定するので面白くありません。

シンプル版

import processing.core.PApplet
import processing.core.PConstants._

class App extends PApplet {
  private[this] val CELL_SIZE = 10
  private[this] val W = 50
  private[this] val H = 50
  private[this] val arr = new Array[Boolean](W * H)
  private[this] val tmp_arr = new Array[Boolean](W * H)

  override def settings(): Unit = {
    size(W * CELL_SIZE, H * CELL_SIZE, JAVA2D)
  }

  override def setup(): Unit = {
    frameRate(5)
    fill(60)
  }

  override def mousePressed(): Unit = {
    for (x <- 0 to W * H - 1) {
      arr(x) = if (random(1.0f) < 0.5f) {
        true
      } else {
        false
      }
    }
  }

  override def draw(): Unit = {
    background(200)
    write_lines()
    update_cells()
    write_cells()
  }

  private[this] def write_lines() = {
    for (x <- 0 to W * CELL_SIZE by CELL_SIZE) line(x, 0, x, height)
    for (y <- 0 to H * CELL_SIZE by CELL_SIZE) line(0, y, width, y)
  }

  private[this] def update_cells(): Unit = {
    for (i <- 0 to W * H - 1) {
      var c = 0
      // 左側
      if (i % W > 0 && arr(i - 1)) {
        c += 1
      }

      // 右側
      if (i % W != W - 1 && arr(i + 1)) {
        c += 1
      }

      // 上、右上、左上
      if (i / W > 0) {
        if (arr(i - W)) c += 1
        if (i % W != W - 1 && arr(i + 1 - W)) c += 1
        if (i % W > 0 && arr(i - 1 - W)) c += 1
      }

      // 下、左下、右下
      if (i / W < H - 1) {
        if (arr(i + W)) c += 1
        if (i % W != W - 1 && arr(i + 1 + W)) c += 1
        if (i % W > 0 && arr(i - 1 + W)) c += 1
      }

      // 生存判定
      if (arr(i) && (c == 2 || c == 3)) {
        tmp_arr(i) = true
      } else if (arr(i)) {
        tmp_arr(i) = false
      } else if (c == 3) {
        tmp_arr(i) = true
      } else {
        tmp_arr(i) = false
      }
    }

    tmp_arr.copyToArray(arr)
  }

  private[this] def write_cells(): Unit = {
    for (i <- 0 to W * H - 1) {
      val y = i / W
      val x = i % W
      if (arr(i)) {
        rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
      }
    }
  }
}

こいつをScalaっぽく書き直すのはまたこんど。

キー入力を受けてその文字を初期データとする

これを参考にして、キー入力を受け付け、入力された文字の形状をライフゲームの初期データとして利用するようにします。具体的には、以下のコードを適当な箇所に追加します。

  private[this] val FONT_SIZE = 76
  private[this] var letter = ""
  lazy private[this] val letterGraphic: PGraphics = {
    createGraphics(W, H)
  }
  lazy private[this] val font: PFont = {
    createFont("ArialBold", FONT_SIZE)
  }
  
  override def keyPressed(): Unit = {
    if (key != CODED) {
      letter = PApplet.str(key)
      updateLetter()
    }
  }

  private def updateLetter(): Unit = {
    letterGraphic.beginDraw()
    letterGraphic.noStroke()
    letterGraphic.background(255, 255)
    letterGraphic.fill(0)
    letterGraphic.textFont(font, FONT_SIZE)
    letterGraphic.textAlign(CENTER, TOP)
    letterGraphic.text(letter, W / 2, 0)
    letterGraphic.endDraw()
    letterGraphic.loadPixels()
    for (i <- 0 to W * H - 1) {
      val y = i / W
      val x = i % W
      val c = letterGraphic.pixels(x + y * W)
      if (brightness(c) < 255) {
        arr(i) = if (random(1.0f) < 0.5f) {
          true
        } else {
          false
        }
      } else {
        arr(i) = false
      }
    }
  }

実行結果はこんな感じになります。ここではランダム性をもたせていますが、ランダムにしない方が面白いかもしれません。「このフォントだとEが一番生存性が高い」とか「Qでやると宇宙船のようなものが生まれる」など楽しめます。

f:id:ux00ff:20170212145016p:plain