【Ruby】 ncurseswでゲームをつくる

Rubyからncurseswを使って簡単なゲームを作った話です。とりあえず例として作成してみた避けゲーの動作の様子を以下に示します。

![minigame.gif](https://qiita-image-store.s3.amazonaws.com/0/54587/6f2c3d1d-893d-3f8e-db97-51bc41a2fed1.gif)


## 動作環境

* Ubuntu 14.04
* Ruby 2.2.0
* ncursesw(gem) 1.4.9

必要なもののインストールは以下のコマンドで

    $ sudo apt-get install libncursesw5-dev
    $ gem install ncursesw

ncurses でもいいのですが、今回は日本語なども文字化けすることなく表示できる ncursesw を選択しました。


## ソースコード

以下に示す main.rb を実行すれば冒頭のGIFと同じゲームが始まります。操作方法とルールは上下左右の方向キーで自機を動かし、飛んでくる弾を避けるだけです。

画面の大きさに合わせて弾が生成されるので、実行時のサイズは縦横ともに制限はありません。むしろそれによって難易度調整ができるとも言えます。

```ruby:main.rb
require 'ncursesw'

class MiniGame
  def initialize
    Ncurses.initscr
    Ncurses.curs_set(0) # カーソル非表示
    Ncurses.cbreak # RAW(改行を待たない)モードにする
    Ncurses.noecho # 入力された内容をエコーしない
    Ncurses.keypad(Ncurses.stdscr, true) # KEY_* 用
    color_config

    @height, @width = Ncurses.LINES, Ncurses.COLS
    @exit = false
    @player = [@height - 1, (@width / 2) - 2]
    @bullets = Array.new(@width - 1) do
      Array.new(@height) { false }
    end
  end

  # 全てのスレッドを実行する
  def run
    threads = []
    threads << drawing_thread
    threads << input_thread
    threads << game_thread
    threads.each(&:join)
    close_scr
  rescue Interrupt
    close_scr
  end

  private

  # 色関連の設定
  def color_config
    @has_colors = Ncurses.has_colors?
    if @has_colors
      Ncurses.start_color
      if Ncurses.use_default_colors == Ncurses::OK
        Ncurses.init_pair(1, Ncurses::COLOR_CYAN, -1)
      end
    end
  end

  # 描画するスレッド
  def drawing_thread
    Thread.new do
      until @exit
        draw_player
        draw_bullets
        Ncurses.refresh
        sleep 0.01
        Ncurses.clear
      end
    end
  end

  # 入力を監視するスレッド
  def input_thread
    Thread.new do
      until @exit
        case Ncurses.getch
        when "q".ord then @exit = true
        when Ncurses::KEY_UP
          @player[0] -= 1
          @player[0] = 1 if @player[0] < 2
        when Ncurses::KEY_DOWN
          @player[0] += 1
          @player[0] = @height - 1 if @player[0] > @height - 1
        when Ncurses::KEY_RIGHT
          @player[1] += 1
          @player[1] = @width - 3 if @player[1] > @width - 3
        when Ncurses::KEY_LEFT
          @player[1] -= 1
          @player[1] = 1 if @player[1] < 1
        end
      end
    end
  end

  # 飛んでくる弾を生成したり当たり判定をしたりするスレッド
  def game_thread
    Thread.new do
      until @exit
        @bullets.map! do |row|
          row.unshift rand < 0.02
          row.pop
          row
        end
        @exit = true if gameover?
        sleep 0.1
      end
      Ncurses.move(0, 0)
      Ncurses.addstr("Game Over!\n")
      Ncurses.addstr("Please press any key...\n")
    end
  end

  # 自機を描画
  def draw_player
    Ncurses.attron(Ncurses::COLOR_PAIR(1)) if @has_colors
    Ncurses.move(*@player.zip([-1, 0]).map{ |a, b| a+b })
    Ncurses.addstr("▓")
    Ncurses.move(*@player.zip([0, -1]).map{ |a, b| a+b })
    Ncurses.addstr("▓▓▓")
    Ncurses.attroff(Ncurses::COLOR_PAIR(1)) if @has_colors
  end

  # 飛んでくる弾を描画
  def draw_bullets
    @bullets.each_with_index do |row, j|
      row.each_with_index do |bullet, i|
        if bullet
          Ncurses.move(i, j)
          Ncurses.addstr("▓")
        end
      end
    end
  end

  # 弾にあたったらゲームオーバー
  def gameover?
    @player.tap do |i, j|
      break [[i, j], [i, j-1], [i, j+1], [i-1, j]]
        .any? { |pi, pj| @bullets[pj][pi] }
    end
  end

  # 後処理
  def close_scr
    Ncurses::curs_set(1)
    Ncurses.nocbreak
    Ncurses.echo
    Ncurses.endwin
  end
end

game = MiniGame.new
game.run
```


## ncurseswの使い方

Rubyで ncursesw や ncurses を扱うことについて書かれたサイトはそんなに多くは見かけませんが、C言語でのサンプルとか説明が豊富で、結構参考になります(関数名とかは同じなので)。ここでは主に今回つくったゲームで使った機能について触れます。

### 初期設定と後処理

initialize メソッド内のもろもろの設定を行うメソッドと、close_scr メソッド内のそれを念の為元に戻すメソッドなどについて。

* `Ncurses.initscr` / `Ncurses.endwin`
  * 初期化のため最初に呼び出す / こちらはプログラムの最後に
* `Ncurses.curs_set(0)` / `Ncurses::curs_set(1)`
  * 0でカーソルを非表示 / 1で表示
* `Ncurses.cbreak` / `Ncurses.nocbreak`
  * キー入力において改行を待たずに入力が即座に反映されるように
  * する / しない
* `Ncurses.noecho` / `Ncurses.echo`
  * キー入力をスクリーンにエコー
  * しない / する
* `Ncurses.keypad(Ncurses.stdscr, true)`
  * 後述の Ncurses.getch において、ture にすることでファンクションキーや方向キーなどを押した際にキーコードを返してくれるようになる

### 色の設定

色は背景色と前景色がペアとして扱われ、そのペアにつけられた番号を使って文字や背景への色付けを行います。

```ruby
  def color_config
    @has_colors = Ncurses.has_colors?
    if @has_colors
      Ncurses.start_color
      if Ncurses.use_default_colors == Ncurses::OK
        Ncurses.init_pair(1, Ncurses::COLOR_CYAN, -1)
      end
    end
  end

  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  def draw_player
    Ncurses.attron(Ncurses::COLOR_PAIR(1)) if @has_colors

    #
    # この間はCOLOR_PAIR(1)の色で描画される
    #

    Ncurses.attroff(Ncurses::COLOR_PAIR(1)) if @has_colors
  end
```

* `Ncurses.has_colors?`
  * 現在の端末で色が使用できるか否かを返す
* `Ncurses.start_color`
  * 色の使用を開始する際に呼び出す
* `Ncurses.use_default_colors`
  * 背景色と前景色に使う色番号をどちらも -1 にする
  * -1 はデフォルトの端末の背景色・前景色の意味
* `Ncurses.init_pair(1, Ncurses::COLOR_CYAN, -1)`
  * 色のペアをつくる
  * この例ではペア番号の1に前景がCYAN、背景がデフォルト色というペアになっている
  * 設定したペアの使い方は draw_player メソッド内を参照

### その他

* `Ncurses.getch`
  * キーの入力待ちをする
  * Ncurses::KEY_* のようにそれぞれのキーに定数が用意されている
* `Ncurses.move(i, j)`
  * スクリーン上のi行j列にカーソルを移動
* `Ncurses.addstr("文字列")`
  * カーソル位置に文字列を追加
* `Ncurses.refresh`
  * addstr などで追加した文字列をスクリーンに反映
* `Ncurses.clear`
  * スクリーン上の文字列などの情報をクリア

## おわりに

この避けゲーは以前つくった常駐型アプリっぽいものにミニゲームとして実装されているので、もしよければこちらものぞいてみてください。

[【Ruby】 ncurseswを使って常駐型アプリ](http://syoshinsyakangeisagi.blogspot.com/2016/02/ruby-ncursesw.html)

![debug_room_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/54587/b06298bd-b3e3-b46f-4603-381030f2cd13.gif)


参考サイト

[https://github.com/sup-heliotrope/ncursesw-ruby](https://github.com/sup-heliotrope/ncursesw-ruby)
[NCURSES Programming HOWTO](http://www.on-sky.net/~hs/misc/?NCURSES+Programming+HOWTO#l15)
[せりか式 ncurses](http://www.kis-lab.com/serikashiki/man/ncurses.html#mouse)