Rubyでリバーシ(オセロ)
Rubyでリバーシを作ってみました。(Reversi.rb)
まずは、リバーシのルールを実装し、同一マシンで人vs人ができます。
=begin Reversi Haskellの習得のため、Schemeで書かれたリバーシ(オセロ)をHaskellに移植してみる。 Rubyの習得のため、Haskellで書かれたリバーシ(オセロ)をRubyに移植してみる。:-) 参考: http://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B7 http://www.stdio.h.kyoto-u.ac.jp/~hioki/gairon-enshuu/kadai2005/7.html 盤面上のマスは"x y"で指定する(1<= x,y <= 8). リバーシは両者ともコマが置けないパターンが存在するので注意。 =end # リバーシ class Reversi SIZE = 8 # 盤面サイズ # 8方向 DIRS = [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]] # 見栄え IMG = {:wall=>"■", :white=>"●", :black=>"○", :empty=>" "} # 盤面の初期化 # 境界に「壁」をダミーとして置いてある. def initialize() @board = Array.new(SIZE+2) for y in 0..SIZE+1 @board[y] = Array.new(SIZE+2) for x in 0..SIZE+1 if (x==5 and y==5) or (x==4 and y==4) then @board[y][x] = :white elsif (x==5 and y==4) or (x==4 and y==5) then @board[y][x] = :black elsif x==0 or x==SIZE+1 or y==0 or y==SIZE+1 then @board[y][x] = :wall else @board[y][x] = :empty end end end end # まず、get/setを定義する。それ以外のメソッドはコレを使う。 # 指定された位置のコマを返す。 def get(x,y) @board[y][x] end # コマを指定した位置に置く def set(x,y,col) @board[y][x] = col end # 盤面の内容を文字列にして返す。 def to_s() s = "" for y in 0..SIZE+1 for x in 0..SIZE+1 s += IMG[get(x,y)] end s += "\n" end s end # その場所におけるか? def can_put(x,y,col) # 引数の範囲チェック if not ((1..SIZE).include?(x) and (1..SIZE).include?(y)) then return false end # すでにコマがあるとダメ if get(x,y) != :empty then return false end # 8方向それぞれについて for dir in DIRS # ひっくり返せる場所があればOK pos = get_reverse_end_pos(x, y, dir, col) if pos != nil then return true end end # なければダメ return false end # 指定した位置にコマを置く def put(x, y, col) # まずその位置にコマを置く set(x, y, col) # 相手の色 enemy = (col == :black) ? :white : :black # 8方向それぞれについて for dir in DIRS # ひっくり返せる場所があるか? pos = get_reverse_end_pos(x, y, dir, col) # あったら、その場所までを自分のコマに置き換える。 if pos then rx = x + dir[0] ry = y + dir[1] until rx == pos[0] and ry == pos[1] do set(rx, ry, col) rx += dir[0] ry += dir[1] end end end self end # 指定された位置に置いたとき、その方向でひっくり返せるところまでの位置を返す。 # 例えば、5,3に白をおいて、-1,-1方向に黒が並び、その先に白があれば、その白の位置を返す。 # ひっくり返せなかればnilを返す。 def get_reverse_end_pos(x, y, dir, col) # 相手の色 enemy = (col == :black) ? :white : :black # そこに相手のコマがあれば、 if get(x+dir[0], y+dir[1]) == enemy then # その先に自分のコマがあるかどうか調べる。 px = x + dir[0] py = y + dir[1] while get(px, py) == enemy do px += dir[0] py += dir[1] if get(px, py) == col then return [px, py] # あった! end end end nil # なかった・・・ end # 指定されたコマの数を返す。 def get_count(col) count = 0 for y in 0..SIZE+1 for x in 0..SIZE+1 if get(x,y) == col then count += 1 end end end count end # すべてを埋め尽くしたか?置く場所がなくなったか? def is_game_over() for y in 0..SIZE+1 for x in 0..SIZE+1 # どちらかが置けるなら、まだゲームは続く。 if can_put(x,y,:white) or can_put(x,y,:black) then return false end end end true # 白黒、どちらも置ける場所がなかった end # 着手可能な手を集める def collect_hand(col) hands = [] for y in 1..SIZE for x in 1..SIZE # 置けるなら追加 hands << [x,y] if can_put(x,y,col) end end hands end # 深い複製 def deep_clone() result = Reversi.new for y in 0..SIZE+1 for x in 0..SIZE+1 result.set(x,y,get(x,y)) end end result end end # Playerは、盤面をみて手を返す。 class Player attr_reader :color def initialize(color) @color = color end # 必ず、以下のメソッドをオーバライドすること # @param board Reversiオブジェクト。nil不可 # @return x,yを格納したリスト。要素はint # あるいは、 :pass または :give_up のシンボルを返す。 def move(board) :give_up end end class ManPlayer < Player # 座標を入力する。正しくない入力の時にはやり直す。 # "3 5" => 3,5 # "pass" => :pass # "give up" または Ctrl+Zのときには :give_up を返す。 def move(board) while true do s = gets.chomp if s == "pass" then return :pass end if s == nil or s == "give up" then return :give_up end xs,ys = s.split() x = xs.to_i y = ys.to_i if board.can_put(x, y, @color) then break end puts "そこには置けないか、入力方法が違っています。" print prompt(@color) end [x, y] end end def prompt(col) Reversi::IMG[col] + "#{col}>> " end # enjoy! def play(b_player, w_player) reversi = Reversi.new() player = b_player # 先行は黒 puts "座標は '横 縦'で1から8の数字で入力(例:'3 4')。'pass'、'give up'、Ctrl+Zで終了。" last_hand = nil until reversi.is_game_over col = player.color # 盤面とプロンプトを表示 puts reversi print prompt(col) # 入力 hand = player.move(reversi) if hand == :give_up then break end if last_hand == :pass and hand == :pass then break end if hand == :pass then puts "pass" else # 指定された座標にコマを置く。 x,y = hand puts "#{x} #{y}" reversi.put(x, y, col) end # プレイヤーを入れ替える。 player = (player == b_player) ? w_player : b_player last_hand = hand end puts reversi b_count = reversi.get_count(:black) w_count = reversi.get_count(:white) puts Reversi::IMG[:black] + "#{:black} #{b_count}" puts Reversi::IMG[:white] + "#{:white} #{w_count}" case b_count <=> w_count when 1 winner = :black when -1 winner = :white when 0 winner = :draw end puts "winner:#{winner}" winner end def test_man_vs_man() play(ManPlayer.new(:black), ManPlayer.new(:white)) end
次に、簡単な人工知能(?)を作ってみました。(ReversiAI.rb)
require "Reversi" # マヌケなプレイヤー # 打てる手をランダムに打つ。 class CrazyPlayer < Player def move(board) # 着手可能な手を集める hands = board.collect_hand(@color) if hands.empty? then return :pass end # ランダムに返す。 hands[rand(hands.length)] end end # 一手先しか読めないプレイヤー # 着手可能な手のなかで一番多くひっくり返せる手を打つ。 class BeginnerPlayer < Player def move(board) # 着手可能な手を集める hands = board.collect_hand(@color) if hands.empty? then return :pass end # その中で一番いい手を捜す。 max_count = 0 best_hand = nil for h in hands c = board.deep_clone().put(h[0],h[1],@color).get_count(@color) if c > max_count then best_hand = h max_count = c end end best_hand end end # 2つのプレイヤーに100回勝負させて、勝率の多い方を求める。 # BeginnerPlayer(黒)とCrazyPlayer(白)だと black 64 white 32 draw 4 になる。 # 数分かかるので注意。 # CrazyPlayer以外は乱数を使わないので、それら同士が対戦すると、 # 常に同じ結果になってしまう。 def test100() player_b = BeginnerPlayer.new(:black) player_w = CrazyPlayer.new(:white) b_count = 0 w_count = 0 draw_count = 0 100.times do winner = play(player_b, player_w) case winner when :black b_count += 1 when :white w_count += 1 when :draw draw_count += 1 end end puts "black #{b_count} white #{w_count} draw #{draw_count}" end
とりあえず、Rubyで100行ぐらいのソースが書けるようになりました。
まだまだ洗練されていないです。