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行ぐらいのソースが書けるようになりました。
まだまだ洗練されていないです。