Gem 'Ruby2D' でテトリス

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

青い線が制御点を結んだ折れ線で、赤い曲線がベジェ曲線です。
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