15ゲーム--データベースなしでRailsを使う

Ruby on Railsではアプリケーションの骨格が自動で作られ、非常に便利です。しかしそれはデータベースの構築が前提になっています。データベースが作られても使わなければ良いとも言えましょうが、要らないものは使わないようにしたいものです。

例として15ゲームを作ります。15ゲームでデータベースを使う局面がないわけではありませんが、Railsで普通にアプリケーションを構築すると、ゲームの動きに直接関係するモデルもデータベースに保存されます。またそれでないとうまく動かないのです。

15ゲームで言えば、15個の駒の配置をモデルと表示の間でやりとりすることになります。表示するにはコントローラーのインスタンス変数に何らかの形で配置情報を入れることで受け渡すことができます。プレーヤーがその配置のひとつの駒を動かす意思を(クリックなどで)示すとそれを処理するために表示からコントローラーにリクエストが送られます。リクエストを受け取ったコントローラーは元の配置をどのように認識するのでしょうか。実はこのコントローラーは新しくnewされたインスタンスで、先のインスタンスの情報は持っていないのです。そこで、リクエストには配置情報をもっているインスタンスのデータベース上のIDが含まれ、これを使ってデータベースから配置情報を復元するのです。

このやり方は名簿管理など、もともと永続すべきデータを扱っているならごく自然な方法ですが、ゲームのように終われば消えてかまわないデータをやりとりするのに使うのは「牛刀を以って鶏を割く」のそしりを免れないでしょう。

1. データベースなしプロジェクトの生成

データベース付きのモデルクラス(の親クラス)であるActiveRecordを使わないための設定はconfigにおいて行います。まずはいつものようにプロジェクトを生成します。

>rails 15game >cd 15game

ここで、通常なら>rake db:createとやるところですが、データベースはないので、やりません。
/config/environment.rbを開いて27行目あたりを読むとデータベースなしにするにはフレームワークからActive Recordを除外すべし云々とあります。
それに従って29行目のコメントマーク(#)を外します。

# Skip frameworks you're not going to use. To use Rails without a database, # you must remove the Active Record framework. config.frameworks -= [ :active_record, :active_resource, :action_mailer ]

これでActiveRecordはプロジェクトに含まれなくなり、データベースなしのプロジェクトができました。ここでサーバーを起動します。

>ruby script/server

そしてブラウザーでhttp://localhost:3000/を見ると

Rails welcome 画面

Railsでおなじみの画面がでてきます。この中のAbout your application's environmentをクリックすると

Rails welcome 画面-開発環境

開発環境にActive Recordが入っていないことが確認できます。

2. 15ゲームのクラス構成

15ゲームを構成するクラスの候補を探します。まずゲームを記述してみます。

『15ゲームは、4x4の枡をもつ盤を使う。各枡には1から15までの番号を持つ駒が順不同に入っていて、1つの枡だけが空きになっている。ゲームの目的は適当な駒を空きの枡に移動させることを繰り返すことにより、1から15まで順にそろった状態に持ち込むことである。』

クラス候補としては、「ゲーム」、「盤」、「枡」、「駒」、「空き」などの名詞が挙りますが、ここでは「盤」と「枡」をクラスとし、駒は枡の属性である番号のみの存在とすることにします。空きはもともと枡の状態ですが、ここでは番号属性が0のときを空きとします。ゲーム全体をクラスとすることも十分あり得ると思いますが、ここではコントローラーに機能を持たせることにしました。なお駒に個別のイメージを持たせたりするならクラスとして独立させることも十分あり得ると思います。

3. コントローラー・ビューを作る

コントローラーとビューの骨格を作ります。

>ruby script/generate controller games index

これで/app/controllers/game_controller.rbと/app/views/game/index.html.erbが作られます。前者は

class GameController < ApplicationController def index end end

後者は

<h1>Game#index</h1> <p>Find me in app/views/game/index.html.erb</p>

と、骨格だけ用意されます。意味のある中身は、モデルクラスを作ってからになります。

4. 盤と枡のモデルを作る。

いよいよ本題のモデルつくりです。

「盤」のモデルとしてBoardクラスを作ります。モデル作成にscript/generateは使えません。ここがActiveRecordを除外した弱みですが、テスト関係を無視すればやることはほとんどありません。/app/models/board.rbを新規作成し、次のようにx, yを属性値として登録します。x,yは縦横の枡数で、15ゲームなら各々4ですが、ここは一般化しておきます。そしてnewするとき設定します。

/app/models/board.rb

class Board attr_reader :x, :y def initialize(x, y) @x = x @y = y end end

「枡」のモデルでは属性として位置情報x_pos, y_posとそこの駒の番号numberを持つものとします。

/app/models/box.rb

class Box attr_reader :x_pos, :y_pos attr_accessor :number def initialize(x_pos, y_pox, number) @x_pos = x_pos @y_pos = y_pos @number = number end end

盤と枡の関連付けが必要ですActiveRecordのもとではhas_manyとbelongs_toで簡単に関連付けができますが、再び三度自分で何とかしなくてはなりません。

結局、Boardに配列属性boxesを用意してそこにBoxインスタンスの参照を保持することにしました。

/app/models/board.rb (前のリストを差し替え)

class Board attr_reader :x, :y, :boxes def initialize(x, y) @x = x @y = y @boxes = [] i = 1 @y.times do |y| @x.times do |x| b = Box.new(x, y, i) if i > @x * @y - 1 b.number = 0 # 最後は空き end @boxes << b i += 1 end end end end

5. 最初のビュー

モデルの確認のため、表示してみましょう。game_controller.rbのindexでBoardをnewし、それをindex.html.erbで表示に使います。受け渡しに使うのはコントローラーのインスタンス変数です。

/app/controllers/game_controller.rb

class GameController < ApplicationController def index @board = Board.new(4, 4) end end

/app/view/game/index.html.erb

<h1>15ゲーム</h1> <table border = 1> <% @board.y.times do |y| %> <tr> <% @board.x.times do |x| %> <td> <%= @board.box(x, y).to_s %> </td> <% end %> </tr> <% end %> </table>

ここでbox(x, y)はBoardの追加関数で、その返り値のto_sはBoxの追加関数です。

/app/models/board.rb (追加分)

class Board def box(x, y) @boxes.detect { |b| b.x_pos == x && b.y_pos == y } end end

/app/models/box.rb (追加分)

class Box def to_s @number == 0 ? " " : @number.to_s # 0は空白 end end

ここでサーバーを起動して、

>ruby script/server

ブラウザーでhttp://localhost:3000/gameを見ると、

15ゲーム画面0

と、見栄えはともかく、16箇所の枡に15個の駒が順に並んだ盤が表示されます。

ここまでのプログラムのダウンロード

6. 駒を動かす

空きの枡の隣の駒は動かせるようにしなければゲームになりません。普通やるように、駒の番号をクリックしたら動くことにします。そのためには番号をパラメーターとするリンクを番号に貼り付けます。しかしそれだけでは足りません。前に述べたようにサーバーに戻るとBoardインスタンス情報は失われているので、それを渡す必要があります。

Boardインスタンス情報をパラメーターとして受け渡すには、文字列にするのがよさそうです。そこで、まずBoardにx, y, boxesの情報を文字列にパックするメソッドを作ります。その文字列を受け取ってBoardのインスタンスを生成するメソッドはnewと良く似ているので、iniitalizeを改造して3番目の引数に文字列の先頭x,y部を除いた残りを受け取るようにしました。

/app/models/board.rb (追加修正分)

class Board def initialize(x, y, str = "auto") @x = x @y = y @boxes = [] if str == "auto" i = 1 str = "" (@x * @y).times do str << (i < @x * @y ? sprintf("%02d", i) : "00") i += 1 end end @y.times do |y| @x.times do |x| @boxes << Box.new(x, y, str.slice!(0..1).to_i) end end end def pack str = sprintf("%02d%02d", @x, @y) @y.times do |y| @x.times do |x| str += sprintf("%02d", box(x, y).number) end end str end end

さて、せっかくの盤情報受け渡し手段ですが、肝心のリクエストのやり方が未定です。自然なスタイルとしては、『動かせる駒にリンクをつけ、クリックしたらリクエストを送る。受けたサーバーではその駒を空きに移動して、新しい盤状態を表示する』ことでしょう。そのためには、

  1. 空きの枡の検知。
  2. 空きに隣り合う枡のリストアップ。
  3. 指定の枡と空きの枡とで番号を入れ替え。
という3つのメソッドが必要になります。いずれも盤のメソッドです。

/app/models/board.rb (追加分)

class Board # 空きの枡 def empty_box @boxes.detect { |b| b.number == 0 } end # 空きに隣り合う枡群 def boxes_next_to_empty eb = empty_box ebx = eb.x_pos eby = eb.y_pos bne = [] bne << box(ebx, eby + 1) << box(ebx, eby - 1) bne << box(ebx + 1, eby) << box(ebx - 1, eby) bne.compact end # 駒を動かす def move(number) eb = empty_box return nil unless box = boxes_next_to_empty.detect { |b| b.number == number } eb.number = number box.number = 0 end end

これらのメソッドを使って、まずビューでリクエストを出す仕掛けをします。

/app/view/game/index.html.erb (差し替え)

<h1>15ゲーム</h1> <table boarder = 1> <% @board.y.times do |y| %> <tr> <% @board.x.times do |x| %> <% bx = @board.box(x, y) %> <td> <%= link_to_if @board.boxs_next_to_empty.index(bx), bx.to_s, :action => 'move', :id => bx.number, :packed_info => @board.pack %> </td> <% end %> </tr> <% end %> </table>

リクエストを受けてコントローラーが駒を動かして、再表示します。

/app/controllers/game_controller.rb (追加分)

class GameController < ApplicationController def move str = params[:packed_info] x = str.slice!(0..1).to_i y = str.slice!(0..1).to_i @board = Board.new(x, y, str) @board.move params[:id].to_i render :index end end

ブラウザーでhttp://localhost:3000/gameを見ると、

15ゲーム画面1

のように空きの枡の隣の番号にリンクがついたので、12をクリックすると、

15ゲーム画面2

と変化し、さらに11をクリックすると、

15ゲーム画面3

と変わります。これで、駒を動かすところまで実現しました。

ここまでのプログラムのダウンロード

7. ゲームの完成へ

盤と枡、駒という道具立てはできました。しかし、ゲームとしては問題(最初のパターン)の提示、終了の判定が少なくとも必要です。また手数の表示もほしいところです。また終了したところで、次の問題を始められるようにしましょう。これらはゲームクラスのメソッドで、このクラスはコントローラーに兼任させていますから、そこに設けます。

問題の作り方ですが、1から15までを乱数で並べるのでは完成しないパターンになる可能性があります。たとえば隣り合った2つの駒を(空中で)入れ替えると完成しないパターンになります。それで、問題の作成には駒をランダムに多数回動かして作ります。そうして作ったパターンの形を一定にするために、空きを右下に移動させることにします。

/app/controllers/game_controller.rb(index修正)

class GameController < ApplcaionController def index @tries = 0 # 試技数リセット @board = Board.new(4, 4) # 多数回ランダムに駒を動かす (@board.x * @board.y * 100).times do boxes = @board.boxes_next_to_empty @board.move(boxes[rand(boxes.size)].number) end # 空きを最右列へ eb = @board.empty_box while eb.x_pos < @board.x - 1 @board.move(@board.box(eb.x_pos + 1, eb.y_pos).number) eb = @board.empty_box end # 空きを最下段へ while eb.y_pos < @board.y - 1 @board.move(@board.box(eb.x_pos, eb.y_pos + 1).number) eb = @board.empty_box end # 初めから完成でなければ、1回目の手を促す。 @tries += 1 unless @complete = _complete? end end

これを受けてビューは、

/app/views/game/index.html.erb(差し替え)

<h1>15ゲーム</h1> <h4> <% if @complete %> <%= "完成です。(#{@tries}回)" %> <% else %> <%= "#{@tries}回目" %> <% end %> </h4> <table border = 1> <% @board.y.times do |y| %> <tr> <% @board.x.times do |x| %> <% bx = @board.box(x, y) %> <td> <%= link_to_if !@complete && @board.boxes_next_to_empty.index(bx), bx.to_s, :action => 'move', :id => bx.number, :packed_info => @board.pack, :tries => @tries %> </td> <% end %> </tr> <% end %> </table> <br /> <%= link_to "新しい問題", :action => 'index' %>

ビューでのクリックに応答するアクションmoveでも終了判定と完成でないときの回数の増加が必要です。

/app/controllers/game_controller (move修正分)

class GameController < ApplicationController def move str = params[:packed_info] x = str.slice!(0..1).to_i y = str.slice!(0..1).to_i @board = Board.new(x, y, str) @board.move params[:id].to_i @tries = params[:tries].to_i @tries += 1 unless @complete = _complete? render :index end end

なお、終了判定は他からは見えないプライベート関数です。

/app/controllers/game_controller.rb (_complete追加分)

class GameController < ApplicationController private def _complete? i = 1 @board.y.times do |y| @board.x.times do |x| return false unless i > @board.x * @board.y - 1 || @board.box(x, y).number == i i += 1 end end true end end

ブラウザーでhttp://localhost:3000/gameを見ると、

15ゲーム画面4

と、1回目の表示があり、

15ゲーム画面5

と、途中では次の手の回数表示、完成すると、

15ゲーム画面6

完成とそこまでの回数が表示されます。

これでRailsを使ったプロジェクトとしては一応完成ですが、枡の横幅が変わったりして見栄えはよくありません。そこで最低限の改良をビューで施した例を示します。

/app/views/game/index.html.erb (差し替え)

<h1>15ゲーム</h1> <h4> <% if @complete %> <%= "完成です。(#{@tries}回)" %> <% else %> <%= "#{@tries}回目" %> <% end %> </h4> <table border = 1> <% @board.y.times do |y| %> <tr> <% @board.x.times do |x| %> <% bx = @board.box(x, y) %> <td width = 40, align = 'center'> <font size = 6> <%= link_to_if !@complete && @board.boxes_next_to_empty.index(bx), bx.to_s, :action => 'move', :id => bx.number, :packed_info => @board.pack, :tries => @tries %> </font> </td> <% end %> </tr> <% end %> </table> <br /> <%= link_to "新しい問題", :action => 'index' %>

これをブラウザーで見ると、

15ゲーム画面7

のように文字サイズ、枡の形が整ったと思います。なお、ブラウザーとしてFirefox3.6を使っています。その他のブラウザーでは違って見えるかもしれません。

完成版プログラムのダウンロード

以上

Railsトップ