コンウェイのライフゲームを Ruby で実装してみた
有名な「コンウェイのライフゲーム」を素朴に実装してみました。一種の生態系シミュレーションですね。ルールは簡単です。「セル」が集まって長方形領域を形成しているとき、それぞれの「セル」はまわりの 8つの「セル」の状態によって「生きる」か「死ぬ」かします。
- 「セル」がないとき
- まわりにちょうど 3つの「セル」があれば「誕生」します。
- 「セル」があるとき
- まわりに 0 または 1個の「セル」があれば過疎で「死に」ます。
- まわりに 2 または 3個の「セル」があればそのまま「生き」ます。
- まわりに 4個以上の「セル」があれば、過密で「死に」ます。
このパターンを繰り返します。

パターンはランダムに与えられ、毎回自動的に lifemap.txt に(上書き)記録されます。-r オプションをつけて実行すると、開始パターンを lifemap.txt からロードして実行します。描画には GTK+ を使っています。
ソースは以下です。Gist。
require 'gtk2' Width = 30 Height = 30 class Component def initialize f = Field.new(Width, Height) f.generate(150) #ランダムに150個セットする if ARGV[0] == "-r" File.open("lifemap.txt") {|io| f = Marshal.load(io)} else File.open("lifemap.txt", "w") {|io| Marshal.dump(f, io)} end @window = Gui.new(f) @disp = FieldDisplay.new(f) end def main @disp.show end end class Field def initialize(width, height) @width = width @height = height @field = Array.new(@width + 2) {Array.new(@height + 2, 0)} end attr_reader :width, :height def set(x, y) @field[x][y] = 1 end def reset(x, y) @field[x][y] = 0 end def get(x, y) @field[x][y] end def generate(n) n.times {set(rand(@width) + 1, rand(@height) + 1)} end #次の世代を得る def next nxf = Array.new(@width + 2) {Array.new(@height + 2, 0)} @width.times do |x| @height.times do |y| x1, y1 = x + 1, y + 1 n = alive_cells(x1, y1) if get(x1, y1).zero? nxf[x1][y1] = 1 if n == 3 else nxf[x1][y1] = 1 if n == 2 or n == 3 end end end @field = nxf end def alive_cells(x, y) get(x - 1, y - 1) + get(x, y - 1) + get(x + 1, y - 1) + get(x - 1, y) + get(x + 1, y) + get(x - 1, y + 1) + get(x, y + 1) + get(x + 1, y + 1) end end class Gui CellWidth = 10 Space = 3 Margin = 8 def initialize(f) @f = f @w = Gtk::Window.new @width = CellWidth * @f.width + Space * (@f.width - 1) + 2 * Margin @height = CellWidth * @f.height + Space * (@f.height - 1) + 2 * Margin @w.set_size_request(@width, @height) @w.set_app_paintable(true) @w.realize @drawable = @w.window @gc = Gdk::GC.new(@drawable) colormap = Gdk::Colormap.system @white = Gdk::Color.new(65535, 65535, 65535) @black = Gdk::Color.new(0, 0, 0) colormap.alloc_color(@white, false, true) colormap.alloc_color(@black, false, true) end end class FieldDisplay < Gui def initialize(f) super(f) end def show @w.signal_connect("expose_event") do @gc.set_foreground(@black) @drawable.draw_rectangle(@gc, true, 0, 0, @width, @height) end Gtk.timeout_add(160) do @f.width.times do |x| @f.height.times do |y| if @f.get(x + 1, y + 1).zero? reset(x + 1, y + 1) else set(x + 1, y + 1) end end end @f.next end @w.signal_connect("destroy") {Gtk.main_quit} @w.show Gtk.main end def set(x, y) @gc.set_foreground(@white) draw_cell(x, y) end def reset(x, y) @gc.set_foreground(@black) draw_cell(x, y) end def draw_cell(x, y) x1 = Margin + (x - 1) * CellWidth + (x - 1) * Space y1 = Margin + (y - 1) * CellWidth + (y - 1) * Space @drawable.draw_rectangle(@gc, true, x1, y1, CellWidth, CellWidth) end end Component.new.main
※追記(2018/1/18)
コンソール版も作ってみました。
※追記(2018/4/12)
フィールド・エディタ付きのを作ってみました。
GTK+で落書き(Ruby)
GTK+ でアニメーションしてみました。エンターキーの入力で終了します。簡易ライブラリ mygtk.rb についてはここを参照してください。
circle_fall.rb
require_relative 'mygtk' L = 400 MyGtk.app width: L, height: L, title: "Circle Fall" do timer(10) do show = proc do |y| x = rand(L - 10) color(rand(65535), rand(65535), rand(65535)) arc(true, x, y, 10, 10, 0, 64 * 360) end show.call(0) if rand < 0.15 show.call(rand(L - 10)) if rand < 0.05 img = get_pic(0, 0, L, L) show_pic(img, 0, 1) color(0, 0, 0) rectangle(true, 0, 0, L, 1) end key_in do |w, e| Gtk.main_quit if e.keyval == Gdk::Keyval::GDK_Return end draw do color(0, 0, 0) rectangle(true, 0, 0, L, L) end end
これ、どこかでメモリリークしているようです。
RubyGem 'Gosu'
Ruby の Gem で色いろ遊んでみたいと思うのだけれど、「gem おすすめ」などで検索しても Rails の Gem しか出てきません。誰かおもしろい Gem を教えてくれないですかね。だからというわけではないですが、ちょっと探してみたところ、ゲーム作成用の Gem で「Gosu」というのを見つけました。かなり有名な Gem みたいですね。ゲームに特に興味はないのですが、ちょっとインストールしてみました。Windows, Mac, Linux で動作するようです。Linux Mint 18 で確認しました。
Hello • Gosu
公式サイトはこれです。インストールは、僕は Bundler でインストールしました。ついでに、gem 'gosu-examples' もインストールしておくとよいでしょう。僕の環境では $ bundle exec gosu-examples --fullscreen でデモが立ち上がります。

サンプル・プログラムを見てみると、かなりすごいですね。こんなことが Ruby で出来るのか。
The Japanese translation of https://github.com/jlnr/gosu/wiki/Ruby-Tutorial (GosuのRubyチュートリアルの日本語訳) · GitHub
チュートリアルの日本語訳を作った方がおられるので、リンクしておきます。
GTK+でヒルベルト曲線(Ruby)
前記事の Ruby/GTK+ 用のモジュールを使って、ヒルベルト曲線を描いてみました。Ruby/Tk 版(参照)と殆ど同じです。

require './mygtk' class Draw def initialize(n, slot) @lgth = Width / 2 ** n @y = (Width - @lgth * (2 ** n - 1)) / 2 #見栄えを整えているだけで、特に意味のない計算 @x = Width - @y @oldx, @oldy = @x, @y @slot = slot end def ldr(n) return if n == 0 dlu(n - 1); @x -= @lgth; dline ldr(n - 1); @y += @lgth; dline ldr(n - 1); @x += @lgth; dline urd(n - 1) end def urd(n) return if n == 0 rul(n - 1); @y -= @lgth; dline urd(n - 1); @x += @lgth; dline urd(n - 1); @y += @lgth; dline ldr(n - 1) end def rul(n) return if n == 0 urd(n - 1); @x += @lgth; dline rul(n - 1); @y -= @lgth; dline rul(n - 1); @x -= @lgth; dline dlu(n - 1) end def dlu(n) return if n == 0 ldr(n - 1); @y += @lgth; dline dlu(n - 1); @x -= @lgth; dline dlu(n - 1); @y -= @lgth; dline rul(n - 1) end def dline firebrick = @slot.color(45568, 8704, 8404) @slot.line(@oldx, @oldy, @x, @y, firebrick) @oldx, @oldy = @x, @y end end Width = 400 MyGtk.app width: Width, height: Width, title: "Hilbert curve" do draw do ghostwhite = color(63488, 63488, 65535) rectangle(true, 0, 0, Width, Width, ghostwhite) n = (ARGV[0] || 5).to_i #次数を引数に。デフォルトは5次 exit if n < 1 Draw.new(n, self).ldr(n) end end
MyGtk.app のブロックの外へメソッドを持ち出したければ、self を持ち出してインスタンスにすれば可能です。Drawクラスでは @slot にブロック内の self が入っています。
GTK+でお絵かきしてみた(Ruby)
(後記:2018/9/22) Gem の使い方のリファレンスを作りました。
Gem 'oekaki' リファレンス
Ruby だけだと遊びで線を引く程度のことも簡単でないので、Ruby で GTK+ を使ってシンプルにお絵かきするだけのモジュールを書いてみました。できるのは、点を打つ、線を引く、長方形を描く、円(楕円、弧)を描く、多角形を描く、文字の表示、画像の取得と表示、画像ファイルの読み込みと書き出し、キー入力、マウスクリック、そんなことくらいですか。
Gem 'gtk2' が必要です。
(後注:このモジュールを RubyGem 'oekaki' として公開しました。参照。MyGtk.app の代わりに Oekaki.app とするだけで、あとはすべて同じです。)
円を書いてみます。

require_relative 'mygtk' MyGtk.app width: 300, height: 300 do #ウィンドウの大きさは 300×300 draw do white = color(65535, 65535, 65535) red = color(65535, 0, 0) rectangle(true, 0, 0, 300, 300, white) #画面全体を白く塗りつぶす arc(true, 0, 0, 300, 300, 0, 64 * 360, red) #赤い円を描く end end
モジュール 'mygtk' を require_relative しています。下のモジュール 'mygtk.rb' をカレントディレクトリに保存して下さい。以下、require_relative は同じなので書きません。
描画する部分を drawメソッドのブロックに入れます。ウィンドウの大きさの変更などで再描画される際にも呼び出されます。
rectangle は長方形、arc は円を描きます。第一引数の true は中を埋めて描くという意味です。(0, 0, 300, 300) は図形の左上隅の座標と横縦の大きさです。arc の (0, 64 * 360) は円弧の描画を開始する角度と終了する角度で、1°の 64分の1 が単位です。
線が移動していくアニメーションです。

MyGtk.app width:300, height: 300, title: :lines do r = 0 id = Gtk.timeout_add(40) do color(0, 65535, 0) line(r, 0, 299 - r, 299) #線を描く Gtk.timeout_remove(id) if r >= 300 r += 1 end draw do color(65535, 65535, 65535) rectangle(true, 0, 0, 300, 300) end end
Gtk.timeout_add(interval) を直接使ってアニメーションにしています(interval は 1/1000秒単位です)。
画像ファイルから画像を読み込んで表示し、その上に点の雪を降らせるアニメーションです。エンターキーの入力で終了します(参照)。

MyGtk.app width: 300, height: 300 do timer(5) do color(65535, 65535, 65535) point(rand(300), rand(300)) end key_in do |w, e| #キー入力 Gtk.main_quit if e.keyval == Gdk::Keyval::GDK_Return end draw do color(65535, 65535, 65535) rectangle(true, 0, 0, 300, 300) img = load_pic("import_codes/cairo_sample2.png") #画像ファイルの読み込み show_pic(img, 0, 0) #読み込んだ画像の表示 end end
timer(interval) メソッドは interval の間隔でブロック内を繰り返し呼び出します(内部で Gtk.timeout_add を使っています)。ここでは点の雪を降らせています。
多角形と文字列を表示します。png ファイルとして出力もしています。

MyGtk.app width:300, height: 300 do draw do white = color(65535, 65535, 65535) red = color(65535, 0, 0) blue = color(0, 0, 65535) rectangle(true, 0, 0, 300, 300, white) ar = [] 50.times {ar << [rand(300), rand(300)]} #線の数は50本(配列に座標を入れる) polygon(true, ar, blue) #多角形を描く text("Polygon", 180, 260, 20 * 1000, red) #文字列を描く img = get_pic(0, 0, 300, 300) #画像の取り込み save_pic(img, "sample.png") #画像ファイルに保存 end end
print の 20 * 1000 はフォントの大きさです。指定の意味は GTK+ の仕様そのままです。
最後にモジュール本体のコードを置いておきます。
mygtk.rb
require 'gtk2' require 'matrix' include Math module MyGtk W = Gtk::Window.new class Tool def initialize @window = W @drawable = W.window @gc = Gdk::GC.new(@drawable) @colormap = Gdk::Colormap.system @color = Gdk::Color.new(0, 0, 0) @fontdesc = Pango::FontDescription.new @width, @height = 0, 0 end attr_reader :window attr_accessor :width, :height def color(r, g, b) @color = Gdk::Color.new(r, g, b) @colormap.alloc_color(@color, false, true) @color end def rectangle(fill, x, y, width, height, color = nil) set_color(color) @drawable.draw_rectangle(@gc, fill, x, y, width, height) end def arc(fill, x, y, width, height, d1, d2, color = nil) set_color(color) @drawable.draw_arc(@gc, fill, x, y, width, height, d1, d2) end def circle(fill, x, y, r, color = nil) arc(fill, x - r, y - r, 2 * r, 2 * r, 0, 64 * 360, color) end def point(x, y, color = nil) set_color(color) @drawable.draw_point(@gc, x, y) end def line(x1, y1, x2, y2, color = nil) set_color(color) @drawable.draw_lines(@gc, [[x1, y1], [x2, y2]]) end def lines(array, color = nil) set_color(color) @drawable.draw_lines(@gc, array) end def polygon(fill, array, color = nil) set_color(color) @drawable.draw_polygon(@gc, fill, array) end def text(str, x, y, size, color = nil) set_color(color) @fontdesc.set_size(size) layout = Pango::Layout.new(W.pango_context) layout.font_description = @fontdesc layout.text = str @drawable.draw_layout(@gc, x, y, layout) end def set_color(color) @color = color if color @gc.set_foreground(@color) end private :set_color def load_pic(filename) GdkPixbuf::Pixbuf.new(file: filename) end def save_pic(img, filename, type = "png") img.save(filename, type) end def show_pic(img, x, y) @drawable.draw_pixbuf(@gc, img, 0, 0, x, y, img.width, img.height, Gdk::RGB::DITHER_NONE, 0, 0) end def get_pic(x, y, width, height) GdkPixbuf::Pixbuf.from_drawable(nil, @drawable, x, y, width, height) end def timer_stop(id) Gtk.timeout_remove(id) end def star(fill, x1, y1, x2, y2, color = nil) set_color(color) Star.new(fill, x1, y1, x2, y2, @color).draw end def clear(color = nil) color ||= Gdk::Color.new(0, 0, 0) set_color(color) rectangle(true, 0, 0, @width, @height) end def get_window_size W.size end end class Event < Tool def initialize super end def draw(&bk) W.signal_connect("expose_event", &bk) end def timer(interval, &bk) Gtk.timeout_add(interval, &bk) end def key_in(&bk) W.signal_connect("key_press_event", &bk) end def mouse_button(&bk) W.add_events(Gdk::Event::BUTTON_PRESS_MASK) W.signal_connect("button_press_event", &bk) end def make_window(&bk) w = Gtk::Window.new w.instance_eval(&bk) w.show_all w end def window_changed(&bk) W.signal_connect("configure_event") do @width, @height = get_window_size yield end end end class Star < Tool module Add refine Vector do def to_w(o) v = self [o[0] + v[0], o[1] - v[1]] end end end using Add def initialize(fill, x1, y1, x2, y2, color) @fill = fill @o = []; @a = []; @b = [] @o[0], @o[1] = x1, y1 @a[0] = Vector[x2 - x1, y1 - y2] θ = PI / 5 rt1 = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]] rt2 = rt1 * rt1 1.upto(4) {|i| @a[i] = rt2 * @a[i - 1]} t = cos(2 * θ) / cos(θ) @b[0] = rt1 * @a[0] * t 1.upto(4) {|i| @b[i] = rt2 * @b[i - 1]} super() @color = color end def draw_triangle(n) ar = [@a[n].to_w(@o), @b[n].to_w(@o), @b[(n - 1) % 5].to_w(@o)] polygon(@fill, ar) end private :draw_triangle def draw if @fill 5.times {|i| draw_triangle(i)} ar = [] 5.times {|i| ar << @b[i].to_w(@o)} polygon(@fill, ar) else ar = [] 5.times {|i| ar << @a[i].to_w(@o); ar << @b[i].to_w(@o)} polygon(@fill, ar) end end end class Turtle < Tool def initialize super @pen = Tool.new @pen_po = Vector[0, 0] @dir = Vector[1, 0] @color_t = [65535, 65535, 65535] @width, @height = @pen.get_window_size end attr_accessor :pen_po, :dir def left(deg) θ = PI * deg / 180 @dir = Matrix[[cos(θ), -sin(θ)], [sin(θ), cos(θ)]] * @dir end def right(deg) left(-deg) end def forward(length, draw = true) next_po = @pen_po + @dir * length if draw @pen.color(*@color_t) @pen.line(@width / 2 + next_po[0], @height / 2 - next_po[1], @width / 2 + @pen_po[0], @height / 2 - @pen_po[1]) end @pen_po = next_po end def back(length) forward(-length, false) end def color(r, g, b) @color_t = [r, g, b] @pen.color(*@color_t) end def circle(radius, fill = false) @pen.color(*@color_t) @pen.circle(fill, @width / 2 + @pen_po[0], @height / 2 - @pen_po[1], radius) end def move(x, y) @pen_po = Vector[x, y] end end def self.app(width: 300, height: 300, title: "gtk", resizable: false, &bk) W.title = title W.set_size_request(width, height) W.set_resizable(resizable) W.set_app_paintable(true) W.realize e = Event.new e.width, e.height = width, height e.clear e.instance_eval(&bk) W.signal_connect("destroy") {Gtk.main_quit} W.show_all Gtk.main end end class Gtk::Window def button(&bk) b = Gtk::Button.new b.instance_eval(&bk) b end end
注意すべきは、draw や timer などの取るブロックは必ず true を返すようにして下さい。わかりにくいバグを引き起こすことがあります。
※参考
Ruby-GNOME2 Project Website - Ruby-GNOME2 Project Website
gnuplot で陰関数の表示(Ruby)
Python の matplotlib の plot_implicit による陰関数の描画がどうも信用ならないので、gnuplot でやってみることを考えた。しかし gnuplot 自体には陰関数を直接描画する機能はないので、工夫が必要となる。
gnuplot で陰関数を描く
基本的には上サイトでやっているように、3次元描画をして高さ 0 の等高線を描くという手段をとる。こんな感じになる。

描画した関数はひとつ前の記事と同じである。
Ruby コードは以下。なお、Gem は 'gnuplot' ではなく 'numo/gnuplot'を使った。この Gem の利用例はここに詳しい。
require 'bundler/setup' require 'numo/gnuplot' Numo.gnuplot do set "contour" #等高線の描画を指定 set "cntrparam levels discrete 0" #指定された高さの点に等高線を引く set style: "line 1 lw 2" set style: "increment user" set isosamples: [100, 100] #面描画に使用する線の数を設定する(3次元) unset "surface" #網を表示しない unset "ztics" #z軸の目盛の数値を表示しない set border: 15 #枠の設定(15は上下左右に枠を描く) set view: [0, 0] #視点の設定(0,0はz軸方向から見る) set "zeroaxis" #すべての軸の座標軸を表示する unset "key" #関数名を書かない set xrange: -1.1..1.1 set yrange: -1..1.6 splot "x ** 6 + 3 * x ** 4 * y ** 2 + 6 * x ** 4 * y - 2 * x ** 4 + 3 * x ** 2 * y ** 4" + "- 2 * x ** 2 * y ** 3 - 6 * x ** 2 * y ** 2 - 6 * x ** 2 * y + 3 * x ** 2 + y ** 6" + "- 3 * y ** 4 + 3 * y ** 2 - 1" end gets
どうも等高線の色を指定するのがうまくいかない。バグだろうか?
3次元で描いてみるとこんな感じ。

2抜きを許したストラックアウトの抜き方(Ruby)
| 1 | 2 | 3 |
| 4 | 5 | 6 |
| 7 | 8 | 9 |
ストラックアウトといって 3×3 のボードに玉をぶつけて数字を抜いていく遊びがありますね。その抜き方は何通りあるでしょうといえば、9!(9の階乗)通りとすぐ求められてつまらないので、2抜きを許します。つまり、ひとつの玉で同時に、隣接した 2つを抜くことを許すわけです。ただし、真ん中の 5は同時に抜くことはできません。さて、この場合の抜き方は何通りでしょう。
Ruby で解くとこんな感じです。
A = (1..9).to_a Hits = A.inject([]) {|r, a| r << [a]} + [[1, 2], [2, 3], [3, 6], [6, 9], [9, 8], [8, 7], [7, 4], [4, 1]] #玉が当たれば false、外れれば true を返す def check(rest, hit) hit.each {|i| return true unless rest.include?(i)} false end def throw(rest) return @h[rest] if @h[rest] return 1 if rest.empty? sum = 0 Hits.each do |hit| next if check(rest, hit) sum += throw((rest - hit).sort) end @h[rest] = sum end @h = {} puts throw(A) #=>798000
798000通りと求められます。毎度おなじみの(?)深さ優先探索ですね。いわゆるメモ化をしないと現実的な時間で求められません。
下での模範解答はさらに短いです。すごいなあ。

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問
- 作者: 増井敏克
- 出版社/メーカー: 翔泳社
- 発売日: 2015/10/16
- メディア: Kindle版
- この商品を含むブログ (7件) を見る