Gosu + Chipmunk で遊ぶ(Ruby)


物理エンジン Chipmunk + Gosu でごく簡単なドミノ倒しみたいな動画を作ってみました。Ruby 2.3.3, Linux Mint 18.3 で確認しました。

gosu_chipmunk_sample3.rb

require 'gosu'
require 'rmagick'
require 'chipmunk'

CV = CP::Vec2

Width, Height = 500, 200
Wall_h = 50
BOD_w, BOD_h = 20, 100

class MyWindow < Gosu::Window
  def initialize
    super Width, Height, false
    self.caption = "Gosu + Chipmunk"
    
    @space = CP::Space.new
    @space.iterations = 3
    @space.gravity = CV.new(0, 100)
    
    background = Magick::Image.new(Width, Height) {self.background_color = "snow"}
    
    #地面
    gc = Magick::Draw.new
    gc.fill('firebrick')
    gc.rectangle(0, Height - Wall_h, Width, Height)
    gc.draw(background)
    
    #板
    board_image = Magick::Image.new(BOD_w, BOD_h)
    gc = Magick::Draw.new
    gc.fill('lightgreen')
    gc.rectangle(0, 0, BOD_w, BOD_h)
    gc.draw(board_image)
    @board = Gosu::Image.new(board_image)
    
    #円
    circle_image = Magick::Image.new(11, 11) {self.background_color = 'transparent'}
    gc = Magick::Draw.new
    gc.fill('skyblue')
    gc.circle(5, 5, 0, 5)
    gc.draw(circle_image)
    @circle = Gosu::Image.new(circle_image)
    
    #chipmunk
    #地面
    sb = CP::Body.new_static
    x1, y1 = 0, Height - Wall_h
    x2, y2 = Width, Height
    verts = [CV.new(x1, y1), CV.new(x1, y2), CV.new(x2, y2), CV.new(x2, y1)]
    wshape = CP::Shape::Poly.new(sb, verts, CV.new(0, 0))
    wshape.e = 1
    wshape.u = 1
    @space.add_shape(wshape)
    
    #板
    set_boards(5)
    
    #円
    @bodyc = CP::Body.new(7, CP::INFINITY)
    @bodyc.p = CV.new(0, 20)
    @bodyc.v = CV.new(50, 0)
    shape = CP::Shape::Circle.new(@bodyc, 5, CV.new(0, 0))
    shape.e = 0.8
    shape.u = 0
    @space.add_body(@bodyc)
    @space.add_shape(shape)
    
    @background_image = Gosu::Image.new(background)
  end
  
  def set_boards(n)
    @boards = []
    n.times do |i|
      x, y = BOD_w / 2.0, BOD_h / 2.0
      bx = [CV.new(-x, -y), CV.new(-x, y), CV.new(x, y), CV.new(x, -y)]
      body = CP::Body.new(10, CP.moment_for_poly(10, bx, CV.new(0, 0)))
      body.p = CV.new(50 + 80 * i, Height - Wall_h - y)
      shape = CP::Shape::Poly.new(body, bx, CV.new(0, 0))
      shape.e = 0
      shape.u = 1
      @space.add_body(body)
      @space.add_shape(shape)
      
      @boards << body
    end
  end
  
  def update
    @space.step(1.0 / 60)
  end
  
  def draw
    @background_image.draw(0, 0, 0)
    @boards.each do |b|
      @board.draw_rot(b.p.x, b.p.y, 2, (b.a - Math::PI / 2).radians_to_gosu)
    end
    @circle.draw_rot(@bodyc.p.x, @bodyc.p.y, 1, 0)
  end
end

MyWindow.new.show

パラメータの調節がむずかしかったですね。それぞれの質量の値は重要です。長方形はあんまり軽すぎると不安定になったりします。


GTK+ で落書き 14(Ruby)


自作の Gem 'oekaki' を使っています。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura
 

require 'oekaki'

circles = []
check = lambda do |x, y, r|
  return false if x < r or y < r or x > 500 - r or y > 500 - r
  circles.each do |x1, y1, r1|
    l = Math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2)
    return false if l < r + r1
  end
  circles << [x, y, r]
  true
end

Oekaki.app width: 500, height: 500 do
  draw do
    clear(color(0xad * 256, 0xd8 * 256, 0xe6 * 256))    #lightblue
  end
  
  timer(50) do
    x, y = rand(500), rand(500)
    r = rand(10..100)
    color(rand(65535), rand(65535), rand(65535))
    circle(true, x, y, r) if check.call(x, y, r)
    true
  end
end

つまらないものなのですけれど、じつは Kernel#rand の引数に Range オブジェクトを使ってみたかっただけなのでした…。一箇所だけ、わかりますかね。

「シェルピンスキーのギャスケット」を描いてみる(Ruby)

「シェルピンスキーのギャスケット」を Ruby で描いてみました。

 
フラクタルな図形なので正確ではないですが、7次まで描いてみました。描画には自作の Gem 'oekaki' を使っています。
oekaki | RubyGems.org | your community gem host
GTK+でお絵かきしてみた(Ruby) - Camera Obscura
 
コードです。
sierpinski_gasket.rb (Gist)

require 'oekaki'

Width, Height = 500, 450

Oekaki.app width: Width, height: Height, title: "Sierpinski gasket" do
  draw do
    clear
    
    x1, y1 = Width / 2, Height - Width * 0.5 * Math.sqrt(3)
    x2, y2 = 0, Height
    x3, y3 = Width, Height
    po0 = [[x1, y1], [x2, y2], [x3, y3]]
    
    color(0xff * 256, 0xd7 * 256, 0)    #gold
    polygon(true, po0)
    
    color(0, 0, 0)
    
    sg = lambda do |n, po|
      y = (po[0][1] + po[1][1]) / 2.0
      l = (po[2][0] - po[1][0]) / 2.0
      p1 = [po[1][0] + l / 2, y]
      p2 = [po[2][0] - l / 2, y]
      p3 = [po[1][0] + l, po[1][1]]
      
      polygon(true, [p1, p2, p3])
      return if n == 1
      
      sg.call(n - 1, [po[0], p1, p2])
      sg.call(n - 1, [p1, po[1], p3])
      sg.call(n - 1, [p2, p3, po[2]])
    end
    
    sg.call(7, po0)
  end
end

lambda の再帰を使って反復を実装しています。

指定したディレクトリ以下から再帰的にファイルをランダムに取ってくる


 
へー、Ruby ではどうやるかなとちょっと考えてみた。

require 'find'

def get_filenames_randomly(directory)
  Find.find(directory).select {|x| File.file?(x)}.shuffle.to_enum
end

get_filenames_randomly("/usr/lib").take(10).each {|f| puts f}

こんな感じ? Ruby だから Enumerator を返すことにする。実質一行だな。

結果。

/usr/lib/python2.7/dist-packages/twisted/protocols/portforward.py
/usr/lib/x86_64-linux-gnu/libgtk-3.so
/usr/lib/python2.7/dist-packages/reportlab/lib/enums.py
/usr/lib/x86_64-linux-gnu/libicule.so.55.1
/usr/lib/ruby/2.3.0/rdoc/markup/attributes.rb
/usr/lib/x86_64-linux-gnu/vlc/plugins/codec/libsvcdsub_plugin.so
/usr/lib/python2.7/dist-packages/twisted/protocols/mice/__init__.pyc
/usr/lib/x86_64-linux-gnu/wine/dbghelp.dll.so
/usr/lib/python3.5/lib2to3/pgen2/__pycache__/parse.cpython-35.pyc
/usr/lib/python2.7/dist-packages/dbus/service.pyc

 
ちなみに

enum = get_filenames_randomly("/usr/lib")
10.times {puts enum.next}

でもほぼ同じ。(10回に満たない場合の挙動がちがう。)

ハノイの塔をプログラミングで解く


ハノイの塔とは一種のパズルで、ルールは以下のようです。

  • 3本の杭と、中央に穴の開いた大きさの異なる複数の円盤から構成される。
  • 最初はすべての円盤が左端の杭に小さいものが上になるように順に積み重ねられている。
  • 円盤を一回に一枚ずつどれかの杭に移動させることができるが、小さな円盤の上に大きな円盤を乗せることはできない。
https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%8E%E3%82%A4%E3%81%AE%E5%A1%94

 
再帰プログラミングでとても簡単に解けることを知りました。Ruby で解いてみます。

考え方はこうです。三つの場所を A, B, C とします。A に n 枚の円盤があるとき、

  1. n - 1 枚の円盤を A から C に移します。
  2. A に残った 1枚を B に移します。
  3. n - 1 枚の円盤を C から B に移します。

ただし、n が 1 のときはただ A から B へ移せばよい。

これを素直にプログラミングするだけです。

def hanoi(n, from, to, via)
  if n == 1
    puts "#{from} から #{to} へ移す"
  else
    hanoi(n - 1, from, via, to)
    puts "#{from} から #{to} へ移す"
    hanoi(n - 1, via, to, from)
  end
end

hanoi(3, :A, :B, :C)

結果。円盤が 3枚の場合です。

A から B へ移す
A から C へ移す
B から C へ移す
A から B へ移す
C から A へ移す
C から B へ移す
A から B へ移す

これですべて A から B に移ります。特に n を大きくしてみると、こんな単純な再帰で解が求められるのが不思議な感じがします。
 
なお、これは以下の記事の LISP コードを Ruby に置き換えただけのことです。元記事に感謝します。

物理エンジン Chipmunk を Gosu で表示(Ruby)


2D物理エンジン「Chipmunk」を Ruby から使ってみました。表示には Gem 'gosu' を使っています。Linux Mint 18.3, Ruby 2.3.3 で確認しました。
 
gosu_chipmunk_sample2.rb (Gist)

require 'gosu'
require 'rmagick'
require 'chipmunk'

Width, Height = 600, 600
Wall_h = 20

class MyWindow < Gosu::Window
  def initialize
    super Width, Height, false
    self.caption = "Gosu + Chipmunk"
    
    @space = CP::Space.new
    @space.gravity = CP::Vec2.new(0, 250)
    
    #円の画像の生成
    circle_image = Magick::Image.new(21, 21) {self.background_color = 'transparent'}
    gc = Magick::Draw.new
    gc.fill('yellow')
    gc.circle(10, 10, 0, 10)
    gc.draw(circle_image)
    @circle = Gosu::Image.new(circle_image)

    #壁の生成
    background = Magick::Image.new(Width, Height) {self.background_color = "black"}
    create_wall(0, Height - Wall_h, Width, Height - Wall_h, Wall_h, background)
    create_wall(Wall_h, 0, Wall_h, Height, Wall_h, background)
    create_wall(Width, 0, Width, Height, Wall_h, background)
    create_wall(120, 150, 400, 200, Wall_h, background)
    create_wall(200, 450, 480, 400, Wall_h, background)
    @background_image = Gosu::Image.new(background)
    
    @circles = []
  end
  
  def create_circle
    body = CP::Body.new(1, CP::INFINITY)
    body.p = CP::Vec2.new(rand(Width / 2 + Width / 4), -20)
    body.v = CP::Vec2.new(rand * 500 - 250, 0)
    shape = CP::Shape::Circle.new(body, 10, CP::Vec2.new(0, 0))
    shape.e = 0.8
    shape.u = 0
    
    @space.add_body(body)
    @space.add_shape(shape)
    
    body
  end
  
  def create_wall(x1, y1, x2, y2, height, bg)
    sb = CP::Body.new_static
    p1 = CP::Vec2.new(x1, y1)
    p4 = CP::Vec2.new(x2, y2)
    p0 = p4 - p1
    p2 = p1 + p0.normalize.rotate(CP::Vec2.new(0, 1)) * height
    p3 = p2 + p0
    verts = [p1, p2, p3, p4]
    wshape = CP::Shape::Poly.new(sb, verts, CP::Vec2.new(0, 0))
    wshape.e = 1
    wshape.u = 0
    @space.add_shape(wshape)
    
    gc = Magick::Draw.new
    gc.fill('firebrick')
    verts = verts.map {|vt| [vt.x, vt.y]}.flatten
    gc.polygon(*verts)
    gc.draw(bg)
  end
  
  def update
    @space.step(1.0 / 60)
    @circles << create_circle if rand < 0.1 and @circles.size < 300
  end
  
  def draw
    @background_image.draw(0, 0, 0)
    @circles.each {|c| @circle.draw_rot(c.p.x, c.p.y, 1, 0)}
  end
end

MyWindow.new.show

Chipmunk で計算したものを Gosu を使って表示しています。画像の生成には RMagick を使っています。
メソッド create_wall(x1, y1, x2, y2, height, bg) の (x1, y1), (x2, y2) は壁の上面のラインを指定します。height は壁の厚みです。

おもしろいことに、上のコードで行数を取っているのはほとんどが円や壁の設定です。計算は Chipmunk が、描画は Gosu が自動的にやってくれるため、メインループ(メソッド update, draw)は数行しかありません。

全体の構造も単純です。Chipmunk の空間は @space = CP::Space.new であり、@space.gravity で重力を発生させます。円の本体は CP::Body.new で生成し、壁は動かないので CP::Body.new_static で生成します。@space.stepメソッドで時間を進めます。
Gosu はまず Gosu::Window を継承した MyWindow クラスを作り、そのインスタンスshow すれば基本的に描画されます。メインループ内のロジックは update, 描画は drawメソッド内に書きます。こうするとデフォルトで 1/60 秒ごとに描画が繰り返されます。

Ruby の強力な「オブジェクト指向プログラミング」のおかげで、じつに簡単に物理エンジンが動かせかつ可視化できていると思います。


Gosu + Chipmunk + RMagick については以下に簡単なまとめとサンプルを書いておきました。

Chipmunk については下のサイトがすごくわかりやすかったです。

美しい?IPアドレス(Ruby)

アルゴリズム・パズルです。

問題。

IPアドレスは例えば 192.168.1.2 のように表せますね。これは2進数で32ビットの数字を、8ビットずつに区切ってさらに10進数で表した形式になっています。
ここで、0~9 のすべての数字が 1度ずつ現れたような IPアドレスで(数字の先頭の 0 は勘定に入れません)、32ビットの2進数でこれを表すと(これは先頭の 0 を含め、必ず32ビットの形で考えます)ビットの並びが左右対称になるようなものは、全部で何個あるでしょうか?

 
Ruby で解いてみます。
q41a.rb

def check(str)
  10.times do |i|
    n = str.count(i.to_s)
    return false unless n == 1 
  end
  true
end

store = []
0.upto(0b1111111111111111) do |i|
  bi = "%016b" % i
  bi += bi.reverse
  str = [bi[0, 8], bi[8, 8], bi[16, 8], bi[24, 8]].map {|x| ("0b" + x).to_i(0)}.join(".")
  store << str if check(str)
end
puts store.size
p store

結果。

$ time ruby q41a.rb
8
["34.179.205.68", "34.205.179.68", "68.179.205.34", "68.205.179.34",
 "179.34.68.205", "179.68.34.205", "205.34.68.179", "205.68.34.179"]

real	0m0.432s
user	0m0.428s
sys	0m0.000s

最初は10進数で 0~9 の数字を並び替えて、それから 2進数に直して判定するという方法でやってみましたが、遅くて使い物になりませんでした。なので、逆にまず 2進数で左右対称になるものを作って、10進数で判定してやる方法でやりました。こちらの方が圧倒的に速いですね。