目次へもどる

FieldとBlock

Block

まず,ぷよぷよは商標登録だと思われるので ぷよのことを「ブロック」と呼ぶことにしよう. 実際開発段階ではただの矩形で表示するし. まあ結局いたるところでぷよぷよぷよぷよ連呼するんだけどね!

このブロックを表すクラス「Block」を定義した.

class Block
  attr_accessor :color, :row, :line
  def initialize(col)
    @color = col
    @row = -1
    @line = -1
  end

  def inspect
    sprintf("|(%2d,%2d):%s|",@row,@line,@color.to_s)
  end

  def to_s
    sprintf("|(%2d,%2d):%s|",@row,@line,@color.to_s)
  end
end

まずブロックは色,列,行をプロパティに持つ.なんで行,列じゃなくて列,行かだって? 今にわかります.

また,デバッグのために「to_s」と「inspect」メソッドをオーバーライド. これでブロックの位置と色がわかりやすく表示される.

Field

次にブロックが配置される場所が必要だ. ぷよぷよで言うところのキャラ絵があるところ. これを我々はフィールドと呼ぶことにする.

class Field
end

table

フィールドは普通6列の12行(だっけ?)で構成されるタイル状のデータ構造を持ちそうに見える. なのでそれをテーブルと呼ぶことにして定義しよう.

  def initialize(row_s, line_s)
    @row_s = row_s
    @line_s = line_s
    init_table
  end
  def init_table
    @table = []
    @row_s.times do |row|
      @table.push Array.new(@line_s)
    end
  end

なんで@row_s,@line_sを受け取るようにしてるかといえばもちろん拡張のため. ちびぷよでプレイとか実現できるかもしれない.

テーブルの構造は至って簡単.配列の配列である. まず列数の要素を持つ配列を作成し,その要素それぞれに行数分の要素の配列を格納する. なんで行,列じゃなくて列,行かだって? 今にわかります.

テーブルの初期値はnilなので, @table[r][l]がnilならそのマスにブロックは無いことになる.

ちなみにrowは左から0,1,2,3,4,5である. lineは下から0,1,2,3,4,5...である.

blocklist

さて,テーブルを作ってそこにブロックを格納していくのはいいが, フィールドにあるすべてのブロックになんらかの処理を行いたいときにテーブルのすべてのマスに対してブロックがあるかないかを調べていたらバカである. そこで,テーブルとは別にブロックの一覧を配列で持つことにした.

んでもってこれらを利用してフィールドにブロックを配置するメソッド「set」を定義した.

  def initialize(row_s, line_s)
    @row_s = row_s
    @line_s = line_s
    init_table
    init_blocklist
  end
  def init_table
    @table = []
    @row_s.times do |row|
      @table.push Array.new(@line_s)
    end
  end
  def init_blocklist
    @blocklist = []
  end
  def set(r,l,col)
    if @table[r][l]
      @blocklist.delete @table[r][l]
    end
    block = Block.new(col)
    block.row = r
    block.line = l
    @table[r][l] = block
    @blocklist.push block
  end

(注意)テーブルにブロックを格納といったが,Rubyではすべてがオブジェクトへの参照へ過ぎないので,あるブロックblockに対して,

@table[r][l] = block; @blocklist.push block

とした場合,それぞれ同じblockを参照することになる.

ブロックを消す

さあ,ブロックをフィールドに配置できたらとりあえずやることはただ一つ! ブロックを消しましょう.

つながってる?

ブロックを消す条件は

  • 同じ色のブロックが上下左右に4個以上つながっている
    これである.

これをなるべくいい感じに行う方法を考えた.

  1. まずフィールド上のブロックすべてをチェック対象としてチェックリストに追加
  2. つながっているブロックを管理するためのコネクトテーブルを作成
    コネクトテーブルは「つながっているブロックの配列」の配列
  3. チェックリストの先頭のブロックを取得
  4. そのブロックを起点にしてつながりチェックを開始
    このとき新たにコネクトテーブルに空配列の要素を追加
  5. ブロックがテーブルの範囲外ならreturn
  6. ブロックがチェックリストに入っていなかったらすでにチェック済みなのでreturn
  7. ブロックの色が起点ブロックと異なっていたらreturn
  8. ここまで来たらブロックがつながっているのでブロックをチェックリストから削除し,
  9. 現在のコネクトテーブルに追加
  10. 追加したブロックを起点ブロックとして上下左右を6から再帰的にチェック
  11. チェックリストがまだ残ってるなら3に戻る

この流れで二個以上つながっているブロックのグループがコネクトテーブルに格納される.

  def check_connection(r,l,col)
    return if r < 0 || r >= @row_s
    return if l < 0 || l >= @line_s
    block = @table[r][l]
    return unless @checklist.include? block
    return unless @table[r][l] && @table[r][l].color == col
    @checklist.delete block
    @connect_table[-1].push block
    check_connection(r,l+1,col) # up
    check_connection(r,l-1,col) # down
    check_connection(r+1,l,col) # right
    check_connection(r-1,l,col) # left
  end

  def make_connect_table
    init_connect_table
    @checklist = @blocklist.dup
    while !@checklist.empty?
      block = @checklist.first
      r = block.row; l = block.line
      col = block.color
      @connect_table.push []
      check_connection(r,l,col)
    end
  end

消す

これは簡単.先ほど取得したつながってるグループのサイズが4以上ならそのグループに所属しているブロックをテーブルおよびブロックリストから消せばよい.

  def eliminate_connection
    @connect_table.each do |connection|
      next if connection.size < 4
      @eliminated = true # check flag
      connection.each do |block|
        @table[block.row][block.line] = nil
        @blocklist.delete block
      end
    end
  end

落ちる

さて,ブロックが消えたら当然スキマスイッチが出てくるのでなんとかしないといけない. スキマスイッチとは以下のような状態のスキマのことである.我々はこのスキマをスキマスイッチと呼ぼう.

 b
   ←ここ
 b
----

重力で落ちるゲームというくらいだから隙間の上にあるブロックを落とせばいいだろう.

まず当然のことながら各行ごとに考えれば十分なのでこんなんした.

  def falldown
    @fallen = false
    @row_s.times do |r|
      falldown_line(r)
    end
    @fallen
  end

ちなみに@fallenってのは落ちたらtrue,変化なかったらfalseになる.

当然のことながらって言ったけど,これが先ほどから言っている

なんで行,列じゃなくて列,行かだって? 今にわかります.

の答えである.まず最初に列で分割できたほうが何かと都合が良い. まあ表示するときは先に行で分割したいからめんどくさいことやってるけどね・・・.*1

次に各行ごとの落下処理を考える. まず行のサイズだけインデックスを回して(0〜11),最初にブロックが無かった位置をベースとする. ようするにこのベースがスキマスイッチの候補である.

そしたらベースの一個上のマスからまたしてもインデックスを回して,今度はブロックが見つかったらそこでストップする. あとはストップした位置以降すべてのマスをベースまでおろしてくればいい.

これでおっけーかと思いきや,同じ列にスキマスイッチが一人とは限らない.ちょうどスキマスイッチが二人組のように.

ようするに先ほどベースだったマスの一つ上からもう一度ブロックの無い位置を調べ,そこを新たなベースとして...っていうのを繰り返す. これでこの行についてすべてのスキマスイッチが埋まる.

  def falldown_line(r)
    @line_s.times do |base|
      next if @table[r][base]
      # fall down to base
      ((base+1)...@line_s).each do |b_line|
        next unless @table[r][b_line] # skip space sequence
        @fallen = true # check flag
        @table[r].slice!(base...b_line) # fall down
        @table[r].concat(Array.new(b_line-base)) # fill empty
        # update block pos
        (base...@line_s).each do |l|
          next unless @table[r][l]
          @table[r][l].row = r
          @table[r][l].line = l
        end
        break
      end
    end
  end

ここまでをテスト

ここまで正常に動作するかテストコードを実行してみよう!

$ ruby field.rb

さりげなく文字列から一気にフィールドにブロックを配置できるメソッド「set_table」を定義していたのでこれをつかう.

if __FILE__ == "field.rb"
  field = Field.new(6,12)
  tstr =<<EOF
......
b.....
y.....
b.....
b.r...
r.b...
r....y
bbb.yg
rygrbg
rbygrb
bygrbg
bygrbg
EOF
  field.set_table(tstr)
  field.print_field
  loop do
    fallen = field.falldown
    field.print_field if fallen
    eliminated = field.eliminate
    field.print_field if eliminated
    break if !fallen && !eliminated
  end

落下もしくは消去が起きなくなるまでループが続く. これを実行すると以下の出力が得られる. 長いな.

--------
|      |
|b     |
|y     |
|b     |
|b r   |
|r b   |
|r    y|
|bbb yg|
|rygrbg|
|rbygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|b     |
|y     |
|b     |
|b     |
|r r   |
|r b  y|
|bbb yg|
|rygrbg|
|rbygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|b     |
|y     |
|b     |
|b     |
|r r   |
|r    y|
|    yg|
|rygrbg|
|rbygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|      |
|b     |
|y     |
|b     |
|b     |
|r    y|
|r r yg|
|rygrbg|
|rbygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|      |
|b     |
|y     |
|b     |
|b     |
|     y|
|  r yg|
| ygrbg|
| bygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|b    y|
|y r yg|
|bygrbg|
|bbygrb|
|bygrbg|
|bygrbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|b    y|
|y r yg|
| ygrbg|
|  ygrb|
| ygrbg|
| ygrbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|  r yg|
|  grbg|
| yygrb|
|bygrbg|
|yygrbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|  r yg|
|  grbg|
|   grb|
|b grbg|
|  grbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|    yg|
|  rrbg|
|  ggrb|
|  grbg|
|b grbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|    yg|
|  rrbg|
|    rb|
|   rbg|
|b  rbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|    yg|
|    bg|
|   rrb|
|   rbg|
|b rrbg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|    yg|
|    bg|
|     b|
|    bg|
|b   bg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|     g|
|    yg|
|    bb|
|    bg|
|b   bg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|     g|
|    yg|
|      |
|     g|
|b    g|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|     g|
|     g|
|     g|
|b   yg|
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|     y|
|      |
|      |
|      |
|b   y |
--------
--------
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|      |
|b   yy|
--------

*1 Array#transposeを使う

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2013-08-30 (金) 21:58:54 (3891d)