🦠

『ジェネラティブ・アート』で学んだセル・オートマトン関連まとめ

2023/11/22に公開

https://www.amazon.co.jp/dp/4861008565

ゲーム・オブ・ライフ

module GOL
  class Cell < CA::Cell
    def new_state_update
      c = neighbors.count(&:state)
      if !@state
        @new_state = c == 3
      else
        @new_state = c == 2 || c == 3
      end
    end
  end
end
  • 非生存点は周囲が3個生存していれば誕生する
  • 生存点は周囲が2個か3個生存していれば生き残れる

簡単に言えば「3で誕生」「2と3で生存」となり Life Wiki では G3/S23 と表記する。

ヴィシュニアク・ヴォート

module VishniacVoight
  class Cell < CA::Cell
    def new_state_update
      c = neighbors.count(&:state)
      if @state
        c += 1
      end
      @new_state = c >= 5
      if c == 4 || c == 5
        @new_state ^= true
      end
    end
  end
end
  • 周囲に自分を足して5以上生きていれば生存する (多数派なら生きる)
  • (すぐに収束させないように) 4 か 5 なら反転する

自分を足すのは合計を奇数(9)にしたいため。牛の模様や地形に似たパターンが生まれる。

ブライアンの脳

module BrianBrain
  class Cell < CA::Cell
    def new_state_update
      case @state
      when :off
        c = neighbors.count { |e| e.state == :fire }
        if c == 2
          @new_state = :fire
        else
          @new_state = :off
        end
      when :fire
        @new_state = :rest
      when :rest
        @new_state = :off
      end
    end

    def brightness
      { off: 0.0, fire: 1.0, rest: 0.4 }[@state]
    end
  end
end
  • 「オフ」→「発火」→「休息」→「オフ」の順で切り替わる
  • オフのとき隣接セルが2つ発火していれば自分も発火する

ここは解説の日本語が難しかった。要約すると「神経や意識の伝達を表現してはいないかもしれないが興味深いパターンを生み出す」ということのようだ。

波 (平均化)

module Wave
  class Cell < CA::Cell
    N = 256

    def initialize(...)
      super
      @old_state = 0
    end

    def new_state_update
      average = neighbors.sum(&:state) / NEIGHBORS.size
      case average
      when 0
        @new_state = N
      when N
        @new_state = 0
      else
        @new_state = (@state + average - @old_state).clamp(0, N)
      end
      @old_state = @state
    end

    def brightness
      @state.fdiv(N)
    end
  end
end

3つのルールのうち重要なのはこれだけ。

  • 「自分 + 隣接セルの平均 - 前回の自分」を 0..N の範囲で補正する

上記だけで波の伝搬が起きるのだから最初に考えた人 (著者?) は天才か。

二つめは、

  • 「隣接セルの平均」が 0 なら N

で、何もないところから波を発生させるためにある。もし、マウスでつついたところに N を書き込むような仕組みが別にあるなら、二つめのルールはなくてもいい。

三つめは、

  • 「隣接セルの平均」が N なら 0

で、これも画面が真っ白になるのを防ぐためだけにあるのでなくてもいい。

最大値 N = 256 について。これをよかれと最大値 N = 1.0 とすると小数のせいで隣接セルの平均の精度がよくなりすぎて「隣接セルの平均が0なら」の判定にひっかからなくなり、画面中央から波が発生しなくなる。また整数のときは平均値を求める際に割り切れなかった小数部が削れていくのが、徐々に波が減退していくのに好都合だったようで、小数にしたところ減退しなくなる。

この場合、隣接セルの平均を floor(2) させたり係数 0.99 を掛けて減退させたり 0 との比較を 0.01 以下にするとか、美しさに欠ける微調整が必要になってくる。

def new_state_update
  average = neighbors.sum(&:state).fdiv(NEIGHBORS.size) * 0.99
  case
  when average <= 0.01
    @new_state = 1.0
  when average >= 1.0
    @new_state = 0.0
  else
    @new_state = (@state + average - @old_state).clamp(0.0, 1.0)
  end
  @old_state = @state
end

N を整数のままにしておく場合でも最大を 256 から 800 にすると、これもまた同様に画面中央から波が発生しなくなる。ただ N を増やせば波の発生が反比例的に減っていくのがわかったので調整したいときは整数で管理した方が簡単そう。

共通コード

開く
ca.rb
require "#{__dir__}/../../物理/ベクトル/base"
Base::Palette[:background] = nil

module CA
  class Cell
    NEIGHBORS = [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]]

    class << self
      def value_to_write
        true
      end
    end

    attr_accessor :state

    def initialize(v)
      @v = v
      @state = state_default(v)
    end

    def new_state_update
      c = neighbors.count(&:state)
      @new_state = @state ? (c == 2 || c == 3) : (c == 3)
    end

    def state_update
      @state = @new_state
    end

    def draw
      Gosu.draw_rect(@v.x, @v.y, 1, 1, color)
    end

    private

    def color
      Gosu::Color.from_hsv(0, 0, brightness)
    end

    def brightness
      if @state.kind_of?(Numeric)
        @state
      else
        @state ? 1.0 : 0.0
      end
    end

    def neighbors
      @neighbors ||= NEIGHBORS.collect { |e|
        pos = @v + V[*e]
        if toroidal
          pos = pos.modulo($app.world_wh)
        else
          if pos.min_element.negative?
            pos = nil
          end
        end
        if pos
          $app.field.dig(*pos)
        end
      }.compact
    end

    def state_default(v)
      [true, false].sample
    end

    def toroidal
      true
    end
  end

  class App < ::Base
    attr_accessor :field

    def show
      reset
      super
    end

    def world_wh
      @world_wh ||= window_wh / cell_wh
    end

    private

    def reset
      @field = Array.new(world_wh.x) do |x|
        Array.new(world_wh.y) do |y|
          cell_class.new(V[x, y])
        end
      end
    end

    def button_down(id)
      super

      if id == Gosu::KB_R
        reset
      end
    end

    def update
      super

      mouse_click_then_write

      @field.each { |e| e.each(&:new_state_update) }
      @field.each { |e| e.each(&:state_update) }
    end

    def mouse_click_then_write
      if button_down?(Gosu::MS_LEFT)
        v = (mouse_v / cell_wh).floor
        @field.dig(*v)&.state = cell_class.value_to_write
      end
    end

    def draw
      super

      Gosu.scale(*cell_wh) do
        @field.each { |e| e.each(&:draw) }
      end

      if ENV["FPS"]
        vputs Gosu.fps
      end
    end

    def cell_class
      Cell
    end

    def cell_wh
      @cell_wh ||= V.splat(cell_px)
    end

    def cell_px
      10
    end

    def window_size_default
      V[800, 600]
    end

    def fps_default
      30
    end
  end
end

if $0 == __FILE__
  CA::App.show
end

Discussion