🦠
『ジェネラティブ・アート』で学んだセル・オートマトン関連まとめ
ゲーム・オブ・ライフ
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