Ruby/SDL でテトリス

20180915120557
いやこれ、僕が作ったのではないのですよ。Gem 'rubysdl' のサンプルとして付属していたものを、多少改変しただけですが、きれいなコードで勉強になったのでここにメモしておきます。

コードは Gist に上げておきました。
Ruby/SDL でテトリス · GitHub
あと画像ファイル "icon.bmp" が必要ですので、これを使って下さい。これだけで動きます。必要な Gem は上にも書いたとおり 'rubysdl' ですので、$ gem install rubysdl や Bundler などでインストールして下さい。

キー操作はカーソルキーです。やってみればすぐわかります。
Linux Mint 18.3, Ruby 2.5.1 で確認しました。Ruby/SDLLinux へのインストールなどについてはここも参考にしてもらえればよいと思います。


コード・リーディングしていて思ったのですが、適切なクラス名、メソッド名、変数名を付けることは大事ですね。上のコードでは自分に読みやすいようにそのあたりを改変しました。改悪になっていないといいのですが。それから、多少のコメントを書き加えています。Bug fix は一箇所だけありました。これは Ruby 本体の仕様改変に伴うものだと思います。

そうそう、それから誰か、Windows への Ruby/SDL のインストールの仕方をアップデートしてくれないですかね。いまのままだとどうやっていいのかさっぱりわからない。残念ながら、自分でコードを調べてインストールする実力がないので。

ランダムかつ重複しないように文字列を生成する(Ruby)

あることのために必要だったので、複数の文字列をランダムかつ重複しないように生成するメソッドを書いてみました。

こんな感じです。

$ irb
irb(main):001:0> require_relative "generate_random_strings"
=> true
irb(main):002:0> Utils.generate_random_strings(40)
=> ["kk", "cm", "aq", "vf", "zf", "uh", "qv", "pv", "bb", "jp", "td", "ri", "mr",
    "hq", "gy", "pe", "ta", "ot", "ob", "km", "zu", "cz", "sf", "qo", "zt", "uq",
    "tc", "fd", "xq", "ki", "po", "w", "dj", "ks", "mw", "am", "zr", "az", "iy", "gv"]

重複しない 40個の文字列(アルファベット小文字)が生成されて Array で返ります。文字の長さは最小になるようになっているので、長さ 1 と 2 の文字列が入り混じっています。

文字列の長さを指定して呼ぶこともできます。

irb(main):003:0> Utils.generate_random_strings(40, 4)
=> ["bakz", "aipi", "prgo", "cwfw", "qqkv", "lgtt", "neid", "jjjz", "cjst", "tdfd",
    "sguf", "nkqk", "bvpl", "tldk", "qszi", "qfvj", "mnjy", "epsd", "abix", "ldap",
    "lijm", "jqzl", "gclu", "fxxe", "tcxc", "rayu", "rcsn", "aitp", "focj", "ngxd",
    "ouxc", "reze", "svxc", "ppaz", "roeb", "qgdt", "mhdw", "ewap", "fxjb", "mmrx"]

長さ 4 のランダムな文字列が 40個返りました。


コード。
generate_random_strings.rb

module Utils
  def repeated_permutation(a, b)
    a ** b
  end
  
  def generate_random_strings(num, string_length = nil)
    table = [*"a".."z"]
    limit = [0, 26, 702, 18278, 475254, 12356630]
    result = []
    generate_string1 = ->(n, l) {
      st = ""
      l.times do
        a, n = n % 26, n / 26
        st = table[a] + st
      end
      st
    }
    generate_string2 = ->(n) {
      idx = limit.find_index {|i| i > n}
      generate_string1.(n - limit[idx - 1], idx)
    }
        
    if string_length and 26 < string_length
      raise "Given length of strings too big."
    end
    
    num_table = Set.new
    if string_length
      n = Utils.repeated_permutation(26, string_length)
      raise "Given length of strings too small." if n < num
      while num_table.size < num
        num_table << rand(n)
      end
      num_table.each {|i| result << generate_string1.(i, string_length)}
    else
      idx = limit.find_index {|i| i >= num}
      raise "Result Array too big." unless idx
      while num_table.size < num
        num_table << rand(limit[idx])
      end
      num_table.each {|i| result << generate_string2.(i)}
    end
    result
  end
  module_function :repeated_permutation, :generate_random_strings
end

 

Gem 化

Gem 'kaki-utils' に同梱しました。$ gem install kaki-utils や Bundler でインストールできます。

$ bundle exec irb
irb(main):001:0> require 'kaki/utils'
=> true
irb(main):002:0> Utils.generate_random_strings(40)
=> ["qv", "kj", "wf", "ch", "ds", "hp", "ro", "oj", "xa", "dz", "vv", "zz", "fh",
    "rf", "tr", "gw", "cf", "yx", "ep", "pr", "tl", "sn", "ar", "ao", "ij", "pl", "my",
    "gy", "sk", "yk", "to", "hq", "wj", "vf", "jh", "pu", "cg", "gq", "wu", "dx"]

みたいな感じ。

Ruby/Rouge でコードをシンタックスハイライトした HTML と CSS を出力させる

Ruby コードを HTML化して、ついでにシンタックスハイライトもできるようにできないか、やってみました。
20180903012354
画像だとこんな感じです。全体はこういう風です。

RubyGem 'rouge' というものを使いました。インストールはふつうに $ gem install rouge とかBundler でどうぞ。

とりあえず HTML と CSS を吐かせるのにかなり苦労しました。上の HTML, CSS ファイルはこんなコードで出力しています。
rouge_sample.rb

require 'rouge'

source = File.read('oekaki_sample16.rb')
formatter = Rouge::Formatters::HTML.new
lexer = Rouge::Lexers::Ruby.new

html = formatter.format(lexer.lex(source))
css = Rouge::Themes::ThankfulEyes.render(scope: '.highlight')

html1 = <<EOS
<!DOCTYPE html>
<html lang="ja">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Syntax-Highlight</title>
<head>
<link rel="stylesheet" type="text/css" href="rouge_sample1.css">
<style type="text/css">
pre.highlight {
  font-family: Liberation Mono, Consolas, monospace, sans-serif;
  font-size: 86%;
  padding: 10px;
  line-height: 1.1em;
}
</style>
</head>
<body>

<pre class="highlight">
EOS
html = html1 + html + "</pre>\n\n</body>\n</html>"

open("rouge_sample1.html", "w") {|io| io.write(html)}
open("rouge_sample1.css",  "w") {|io| io.write(css)}

色の付け方は Rouge::Themes::ThankfulEyes のところで指定しています。ここでは ThankfulEyes を選んでいますが、Colorful, Github など、いろいろ選べます。詳しくはこちらを見て下さい。
また、表示させるソースの種類はRouge::Lexers::Ruby.new で指定しています。これなら Ruby ですね。他のものはこちらを見て下さい。たいていのものはあるのではないでしょうか。

円落下の JavaScript 版

obelisk.hatenablog.comここの JavaScript 版です。ボタンをクリックしてみて下さい。

コード。

<script type="text/javascript">
function circleFall(width, height) {
    var w = window.open("", null, "width=" + width + ",height=" + height);
    w.focus();
    w.document.open();
    var st = `
    <!DOCTYPE html>
    <html lang="ja">
    <title>Canvas</title>
    <body style="margin: 0; overflow: hidden;">
    <canvas id="circleFallCanvas" width="${width}" height="${height}"></canvas>
    </body>
    </html>
`;
    w.document.write(st);
    w.document.close();
    
    var canvas = w.document.getElementById("circleFallCanvas");
    var wd = w.innerWidth; var ht = w.innerHeight;    //Chrome は 0 を返す
    if (wd != 0) {width  = wd;}
    if (ht != 0) {height = ht;}
    canvas.width  = width;
    canvas.height = height;
    var context = canvas.getContext("2d");
    
    var max_r, min_r, colorMax, maxNum;
    max_r = 40; min_r = 10;
    colorMax = 256;
    maxNum = 60;
    
    var Circle = function(f) {
        function rnd(num) {return Math.random() * num;}
        
        this.maxR = rnd(max_r - min_r) + min_r;
        this.x = rnd(width);
        this.color = "rgb(" + String(rnd(colorMax)) + "," + String(rnd(colorMax)) +
                          "," + String(rnd(colorMax)) + ")";
        this.fallStep = rnd(3) + 1;
        this.r = 1;
        this.r_step = rnd(0.8) + 0.2;
        if (f) {
            this.y = rnd(height);
        } else {
            this.y = -rnd(max_r);
        }
        
        this.paint = function() {
            context.beginPath();
            context.fillStyle = this.color;
            context.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
            context.fill();
            this.y += this.fallStep;
            this.r += this.r_step;
            if (this.r > this.maxR || this.r < 1) {this.r_step *= -1}
            if (this.y > height + max_r) {return true}
            return false;
        }
    }
    
    var circles = [];
    for (var i = 0; i < maxNum; i++) {circles[i] = new Circle(true);}
    
    window.draw = function() {
        context.fillStyle = "black";
        context.fillRect(0, 0, width, height);
        
        for (var i = 0; i < maxNum; i++) {
            if (circles[i].paint()) {circles[i] = new Circle(false);}
        }
    }
    
    var id = setInterval("draw()", 80);
    function bul() {clearInterval(id);}
    w.onbeforeunload = bul;
}
</script>
<form><input type="button" value="アニメーション" onclick="circleFall(1000, 700)">
 <input type="button" value="(小さい版)" onclick="circleFall(800, 600)"></form>

Ruby で関数型プログラミングっぽく(コピペ) + Haskell 版

parrot.hatenadiary.jpここのブログ記事を読んで感銘を受けました。だからこれを読んでもらえればよいのですが、せっかくなのでコピペしておきます。元記事に感謝です。

まずは問題。

ある数字にそれを逆に並べた数字を足すという計算を、
回文数(上から読んでも下から読んでも同じ数)になるまで繰り返すとき、
もっとも計算回数を要する二桁の数を答えなさい
 
例:ab+ba=123の場合、123+321=444で回文数なので、2回となる

https://parrot.hatenadiary.jp/entry/20110302/1299051431

 
Ruby コードです。多少自己流に書き直しました。

reverse_num = ->(num) {num.to_s.reverse.to_i}
is_palindrome = ->(num) {num == reverse_num.(num)}
execute = ->(count, num) {
  r = num + reverse_num.(num)
  is_palindrome.(r) ? count : execute.(count + 1, r)
}

result = (10..99).map{|n| [n, execute.(1, n)]}
max_count = result.map(&:last).max
result.select{|r| r.last == max_count}.each{|r| puts "num:#{r.first} count:#{r.last}"}

結果。89 と 98 ですね。24回繰り返しています。

$ time ruby palindrome_num.rb
num:89 count:24
num:98 count:24

real	0m0.091s
user	0m0.076s
sys	0m0.012s

 
いや、すばらしい。
 

Haskell

頑張って Scala ならぬ Haskell で同様のことをやってみました。
palindrome_num.hs

reverseNum :: Int -> Int
reverseNum num = read $ reverse $ show num

isPalindrome :: Int -> Bool
isPalindrome num = (num == reverseNum num)

execute :: Int -> Int -> Int
execute count num = if isPalindrome r
                    then count
                    else execute (count + 1) r
                        where r = num + reverseNum num
                        
main :: IO ()
main = putStr $ unlines $ map toS $ [r | r <- result, last r == maxCount]
           where result = [[n, execute 1 n] | n <- [10..99]]
                 maxCount = maximum $ map last result
                 toS [a, b] = "num:" ++ show a ++ " count:" ++ show b

こんなのでいいのかな。
結果。

$ time ./palindrome_num
num:89 count:24
num:98 count:24

real	0m0.003s
user	0m0.000s
sys	0m0.000s

当然のことながら瞬殺ですな。

しかし、Ruby でも結構関数型っぽく書けますね。ほとんどそのまま Haskell になるじゃん。自分は Ruby で慣れているので、Ruby の方が見やすいくらいだ。以下の記事もよろしければどうぞ。
obelisk.hatenablog.com

 

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者:Miran Lipovača
  • 発売日: 2012/05/23
  • メディア: 単行本(ソフトカバー)
 

追記(2020/12/4)

Ruby 版後半があまり美しくないので、少し凝ってみる。

module Enumerable
  def max_select
    pool = []
    max_num = -Float::INFINITY
    each do |i|
      n = yield(i)
      if n > max_num
        max_num = n
        pool = [i]
      elsif n == max_num
        pool << i
      end
    end
    [max_num, pool]
  end
end

Enumerable#max_selectは、ブロックの返り値の最大値(max_num)を求めて、そのような最大値になるようなものをレシーバーからコレクトし(pool)、[max_num, pool]を返すメソッド。例えばこんな風に使える。ローマ字化した県名のうち、もっとも文字数が長いもの。

url = "https://gist.githubusercontent.com/koseki/38926/raw/671d5279db1e5cb2c137465e22424c6ba27f4524/todouhuken.txt"
prefectures = URI.open(url).each_line.map {|l| l.chomp.split.last}
prefectures.max_select(&:size)
#=>[9, ["fukushima", "yamanashi", "hiroshima", "yamaguchi", "tokushima", "kagoshima"]]

9文字が最大値だとわかる。

まあ、他に使いみちがあるかどうかわからないものだが(笑)、これを使って以下のように。

reverse_num = ->(num) {num.to_s.reverse.to_i}
is_palindrome = ->(num) {num == reverse_num.(num)}
execute = ->(num, count = 1) {
  r = num + reverse_num.(num)
  is_palindrome.(r) ? count : execute.(r, count + 1)
}

count, numbers = (10..99).max_select(&execute)
puts "max count: #{count}"
puts "number: #{numbers}"

ちょっと関数型プログラミングっぽいかも知れない笑。

Ruby/SDL を使ってみる

これまで Ruby で画面に線を引いたり円を描いたりするのに、自作の Gem 'oekaki' を使ってきましたが、その中では Gem 'gtk2' を使っていて、たかがお絵かきに GTK+ を使うのは大袈裟すぎるようにも思われたので、Ruby/SDL を使ってみることにしました。Linux MInt 18.3, Ruby 2.5.1 で確認しています。(SDL と SGE が入れば、WindowsMac でも動く筈です。)


まずは SDL のインストールが必要です。Linux では apt-get で入ります。libsdl2-2.0 は既に入っているかもしれません。

sudo apt-get install libsdl2-2.0 libsdl-sge-dev

お絵かきをするには特に SGE(SDL Graphics Extension)が必要です。SDL 2.0 の日本語リファレンスはここにあります。


Ruby/SDL をインストール。gem install rubysdl でよいです。もちろん Bundler で入れてもかまいません。Ruby/SDL のリファレンス・マニュアルはここにあります。簡単なサンプルはこちらもどうぞ。


サンプルとして、Gem 'oekaki' で描いた(参照)のと同様のデモを載せておきます。複数の円が拡大縮小しながら落下していきます。
20180825005956
コード。簡単なライブラリのようなもの。
sdl_draw.rb

require 'sdl'

def draw(width, height, &blk)
  SDL.init(SDL::INIT_VIDEO)
  screen = SDL::Screen.open(width, height, 16, SDL::SWSURFACE)
  SDL::WM::set_caption("SDL", "")
  
  class << screen
    def color(r, g, b)
      format.map_rgb(r, g, b)
    end
  end
  
  Thread.new {screen.instance_eval(&blk)}
  
  loop do
    while (event = SDL::Event.poll)
      case event
      when SDL::Event::Quit
        exit
      end
    end
    sleep 0.2
  end
end

 
サンプル。
sdl_sample3.rb

require_relative 'sdl_draw'

Width, Height = (ARGV.size == 2) ? ARGV.map(&:to_i) : [1000, 700]
Max_r, Min_r = 40, 10
ColorMax = 256
MaxNum = 60

class Circle
  def initialize(ob)
    @slot = ob
    renewal
    @y = rand(Height)
  end
  
  def renewal
    @max_r = rand(Min_r..Max_r)
    @x =  rand(Width)
    @y = -rand(@max_r)
    @color = @slot.color(rand(ColorMax), rand(ColorMax), rand(ColorMax))
    @fall_step = rand(1..4)
    @r = 1
    @r_step = rand(0.2..1.0)
  end

  def paint
    @slot.draw_circle(@x, @y, @r, @color, true, true)
    @y += @fall_step
    @r += @r_step
    @r_step *= -1 if @r > @max_r or @r < 1
    renewal if @y > Height + Max_r
  end
end


draw(Width, Height) do
  circles = Array.new(MaxNum) { Circle.new(self) }
  black = color(0, 0, 0)
   
  loop do
    fill_rect(0, 0, Width, Height, black)
    circles.each(&:paint)
    flip
    sleep(0.08)
  end
end

Ruby/SDL、シンプルでよいですね。これからも使ってみたいです。
 

Gem 'oekaki' 版の動画です。こんな感じ。

 

追記(10/22)

実行時に

uninitialized constant SDL::Mixer (NameError)

のエラーが出る場合は、

$ sudo apt-get install libsdl-mixer1.2 libsdl-mixer1.2-dev

でライブラリを入れてから、Gem を再インストールしてみて下さい。