ブロックス・デュオ全数探索?

会社blogの方にこんな記事やらこんな記事を書いて、思考ルーチンやら解法アルゴリズムが好物であることがばれたので、西尾さんに「次、これなんてどう?」と勧められたのが「ブロックス・デュオ」
GPCC で「ブロックス・デュオ」のプログラム対戦の問題も出ているそうで。ふむふむ。


ざっくり考えてみた範囲では、前半は定石ベース、中盤は評価関数、終盤は全数探索による寄せというところかなあ。
前半は可能な手が異様に多く、この手のボードゲームとしてはちょっと珍しい?*1
逆に、終盤は手がほとんど無くなり、脳内全数探索ができてしまうくらい収束が早い。
そして、双方が21ピースしか持たないので、高々42手で探索が完了する。


で、今日ちょうど西尾さんとお出かけの際につらつらしゃべってみて、「やっぱ一度全数探索に挑戦してみたいのう」という方向で意見が一致(ブロックス経験値ゼロだから、定石とか全然わからんし)。
西尾さんは当然 Python なので、じゃあ俺は Ruby で書いてみよう(え? Erlang? むにゃむにゃもごもご)。


最初の2手と3手目以降はピースの置き方が全然違うので、まずは2手目までを幅優先探索するコードを書いてみた。こんな感じ。

pieces = <<PIECES
*/
**/
***/1
**
*./2
****/1
***
*../2
***
.*./3
**.
.**/4
**
**/5
*****/1
****
*.../2
****
.*../3
***
**./4
***
*.*/5
***.
..**/6
**.
.**
.*./7
**.
.**
..*/8
**.
.*.
.**/9
.*.
***
.*./10
***
*..
*../11
***
.*.
.*./12
PIECES

def coordinate(st)
  piece = []
  y=0
  st.each do |line|
    (0..line.length-1).each do |x|
      piece << [x, y] if line[x]==42
    end
    y+=1
  end
  piece
end

def hsymmetry(piece)
  h = piece.map{|coor| coor[0]}.max
  piece.map{|coor| [h-coor[0], coor[1]]}
end

def rotate(piece)
  v = piece.map{|coor| coor[1]}.max
  piece.map{|coor| [v-coor[1], coor[0]]}
end

def mirror(piece)
  piece.map{|coor| [coor[1], coor[0]]}
end

def create_piece_list(pieces)
  id = 0
  pieces.split(/\/\d*\n/).inject({}) do |m, piece|
    coor = coordinate(piece)
    variation = [t=coor, u=hsymmetry(coor)]
    variation << t = rotate(t)
    variation << t = rotate(t)
    variation << rotate(t)
    variation << u = rotate(u)
    variation << u = rotate(u)
    variation << rotate(u)
    m[id] = variation.map{|t|t.sort}.uniq
    id+=1
    m
  end
end

def first_turn(start_posi, pieces, mirror_flag)
  list = []
  pieces.each do |id, variation|
    symmetry = []
    variation.each do |piece|
      self_mirror_flag = false
      if mirror_flag
        next if symmetry.include?(piece)
        mir = mirror(piece).sort
        self_mirror_flag = (mir == piece)
        symmetry << mir
      end
      piece.each do |offset|
        posi = piece.map{|coor| [coor[0]+start_posi[0]-offset[0], coor[1]+start_posi[1]-offset[1]]}
        list << [id, posi, self_mirror_flag]
      end
    end
  end
  list
end

def create_first_list(pieces)
  rest_pieces = create_piece_list(pieces)
  list = first_turn([4, 4], rest_pieces, true)
  #puts list.length

  list.each do |item|
    id1, posi1, self_mirror_flag = item
    first_turn([9, 9], rest_pieces, self_mirror_flag).each do |id2, posi2, dummy|
      puts "#{[id1,posi1,id2,posi2].inspect}"
    end
  end
end

create_first_list(pieces)

ピースを座標化して回転対称とってズバズバ置いてみたという単純なプログラム。


ここから1手目が 225 通り、2手目が 86346 通りであることがわかった(盤面対角線対称は除いている)。
「1手目がおそらく400くらい、鏡像を同一視して半分の200。2手目はほとんど対称とならないので400のまま。かけ算して8万くらいかなあ」と思っていたので、まさにドンぴしゃ! って喜んでる場合じゃない。どひゃー。


もしも仮に3手目以降がおのおの60秒で全数探索できたとしても(まあまずありえないが)、1日は 86400 秒なので、60日(笑)。
いきなり全数探索は厳しめなようである……*2

*1:最初に打てる手、ということでは隅から始まる4人打ちの方がだいぶ少なくなる

*2:って、簡単に全数探索できる見込みがあるようなら、GPCC の問題も最低でも「ブロックス・デュオを解け(先手必勝か?)」とかになるよなあw