Newer
Older
Ruby / roket.rb
#!/usr/koeki/bin/ruby
# -*- coding: utf-8 -*-
# Curses でロケットの運動を描画する
# 作者:山本裕樹
 
require "curses"
 
# 万有引力定数 G = 6.67430×10^-11 [m^3/kg/s^2]
# 地球の質量 ME = 5.9724×10^24 [kg]
# 地球の半径 RE = 6.3781x10^6 [m]
$G = 6.67430e-11
$ME = 5.9724e24
$RE = 6.3781e6
 
# 現実の座標から端末上の座標に変換するための x, y 方向のスケール
$xscale = 10.0
$yscale = 5.0
 
# 初期値
# ($x0, $y0):ロケットの発射位置(単位 [m])
# ($u0, $v0):ロケットの発射速度(単位 [m/s])
# $dt:時間の刻み(単位 [s])
$x0 = $RE + 10
$y0 = 0
$u0 = 6000
$v0 = 6000
$dt = 60
 
# 原点から (x, y) までの距離
def r(x, y)
  return Math.sqrt(x ** 2 + y ** 2)
end
 
# 運動方程式の右辺1
def f(x, y)
  return - $G * $ME * x / r(x, y) ** 3
end
 
# 運動方程式の右辺2
def g(x, y)
  return - $G * $ME * y / r(x, y) ** 3
end
 
# ku, kv, kx, ky の計算
def calc_k(u, v, x, y, du, dv, dx, dy)
  return f(x + dx, y + dy) * $dt,
         g(x + dx, y + dy) * $dt,
         (u + du) * $dt,
         (v + dv) * $dt
end
 
# ルンゲ=クッタ法で $dt 後の (x,y) と (u,v) を求める
def next_step(u, v, x, y)
  ku1, kv1, kx1, ky1 = calc_k(u, v, x, y, 0, 0, 0, 0)
  ku2, kv2, kx2, ky2 = calc_k(u, v, x, y,
                              ku1 * 0.5, kv1 * 0.5, kx1 * 0.5, ky1 * 0.5)
  ku3, kv3, kx3, ky3 = calc_k(u, v, x, y,
                              ku2 * 0.5, kv2 * 0.5, kx2 * 0.5, ky2 * 0.5)
  ku4, kv4, kx4, ky4 = calc_k(u, v, x, y, ku3, kv3, kx3, ky3)
  return u + (ku1 + 2 * ku2 + 2 * ku3 + ku4) / 6.0,
         v + (kv1 + 2 * kv2 + 2 * kv3 + kv4) / 6.0,
         x + (kx1 + 2 * kx2 + 2 * kx3 + kx4) / 6.0,
         y + (ky1 + 2 * ky2 + 2 * ky3 + ky4) / 6.0
end
 
# 現実の座標 (x,y) から端末上の座標 (cx,cy) へ変換
def x2cx(x) ($cox + x / $RE * $xscale).round end
def y2cy(y) ($coy - y / $RE * $yscale).round end
 
# 端末上の座標 (cx,cy) から現実の座標 (x,y) へ変換
def cx2x(cx) (cx - $cox) * $RE / $xscale end
def cy2y(cy) -(cy - $coy) * $RE / $yscale end
 
# (0, 0) を中心に地球を描く
def draw_earth
  # 地球の色の設定
  Curses.attrset(Curses.color_pair(2))
 
  # 端末上の全ての座標をスキャンして地球の半径内なら地球を出力
  0.upto(Curses.cols - 1) do |cx|
    0.upto(Curses.lines - 1) do |cy|
      if r(cx2x(cx), cy2y(cy)) < $RE
        Curses.setpos(cy, cx)
        Curses.addch(' ')
      end
    end
  end
end
 
# 現実の座標 (x,y) から端末上の座標へ変換して文字 ch を出力
def puts_char(x, y, ch)
  cx = x2cx(x)
  cy = y2cy(y)
  if (cx >= 0 && cx <= Curses.cols - 1) &&
     (cy >= 0 && cy <= Curses.lines - 1)
    Curses.setpos(cy, cx)
    Curses.addch(ch)
  end
end
 
# Cursesの初期化
Curses.init_screen
 
begin
  # 色の設定
  Curses.start_color
  Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
  Curses.init_pair(2, Curses::COLOR_BLACK, Curses::COLOR_BLUE)
  Curses.init_pair(3, Curses::COLOR_CYAN, Curses::COLOR_BLACK)
  Curses.init_pair(4, Curses::COLOR_BLACK, Curses::COLOR_RED)
 
  # 端末の中央の座標
  $cox = ((Curses.cols - 1) * 0.5).round + 0.5
  $coy = ((Curses.lines - 1) * 0.5).round + 0.5
 
  # カーソルを非表示
  Curses.curs_set(0)
 
  # キー入力で待つ時間(ミリ秒)
  Curses.stdscr.timeout = 60
 
  # 初期値を代入
  x = $x0
  y = $y0
  u = $u0
  v = $v0
 
  # 時間
  i = 0
 
  # ロケットの軌道を描くループ
  while 1
    # 地球を出力
    draw_earth
 
    # デフォルトの色
    Curses.attrset(Curses.color_pair(1))
 
    # 時刻の出力
    Curses.setpos(Curses.lines - 1, 0)
    Curses.addstr(sprintf("t=%d[s]\n", i * $dt))
 
    if (r(x, y) > $RE)
      # ロケットを出力
      puts_char(x, y, 'R')
    else
      # 爆発を出力
      Curses.attrset(Curses.color_pair(4))
      puts_char(x, y, 'X')
      Curses.refresh
      # ループを抜ける
      break
    end
 
    # キー入力があるか
    if Curses.getch != nil
      # ループを抜ける
      break
    end
 
    # (x, y) にロケットの跡を出力
    Curses.attrset(Curses.color_pair(3))
    puts_char(x, y, '.')
 
    # 画面表示の更新
    Curses.refresh
 
    # $dt 後の (x,y) を求める
    u, v, x, y = next_step(u, v, x, y)
    i += 1
  end
 
  # キー入力があるまでずっと待つ
  Curses.stdscr.timeout = -1
  Curses.getch
 
ensure
  # Cursesの終了処理
  Curses.close_screen
end