Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

Rubyのグラフライブラリ(gnuplot)で砲台ゲームを実装しよう

今日の風景

f:id:nisemono_san:20161013152351j:plain

寿司の電子工作

はじめに

今は廃刊してしまった、昔懐しい『ベーシックマガジン』という雑誌には、大抵簡単なゲームのコードが載っていた。俺を含めたパソコン少年は、そのコードを見ながらパチパチとコードを打っていたわけだが、そういったサンプルコードリストの一つに、大抵は「砲台ゲーム」というものが存在していた。

仕様としては、ある範囲が的になっており、角度と強さを指定し、投射線を描写する。的の範囲に入っていたら的中となり、的の外になっていたら外れということになる。

大抵、このゲームに関しては、30代から40代にBASICを触っていたおじさんであるならば、何処かで見たことがあると思うのだけれども、舞台がWebになった現代には、このような「砲台ゲーム」を作るといったようなサンプルを見ることは殆どなくなった。

で、前回にRubyからgnuplotを触るエントリを書いたわけだけれど、「あれ、グラフを使えば砲台ゲームが作れるんじゃないか」と思って、実装してみることにした。実行環境はJupyter Notebookを参考にしているので、上のリンクからインストールして欲しい。

投射線を描写する

ここからは殆ど『Pythonからはじめる数学入門』のパクリとなるので、正確な説明が欲しい場合には、実際に買ってみて試して欲しい。

投射線というのは「ある物体を投げたときに描く軌道のこと」と説明することができる。さて、このとき「ものを投げる」ということを考えた場合、「その物体を投げる力」と、「その物体をどの方向に投げるのか」という二つの物差しで考えることができる。

さて、このように考えたとき、我々の世界には「時間」という概念が存在している。つまり、「ある物体を、任意の力と角度で投げた場合の、ある時間の一点」というのが存在している。この詳しい原理について気になる人は、別途物理の教科書を参照して頂くとして、これを算出する式は、コードであるならば次のように書ける。

  def parabora(u, t)
    t = t / 180.0 * Math::PI
    g = 9.8
    t_flight = 2 * u * Math.sin(t) / g
    intervals = (0.0..t_flight).step(0.001)
    return [
      intervals.map {|x| u * Math.cos(t) * x}, 
      intervals.map {|y| u * Math.sin(t) * y - 0.5*g*y*y }
      ] 
  end

一から順番に追っていく。まず、最初の「t」はラジアン変換している部分だ。ラジアンは、2πを360度とするので、角度をまず最初に180度で割る。そうすると、nπのnの部分が出てくるので、それを使う。

次のgは単なる重力なので、特に気にしなくてもよい。

次に気になるのは、t_flightだが、これはボールの軌道をいつ止めるかの式になる。これは、ちょうどy軸が0になる範囲を示している。

あとは、u * Math.cos(t) * xの部分だけれども、これは要するにパワーと角度、そして時間をかけて、cosで求めている。一方、yのほうに関しては、重力補正がかかるので、ちゃんと- 0.5 * g * y * yといったように、時間軸にあわせて重力の値が補正できるようにしておかないと、どこまでも飛んでいってしまう。ちなみに、ここの解説が雑なのは、自分もよく理解していないせいもあるので、実際に知りたい人は物理の本を読むといいと思う。

class GameField
 #...

  def parabora_only(u, t) 
    Gnuplot.open do |gp|
      Gnuplot::Plot.new( gp ) do |plot|
        ball = Gnuplot::DataSet.new(parabora(u, t)) do |ds|
          ds.with = "lines lt rgb\"blue\""
          ds.linewidth = 1
        end
        plot.data << ball
        IRuby.display(plot)
      end
    end
  end

 #...
end

と書くことによって、下のような図を作画することができる。

f:id:nisemono_san:20161013160525p:plain

ゲーム作るぞー

投射線の考え方はわかった。ポイントは的を描写することである。gnuplotにグラフを流しこむことは簡単で、一次元の配列の場合、要素数が xとなる。これを利用し、途中までの外れの部分の配列は「当たり」の部分の配列よりも短くするようにする。これを重ねることによって、ズレが発生し、当たりの部分がはみだして見えるわけですね。要するにズルです。

# ...

  def initialize
    @first = Random.rand(100...400)
    @last = @first + Random.rand(50..100)
  end

  def show
    Gnuplot.open do |gp|
      Gnuplot::Plot.new( gp ) do |plot|
        plot.title  'Game'
        # gnuplotの場合、配列の要素がxになるので、挟まないといけない
        fix = Gnuplot::DataSet.new([600]) { |ds| ds.with = "lines lt rgb \"#ffffff\"" }
        
        field = Gnuplot::DataSet.new([*0..@first].map {|x| 0 }) do |ds|
          ds.with = "lines lt rgb\"green\""
          ds.linewidth = 10
        end

        hole = Gnuplot::DataSet.new([*0..@last].map {|x| 0 }) do |ds|
          ds.with = "lines lt rgb\"red\""
          ds.linewidth = 5
        end

        plot.data << fix
        plot.data << hole
        plot.data << field
        IRuby.display(plot)
      end
    end    
  end

# ...

諸事情によりDRY、つまり重複コードばっかりなのは御愛嬌として、こうすることによって、下のようなグラフができる:

右上に点を付けているのは、グラフが最大の値に合うようになるので、こうすることによって、よりゲームらしい広がりのある表現を作りたかったためである。

さて、これを描写すると次のようになる

f:id:nisemono_san:20161013160539p:plain

おっ、砲台ゲームっぽいですね。

発射メソッドを作る

というわけで弾を発射します。こんな感じでいいのではないかと。

    Gnuplot.open do |gp|
      Gnuplot::Plot.new( gp ) do |plot|
        # gnuplotの場合、配列の要素がxになるので、挟まないといけない
        fix = Gnuplot::DataSet.new([600]) { |ds| ds.with = "lines lt rgb \"#ffffff\"" }
        
        field = Gnuplot::DataSet.new([*0..@first].map {|x| 0 }) do |ds|
          ds.with = "lines lt rgb\"green\""
          ds.linewidth = 10
        end

        hole = Gnuplot::DataSet.new([*0..@last].map {|x| 0 }) do |ds|
          ds.with = "lines lt rgb\"red\""
          ds.linewidth = 5
        end

        ball = Gnuplot::DataSet.new(parabora(u, t)) do |ds|
          ds.with = "lines lt rgb\"blue\""
          ds.linewidth = 1
        end

        plot.data << fix
        plot.data << hole
        plot.data << field
        plot.data << ball
        t2 = t / 180.0 * Math::PI
        last =  u * Math.cos(t2) * (2 * u * Math.sin(t2) / 9.8)
        if last.abs.between?(@first, @last)
          plot.title  '成功'
        else
          plot.title  '残念'
        end
        
        IRuby.display(plot)
      end
    end
  end

結果として、何処に落ちたかどうかは計算しなければいけなかったので、別途u * Math.cos(t2) * (2 * u * Math.sin(t2) / 9.8)みたいなかたちで、最後のx軸の部分を計算している。ただし、マイナスになることもあるので、absでちゃんと判定できるようにすると吉。

f:id:nisemono_san:20161013160653p:plain

これは残念なパターンです。

f:id:nisemono_san:20161013160701p:plain

やりましたね。

ちなみに、全体のソースはこちらになります。

まとめ

数学だと思ったらいきなり物理の話が出てきて、とまどったし、実際なんでこういう方程式になるのかというのかを説明できない馬鹿なので、今回はポコー(心が凹んだときの音)した。しかし、グラフが書けると、このように砲台ゲームを作ることができる。今だとR言語がグラフを作るのに丁度いいので、こういう砲台ゲームを作ってみると楽しいのではないか、と思ったりした。だれか書いてくれませんかね(お前が書け)

Pythonからはじめる数学入門

Pythonからはじめる数学入門