Gem 'Ruby2D' でテトリス

20190428021409
 
Ruby でゲーム制作を意識したグラフィック・ライブラリ 'Ruby2D' でテトリスを作ってみました。完全にオリジナルの実装です。ソースは以下。
Ruby2D を使ったテトリス · GitHub
 
'Ruby2D' については以下で紹介しています。
obelisk.hatenablog.com
ゲームはカーソルキーあるいはゲームパッドで遊べます。ゲームパッドでは、十字キーの左右で移動、下で落下、Aボタンで回転です。最小限度の機能しか実装していないので、あとは勝手に改変してみてください。

Linux Mint 19.2, Ruby 2.6.0 と Windows 8.1, Ruby 2.6.3 で動作確認しました。

ハッシュによる美しいメモ化(Ruby)

qiita.com元ネタはこれです。
 
Ruby のハッシュにはこのようなデフォルトの与え方があります。

h = Hash.new {|hash, key| hash[key] = default}

 
これを利用して、こんな風にフィボナッチ数列をメモ化で計算できます。

fib = Hash.new do |hash, n|
  hash[n] = if n == 0 or n == 1
    n
  else
    hash[n - 1] + hash[n - 2]
  end
end

fib[100]    #=>354224848179261915075

おお、すばらしい…。

ブログ「hp12c」の一問題(Ruby)

またまた Ruby ブログ「hp12c」からの問題(?)です。
melborne.github.io
さて、データ

data = <<EOS
player gameA gameB
Bob    20    56
Ross   68    33
Bob    78    55
Kent   90    15
Alice  84    79
Ross   10    15
Jimmy  80    31
Bob    12    36
Kent   88    43
Kent   12    33
Alice  90    32
Ross   67    77
Alice  56    92
Jimmy  33    88
Jimmy  11    87
EOS

から出力

player  gameA gameB total
Alice   230   203   433
Jimmy   124   206   330
Kent    190   91    281
Ross    145   125   270
Bob     110   147   257

を得よという問題です(totalで降順)。

元ブログでの回答はこちら。

require "csv"

class CSV
  def group_by(&blk)
    Hash[ super.map { |k, v| [k, CSV::Table.new(v)] } ]
  end
end

csv = CSV.new(data, col_sep:' ', headers:true, converters: :numeric, header_converters: :symbol)
scores_by_player = csv.group_by(&:first)
stat = scores_by_player.map do |(_, player), t|
  ab = [:gamea, :gameb].map { |e| t[e].inject(:+) }
  [player, *ab, ab.inject(:+)]
end
puts "%s\t%s\t%s\ttotal" % csv.headers
puts stat.sort_by{ |s| -s.last }.map { |line| "%s\t%d\t%d\t%d" % line }

標準添付ライブラリを使っているわけですね。しかし、メソッドのオーバーライドはさすがにちょっとという気がします。それに、コードが凝りすぎて自分には読みにくい感じ。

ライブラリを使わず、極ふつうに素直にやったらどうなるか、考えてみました。

header, *given = data.each_line.map(&:split)
ga, gb = Hash.new(0), Hash.new(0)
given.each do |name, a, b|
  ga[name] += a.to_i
  gb[name] += b.to_i
end

table = [header + ["total"]] +
        given.map(&:first).uniq.map {|n| [n, ga[n], gb[n], ga[n] + gb[n]]}
        .sort {|a, b| b[3] <=> a[3]}
puts table.map {|p, a, b, t| sprintf "%s\t%s\t%s\t%s", p, a, b, t}

結構めんどうですね。もっとうまくできますかね。

線分の交点(Ruby)

4点 があるとき、線分 の交点を求めるメソッド。
 

require 'matrix'

def cross(x1, y1, x2, y2, x3, y3, x4, y4)
  a = Matrix[[x2 - x1, x3 - x4], [y2 - y1, y3 - y4]]
           .lup.solve([x3 - x1, y3 - y1]) rescue nil
  return nil unless a
  s, t = a[0], a[1]
  f = ((0 <= s and s <= 1) and (0 <= t and t <= 1))
  f ? Vector[x1 + s * (x2 - x1), y1 + s * (y2 - y1)] : nil
end

交点(端点でも OK)があれば Vector で返し、なければ nil を返します。

標準添付ライブラリを使わない場合。Vector の代わりに配列を返します。

def cross(x1, y1, x2, y2, x3, y3, x4, y4)
  l = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3)
  return nil if l.zero?
  vx, vy = x3 - x1, y3 - y1
  s = Rational((y4 - y3) * vx - (x4 - x3) * vy, l)
  t = Rational((y2 - y1) * vx - (x2 - x1) * vy, l)
  f = ((0 <= s and s <= 1) and (0 <= t and t <= 1))
  f ? [x1 + s * (x2 - x1), y1 + s * (y2 - y1)] : nil
end

 
線分でなくて直線の交点の場合は、メソッドでの s, t の範囲のチェックをおこなわなければよいです。

Ruby のローカル変数登録について

以下の Ruby コードを見てほしい。

if false
  a = 1
end
p a    #=>nil

if true
  nil
else
  b = 1
end
p b    #=>nil

c = 1 if false
p c    #=>nil

while false
  d = 1
end
p d    #=>nil

いずれも nil を出力し、「undefined local variable or method」の NameError は出ない。

覚えておくと役に立つかも知れない。

応用編。

$ pry
[1] pry(main)> i = 1 if i.nil?
=> 1
[2] pry(main)> i
=> 1
[3] pry(main)> j = 1 if j == 1
=> nil
[4] pry(main)> j
=> nil

これはたぶん何の役にも立たない。

二次元配列の行(あるいは列)を入れ替える(Ruby)

Julia では簡単とあったので(よく知らない)、Ruby で考えてみた。

まず二次元配列を作る。

$ pry
[1] pry(main)> ary = (1..100).each_slice(10).to_a
=> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
 [21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
 [31, 32, 33, 34, 35, 36, 37, 38, 39, 40],
 [41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
 [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
 [61, 62, 63, 64, 65, 66, 67, 68, 69, 70],
 [71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
 [81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
 [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]]

 
行を入れ替えるのは簡単。3行目と5行目を入れ替えてみる。

[2] pry(main)> ary[2], ary[4] = ary[4], ary[2]
[3] pry(main)> ary
=> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
 [41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
 [31, 32, 33, 34, 35, 36, 37, 38, 39, 40],
 [21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
 [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
 [61, 62, 63, 64, 65, 66, 67, 68, 69, 70],
 [71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
 [81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
 [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]]

 
列を交換するのは少し工夫が要る。さらに3列目と5列目を入れ替えてみる。

[4] pry(main)> ary.each {|a| a[2], a[4] = a[4], a[2]}
[5] pry(main)> ary
=> [[1, 2, 5, 4, 3, 6, 7, 8, 9, 10],
 [11, 12, 15, 14, 13, 16, 17, 18, 19, 20],
 [41, 42, 45, 44, 43, 46, 47, 48, 49, 50],
 [31, 32, 35, 34, 33, 36, 37, 38, 39, 40],
 [21, 22, 25, 24, 23, 26, 27, 28, 29, 30],
 [51, 52, 55, 54, 53, 56, 57, 58, 59, 60],
 [61, 62, 65, 64, 63, 66, 67, 68, 69, 70],
 [71, 72, 75, 74, 73, 76, 77, 78, 79, 80],
 [81, 82, 85, 84, 83, 86, 87, 88, 89, 90],
 [91, 92, 95, 94, 93, 96, 97, 98, 99, 100]]

こんな感じでどうですかね。

ふう、Ruby でも一応できるかな。
 

非破壊的変更で

上の例では元の配列は破壊されてしまう。非破壊的にやるのは、もう少し手間である。それぞれ次のようにやればできる。

行の入れ替え。

exchanged = ary.dup
exchanged[2], exchanged[4] = exchanged[4], exchanged[2]

列の入れ替え。

exchanged = ary.map(&:dup)
exchanged.each {|a| a[2], a[4] = a[4], a[2]}

 
以上で、元の配列 ary は変更されない。 なお、dup を使わず Marshal.load(Marshal.dump(ary)) のように deep copy してもよいが、ここではそこまですることもない。

Ruby と rcairo でベジェ曲線を描いてみる

ベジェ曲線Wikipedia)は滑らかな曲線を描くために使われるものです。いくつかの「制御点」を指定して描きます。計算はそんなにむずかしくなくて、上の Wikipedia の記事で充分わかりますし、ネット上にわかりやすい記事がたくさんあるので検索してみて下さい。

Ruby と 'rcairo' で描いてみた例です。
20190320195050
青い線が制御点を結んだ折れ線で、赤い曲線がベジェ曲線です。

Ruby コード。
bezier_curve.rb

require 'cairo'
require 'matrix'

class BezierCurve
  def initialize(points, step = 0.01)
    @points = points.map {|a| a.class == Array ? Vector[*a] : a}
    @step = step
  end
  
  def calc
    n = @points.size - 1
    Enumerator.new do |y|
      c = [1] + (1..n).map {|k| (n - k + 1..n).inject(&:*) / (1..k).inject(&:*)}
      0.0.step(1.0, @step) do |t|
        j = ->(i) { c[i] * t ** i * (1 - t) ** (n - i) }
        y << @points.map.with_index {|b, i| b * j.(i)}.inject(&:+)
      end
      y << @points.last
    end
  end
end


if __FILE__ == $0
  #画像の大きさ
  W = 300
  
  #cairoの初期設定
  surface = Cairo::ImageSurface.new(W, W)
  context = Cairo::Context.new(surface)
  
  #背景
  context.set_source_color(Cairo::Color.parse("#F1F389"))
  context.rectangle(0, 0, W, W)
  context.fill
  
  #制御点を与える
  points = [[20.0, 280.0], [60.0, 100.0], [200.0, 120.0], [290.0, 230.0]]
  
  #制御点を結ぶ青い線
  context.set_source_color(Cairo::Color::BLUE)
  context.set_line_width(2)
  context.move_to(*points.first)
  points.drop(1).each {|r| context.line_to(*r)}
  context.stroke
  
  #ベジェ曲線(赤色)の描画
  context.set_source_color(Cairo::Color::RED)
  context.move_to(*points.first)
  
  BezierCurve.new(points).calc.each do |r|
    context.line_to(*r.to_a)
  end
  context.stroke
  
  #png画像として出力
  surface.write_to_png("bezier_curve.png")
end

使い方としては、配列 points に制御点を入れて(制御点は配列あるいは Vector クラスで表現します)、BezierCurve.new(points, step).calc で折れ線(step を細かくすればベジェ曲線に見えるわけです)の頂点(Vector クラスで表現されています)を順に与える Enumerator を返します。step は省略されれば 0.01 がデフォルトになります。曲線は t = 0 が開始で t = 1 が終了なので、step は一回に進む t の値を指定します。step = 0.01 ならば曲線が 100分割されるということです。


なお、なめらかな曲線としては「スプライン曲線」というのもあります。下の記事で扱っています。
obelisk.hatenablog.com