WebSDL - Ruby/SDL × WebSocket/Canvas

投稿者 nanki 2010-06-23 02:42:00 GMT

Ruby勉強会@関西44の懇親会にて @cyross, @KazkiMatz さんらとSDLをWebSocketと組み合わせて…という話をしていておもしろそうだったので実装してみた。

websdl
青は@nanki, 赤は@ujm

github - WebSDL

  1. Ruby/SDLの画面描画を全て文字列としてシリアライズし、WebSocketで送信、Canvasに描画する。
  2. ブラウザ上で発生したイベントをWebSocketで送信、SDLのイベントモデルの中に還元してやる。

というのが基本的な仕組み。

# samples/sample1.rb
require 'rubygems'
gem 'rubysdl'
require 'sdl'
require 'websdl'

class TestFrame < Frame
  attr_reader :mx, :my, :color

  def initialize
    @color = 0xff000000 | (1..3).inject(0){|r,i|r << 8 | rand(0xff)}
    @mx = -100
    @my = -100
  end

  def open_screen
    SDL::Screen.open(640, 480, 32, SDL::HWSURFACE || SDL::DOUBLEBUF)
  end

  def mainloop(screen)
    screen.fill_rect(0, 0, screen.w, screen.h, 0)

    while event = SDL::Event.poll
      @mx, @my = event.x, event.y if SDL::Event::MouseMotion === event
    end

    frames.each do |f|
      screen.fill_rect(f.mx - 15, f.my - 15, 30, 30, f.color)
    end
    
    screen.flip
  end
end


SDL = WebSDL if ARGV.first == 'web'

SDL.init(SDL::INIT_VIDEO)
SDL.run(TestFrame, :host => '0.0.0.0', :port => 3000)

上記のようなコードで、

$ rsdl -Ilib samples/sample1.rb

ではSDLのウィンドウが起動し、

$ ruby -Ilib samples/sample1.rb web

でWebSDL版が起動するようになっている。 WebSDL版はsamples/test.html をWebSocket, Canvasに対応したブラウザで開く。

ただCanvasを使うだけでは芸が無いので、マルチユーザに対応、ユーザ間の情報共有にも対応してみた。

言うまでもなくProof of Conceptの段階であり、SDLの機能もdraw_rect, MouseMotion くらいしか実装していない。

また、0.1秒毎に更新しているのがかっこわるいが、適切なイベントに基づいた処理をすれば、パズルゲームやアドベンチャーゲームなど頻繁に画面更新のないものなら実用的なものができる可能性がある。 ウェブへぇボタンとかね。

問題点

一方、SDLのAPIは同期的なものもあり、SDL::Surface#get_pixel (画面上のピクセルの色を返す) などは現在の方法では実現が困難である。

参考:

RubyでIMAPを使ってGMailにアクセスする

投稿者 nanki 2010-05-20 19:29:00 GMT

だいたい以下のような感じになる。

password_from_keychainはOSXのkeychainからパスワードを取ってくるだけの関数。

GMailのTwitterフォルダ(follow通知メールが溜まる)の中から未読(UNSEEN)のメールを探してきて、ユーザ名とおぼしき部分を抜き出して出力、既読にする。

require 'net/imap'

# for Mac
def password_from_keychain(server, user)
  require 'open3'
  _, _, err = Open3.popen3("/usr/bin/security find-internet-password -s #{server} -a #{user} -r imap -g")

  if /^password: "(.*)"$/ === err.gets
    $1
  else
    nil
  end
end

SERVER = 'imap.gmail.com'
USER = 'nanki@example.com'
imap = Net::IMAP.new(SERVER, 993, true)

imap.login(USER, password_from_keychain(SERVER, USER))
imap.select("Twitter")

imap.search(%w(UNSEEN)).each do |msg|
  body = imap.fetch(msg, "BODY[TEXT]")[0].attr["BODY[TEXT]"]

  if /twitter.com\/([^?\/]+)/ === body
    puts "f #{$1}"
    imap.store(msg, "+FLAGS", [:Seen])
  end
end

Ruby1.8, Ruby1.9のIOの挙動の違い

投稿者 nanki 2010-04-28 04:47:00 GMT

表にまとめてみた。 主な違いは、getc/readcharがbyteを返すだけみたいだ。

getchar/getlineがあってほしい((# getcは数値を返して欲しい))

get - nil at EOFread - EOFError at EOFenumeach
(char) 1.8 N/A N/A chars each_char
1.9 getc readchar chars each_char
(byte) 1.8 getbyte, getc readbyte, readchar bytes each_byte
1.9 getbyte readbyte bytes each_byte
(line) 1.8 gets readline lines each_line, each
1.9 gets readline lines each_line, each

termtterのパスワードにMacのキーチェーンを使う

投稿者 nanki 2009-12-19 18:15:00 GMT

termtter では、パスワードをファイルに保存したくない、という理由から?デフォルトでは毎回パスワードを聞くようになっている。

Macではsecurityコマンドというのでキーチェーンにアクセスできるそうなので、それを使ってみる。

標準エラー出力にパスワードを吐くのでpopen3を使う。

# ~/.termtter/config
def password_from_keychain(user)
  require 'open3'
  _, _, err = Open3.popen3("/usr/bin/security find-internet-password -s twitter.com -a #{user} -r http -g")

  if /^password: "(.*)"$/ === err.gets                                                  
    $1
  else
    nil
  end
end
   
config.user_name = 'your_account'
config.password = password_from_keychain(config.user_name)
参考:

Try maglev!

投稿者 nanki 2009-11-25 21:42:00 GMT

噂のMagLevを試してみた。以下 Mac OS X 10.5.8 での話。

⍟ git clone git://github.com/MagLev/maglev.git # clone repository
⍟ cd maglev
⍟ ./instsall.sh # download and install GemStone

⍟ vim .zshrc
export MAGLEV_HOME=/PATH_TO/maglev
export PATH=$MAGLEV_HOME/bin:$PATH

⍟ rake maglev:start
⍟ maglev-ruby -e "p 1 + 1"
2
⍟ maglev-irb
error , Expected nil to be a Boolean.,
          during /..../maglev/bin/maglev-irb
ERROR 2085, Expected nil to be a Boolean. (RuntimeError)

maglev-irb は動かない><

参考:

やはりRubyでは 〜Scalaの無名関数に憧れて〜

投稿者 nanki 2009-10-29 06:18:00 GMT

憧れシリーズ第二弾。

Scalaの勉強でもしようかな、と思ってScalaの本を開いたら、Scalaでは無名関数を_(アンダースコア)を使って定義できるらしい。 Rubyにおけるそれの実現可能性に考えが行ってしまって、Scalaの本を読むのはそれでおしまいになったのだ…

Rubyでも、

[1, 2, 3].map(_ * 2) # => [2, 4, 6]

とか書きたいよね。

Rubyにはもともと、mapなどのブロックを取るメソッドに無名関数(Proc)を引数として渡すことができる

[1, 2, 3].map(&:to_s)

この例では、:to_s は Symbol だが、Proc を返すSymbol#to_proc を定義しておくと、それが呼ばれてよしなにしてくれる。Symbol#to_proc はActiveSupport などで定義されているので、Rails使いにはおなじみだろう。

この仕組みを利用して、 _ に対して送られるメッセージを保存しておいて、to_proc される時に適当なProcを返すような何かを作ればよい。

module Scala
  class LazyCall
    instance_methods.each do |v|
      undef_method(v) unless %w(__id__ __send__).include?(v)
    end

    def initialize(v = nil)
      @v = v
      @msgs = []
    end

    def method_missing(*args, &block)
      @msgs << [args, block] unless args == [:respond_to?, :to_proc]
      self
    end

    def to_proc
      proc do |v|
        @msgs.inject(@v||v) do |r, m|
          r.__send__(*m[0].map{|a|LazyCall === a ? a.to_proc[v] : a}, &m[1])
        end
      end
    end
  end

  def _(v = nil)
    LazyCall.new v
  end

  # _0, _1, ...
  (0...3).each do |i|
    define_method("_#{i}"){LazyCall.new[i]}
  end
end

include Scala

そうしてできたのがこれ。

これを使うと…

%w(a b c d).map(&_ * _.to_i(16))
# => ["aaaaaaaaaa", "bbbbbbbbbbb", "cccccccccccc", "ddddddddddddd"]

[[1, 2], [2, 3]].map(&_0 + _1)
# => [3, 5]

_(Math).sqrt(_0 ** 2 + _1 ** 2).to_proc[[3, 4]]
# => 5.0

& の後に括弧が要ると思っていたがそんなことはなかったぜ。

メッセージをフックする関係上、直接 Math.sqrt(_0 ** 2 + _1 ** 2) とは書けないけど、そう書きたい人はブロック使ってください。

以下簡単な説明

  • _ はLazyCallのインスタンスを返し、受け取ったメッセージを引数と共に保存。
  • [:respond_to?, :to_proc] をはじいているのは、引数として渡される時にこの問い合わせが発生するので、それを無視するため。(副作用として、無名関数では.respond_to? :to_proc が使えなくなると思う)
  • to_proc内で、保存したメッセージを、procの引数に送り直している。
  • 保存された引数の中に、LazyCallのインスタンスがある場合は、call してその結果と置き換えている。これによって、_ * _ のような表記が可能になる。
  • _0は、_.[0]のエイリアス

二夜にわたるProxyObject特集、いかがでしたでしょうか。さて、来週からは…

追記:2009/10/30

Scalaの _ (プレースホルダ構文と呼ぶらしい) は _ + _と書いたとき、二つの _ が同じものを意味しないそうです。

と、閉じた本の続きに書いてあった。

あと、_0 などを使うと、20倍くらい遅い。どうしたものか。


たぶんRubyでは 〜 Maybeモナドに憧れて 〜

投稿者 nanki 2009-10-28 14:25:00 GMT

HaskellにはMaybeモナドというのがあって、エラー処理をかなり適当な感じに書けてとても便利そう。

一方、多くのプログラミング言語では、

request.mobile && request.mobile.docomo?

File.open('example.txt').read rescue nil # これはすこし横着

こういった記述を頻繁に使う必要があり、なんとかしたい。

そこで、

class Never
  instance_methods.each do |v|
    undef_method(v) unless %w(__id__ __send__).include?(v)
  end

  def method_missing(*args)
    self
  end

  def end
    nil
  end
end

class Maybe < Never
  def initialize(value)
    @value = value
  end

  def method_missing(*args, &block)
    Maybe.new @value.__send__(*args, &block)
  rescue Exception
    Never.new
  end

  def end
    @value
  end
end

class Object
  def maybe
    Maybe.new(block_given? ? yield : self)
  rescue Exception
    Never.new
  end
end

上のようなコードを実行すると、

request.mobile.maybe.docomo?.end # => true/false or nil

File.maybe.open("file_does_not_exist.txt").read.end # => nil

こういう風に書く事ができるようになって幸せ。 途中のどこかで失敗すると、単にnilが返る。

このように、包んだオブジェクトのフリをするオブジェクトを使う手法はRailsでは多用されていて、読んでみると割と楽しい。ProxyObjectとか呼ぶらしい。

class Maybe < Never のあたりが、

子「お父さん、運動会絶対見に来てね」
父「絶対行くぞ」

一週間後

子「お父さん、運動会絶対見に来てね」
父「たぶん…」

というような会話を想像してしまって、とても人間的。

参考:

今日のgolf - るびまゴルフ第七回

投稿者 nanki 2009-09-14 15:02:00 GMT

月に一度のgolf. というか、人がやっているのを目にすると、手を出したくなってくる。
たぶん、子供がプラモデル組み立てているのを見て「ちょっとお父さんにかしてみなさい」というタイプ。

結局、前回の最短は23Bだったらしい。変数名まで同じだった :)
文字数を削るという共通の目的が、変数名から意味すら奪うのだろうか。

20Bという噂はなんだったんだろう…

反省を生かして、今回は、27Bまで縮んだところで寝かせておく。

続きにあるのはお気に入りの答えだが、パー。

追記: 2009/09/17 いくつかの方針で使っている方法を組み合わせたら23Bになった。驚き。

参考:

chm.rbのバグ?

投稿者 nanki 2009-08-11 22:54:00 GMT

とあるchmファイルをWindowsで見ていて気がついたのだが、一つのキーワードに複数のドキュメントが割り当てられている場合がある。

同じファイルを、Macで見ると、最初のコンテンツしか表示されない。

調べてみると、chmlib のRuby bindingである chm が原因らしい。

そもそも、chmのインデックスは下記のような形式のドキュメントになっていて、chmの中の、chm.rbでこれをパースしているのだが、キーワードが複数のドキュメントに対応する場合が想定されていないようだ。

…
<li><object type="text/sitemap">
  <param name="Name" value="animal">
  <param name="Name" value="dog">
  <param name="Local" value="dog_doc">
  <param name="Name" value="cat">
  <param name="Local" value="cat_doc">
</object>
…

普通の感覚ではこれを見ても一番上のキーワードが下の方まで有効だと思えないのだが…

あと、目次(Topic)を読むところが、case-sensitiveになっていたので、目次が取れていなかった。

コミットしてから気がついたけど、ゴミが…

参考:

今日のgolf - るびまゴルフ

投稿者 nanki 2009-07-02 23:14:00 GMT

久々にgolf。 問題はコチラ.

標準入力から、3,5 という入力を受け取って、3 から5 までの数字を出力するコード。

まず素直(?)に書いて、

p *eval(gets.split(',').join('..')) # 35B

順等に縮めると

p *eval(gets.sub',','..') # 25B

ここからは手強いので寝かせて。

eval'A,B='+gets;p *A..B # 23B

A, Bの重複がとても気になる…

ちょっとずるいけど、$ ruby -n を使っていいと、

p *eval(sub',','..') # 20B

文字列のRangeを使う方法もやってみたけど、挙動に問題があるのでやめ。

風の噂では、20Bまでいける、と聞いたんだけど…がんばります。

参考:

Rubyist Magazine - るびまゴルフ 【第 6 回】