Scalaでリバーシ(オセロ)

正月はScalaをやってました。
リバーシを題材に、「コップ本」片手に悪戦苦闘をしてました。
添削歓迎!

/**
 * リバーシ ver4.0
 */

/** 位置 */
case class Pos(argx:Int, argy:Int) {
    val x = argx
    val y = argy
    def add(dir:Dir):Pos = Pos(x+dir.dx, y+dir.dy)
}

/** 方向 */
case class Dir(argdx:Int, argdy:Int) {
    val dx = argdx
    val dy = argdy
}

/** 共通定義 */
trait ReversiCommon {
    /** 盤面サイズ */
    val BoardSize = 8

    val Empty = ' '        // 空
    val Black = '●'        // 黒
    val White = '○'        // 白
    val Wall  = '壁'        // 壁
    /**
     *  方向(dx dy)のリスト
     * (右 右上 上 左上 左 左下 下 右下)
     * を意味する.
     */
    val directions = List(
            Dir(0,1),   Dir(-1,1),  Dir(-1,0),  Dir(-1,-1),
            Dir(0,-1),  Dir(1,-1),  Dir(1,0),   Dir(1,1));
}

/** 盤面。まわりを壁で囲っている。 */
class Board extends ReversiCommon {
    var board = new Array[Array[Char]](BoardSize+2)
    for(u <- 0 to BoardSize+1) {
        board(u) = new Array[Char](BoardSize+2)
        for(v <- 0 to BoardSize+1) {
            board(u)(v) = 
                (u,v) match {
                case (0,_)  => Wall
                case (9,_)  => Wall
                case (_,0)  => Wall
                case (_,9)  => Wall
                case (4,4)  => White
                case (5,5)  => White
                case (5,4)  => Black
                case (4,5)  => Black
                case _      => Empty
            }
        }
    }

    def get(x:Int, y:Int):Char = board(x)(y)
    def get(pos:Pos):Char = get(pos.x, pos.y)
    def put(x:Int, y:Int, c:Char):Unit = board(x)(y) = c
    def put(pos:Pos, c:Char):Unit = put(pos.x, pos.y, c)
    // 白や黒の駒の数を返す。Emptyを指定すると空きの数を返す。
    def count(color:Char):Int = {
        var result = 0
        for (line <- board) {
            // 各行をリストに変え、要素がcolorの数を取得する。
            result += line.toList.count(_==color)
        }
        result
    }
    override def toString = {
        // 個々の行は "" ではさんで、行間は改行ではさむ。
        board.map(line=>line.mkString("")).mkString("\n")
    }
    /** 盤面を複製する */
    override def clone():Board = {
        var result = new Board()
        for (y <- 1 to BoardSize) {
            for (x <- 1 to BoardSize) {
                result.put(x, y, this.get(x,y))
            }
        }
        result
    }
    /** 候補を数字で書く。現在の盤面は変更せず、新しい盤面を作って返す。 */
    def createCandidate(hands:List[Pos]):Board = {
        var result = clone
        val chars = "????????????????????".toCharArray()    // 「○に1」から「○に20」まで
//      val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
        var index = 0
        for (hand <- hands) {
            result.put(hand, chars(index))
            index += 1
        }
        result
    }
}

/** ゲームのルールをつかさどるクラス */
class ReversiGame extends ReversiCommon {
    /** 相手の色を返す。 */
    def getOpponent(c:Char) = if (c == Black) White else Black
    /**
     * 1方向に関して駒の挟むことのできる範囲を返す.
     * @param board 盤面
     * @param bw    手番
     * @param pos   駒を置く場所
     * @param dir   調べる方向
     */
    private def getFlippablePos(board:Board, bw:Char, pos:Pos, dir:Dir): Option[Pos] = {
        assert(board != null)
        assert(bw==Black || bw==White, bw)      // 駒は白か黒のどちらか
        assert(board.get(pos)==Empty, board)    // そこは駒が置けない。
        val opp = getOpponent(bw)
        var p = pos add dir
        while (board.get(p) == opp) {
            val next = p add dir
            if (board.get(next) == bw) return Some(p)
            p = next
        }
        None    // この方向には挟めない。
    }
    /**
     * 駒を挟める方向を列挙する
     * @param board 盤面
     * @param bw    手番
     * @param pos   駒を置く場所
     * @return  方向とひっくり返す終端のリスト
     */
    def getFlippableList(board:Board, bw:Char, pos:Pos): List[(Dir,Pos)] = {
        var result = List[(Dir,Pos)]()
        for (dir <- directions) {
            val p = getFlippablePos(board, bw, pos, dir)
            p match {
                case Some(pos)  => result ::= (dir,pos)
                case None       =>
            }
        }
        result
    }
    /** 候補手のリストを生成する.*/
    def getMovableHands(board:Board, bw:Char): List[Pos] = {
        var result = List[Pos]()
        for(y <- 1 to BoardSize) {
            for(x <- 1 to BoardSize) {
                if (board.get(x,y)==Empty) {
                    val pos = Pos(x,y)
                    // ひっくり返せるなら候補に追加する。
                    if (getFlippableList(board, bw, pos).size > 0) {
                        result ::= pos
                    }
                }
            }
        }
        result
    }

    /**
     * 駒を置いて反転処理を行う。
     * 新しい盤面を作成して返す。元の盤面は変更されない。
     * @param board 盤面
     * @param bw    手番
     * @param pos   駒を置く場所。必ず置ける場所を指定すること。
     * @return      次の盤面
     */
    def flipPieces(board:Board, bw:Char, pos:Pos): Board = {
        assert(board != null)
        assert(bw==Black || bw==White, bw)
        // ひっくり返せる位置と方向をリストで取得する。
        val dirposlist = getFlippableList(board, bw, pos)
        assert(dirposlist.size > 0, 
                "board=\n"+board.toString+"\nbw="+bw+" pos="+pos)
        // 戻り値の盤面を作成
        var result = board.clone()
        // まずは、その位置に駒を置く。
        result.put(pos, bw)
        // ひっくり返す
        for ((dir,end) <- dirposlist) {
            var p=pos
            do {
                p = p add dir
                result.put(p, bw)
            }while (p != end)
        }
        result  // 作成した盤面を返す。
    }

    /**
     * 指定された2つのプレーヤーで対戦を行う。
     * @return  勝者を返す。引き分けの場合は、Noneを返す。
    */
    def play(blackPlayer:Player, whitePlayer:Player): Option[Player] = {
        def getOpponentPlayer(player:Player) =
            if (player==blackPlayer) whitePlayer else blackPlayer
        var board = new Board()
        var player = blackPlayer        // 黒が先攻
        var bw = Black
        var lastHand:Option[Pos] = None     // 直前の手
        var endflag = false
        while (! endflag) {
            // プレーヤーが次の手を決める。
            println(board)
            printPrompt(bw, player)
            val sel = player.getHand(board, bw)
            sel match {
                case Some(hand) => {
                    println(hand)
                    if (getFlippableList(board, bw, hand).size==0) {
                        println("その場所は置けません。反則負けです。\n"+
                            board+bw+hand)
                        return Some(getOpponentPlayer(player))
                    }
                    // 次の手があれば駒を置いてひっくり返す。
                    board = flipPieces(board, bw, hand)
                }
                case None => {
                    println("PASS")
                    // もし、直前の手もPASSならば終了。
                    // これは、両者がいけない場合と、すべて埋まった場合を兼ねる。
                    endflag = (lastHand==None)
                }
            }
            lastHand = sel
            player = getOpponentPlayer(player)  // 攻守交替
            bw = getOpponent(bw)
        }
        // 両者の駒の数を数える。
        val b_count = board.count(Black)
        val w_count = board.count(White)
        val winner:Option[Player] = 
            if (b_count > w_count)      Some(blackPlayer)
            else if (b_count < w_count) Some(whitePlayer)
            else None
        println(Black+"Black:"+blackPlayer+"="+b_count+" "+White+"White:"+whitePlayer+"="+w_count)
        winner match {
            case Some(player)   => println("winner="+player)
            case None           => println("draw")
        }
        winner
    }
    def printPrompt(bw:Char, player:Player):Unit = {
        print(""+bw+player+">>")
    }
}

/** 抽象プレイヤー */
trait Player extends ReversiGame {
    /** 手を返す。PASSのときにはNoneを返す。*/
    def getHand(board:Board, bw:Char): Option[Pos]
}

/** 人間のプレイヤー */
class ManPlayer extends Player {
    def getHand(board:Board, bw:Char): Option[Pos] = {
        assert(board != null)
        assert(bw == Black || bw == White, bw)
        val hands = getMovableHands(board, bw)
        if (hands.size == 0) return None    // 置ける場所がない場合は自動でPASS
        // 候補を表示
        println("候補を表示")
        println(board.createCandidate(hands))
        println("候補の中から選んで、番号を入力してください。(Ctrl-Cで中断)")
        var hand:Pos = null
        while (hand==null) {
            printPrompt(bw, this)
            // キーボードから文字列を入力。数値に変換。
            try {
                import java.io._
                val line = new BufferedReader(new InputStreamReader(System.in)).readLine()
                val sel = Integer.parseInt(line) - 1
                hand = hands(sel)
            } catch {
                case _ => println("入力エラー!")
            }
        }
        Some(hand)
    }
    override def toString = "ManPlayer"
}

/** 次の手を、常に最初に見つけた候補を選ぶ間抜けなプレイヤー */
class StupidPlayer extends Player {
    def getHand(board:Board, bw:Char): Option[Pos] = {
        assert(board != null)
        assert(bw == Black || bw == White, bw)
        val hands = getMovableHands(board, bw)
        if (hands.size == 0) return None    // 置ける場所がない場合、PASS
        // 常に最初の手を選ぶ。
        Some(hands(0))
    }
    override def toString = "StupidPlayer"
}

/** 次の手を、候補手の中からランダムに選ぶ間抜けなプレイヤー */
class RandomPlayer extends Player {
    def getHand(board:Board, bw:Char): Option[Pos] = {
        assert(board != null)
        assert(bw == Black || bw == White, bw)
        val hands = getMovableHands(board, bw)
        if (hands.size == 0) return None    // 置ける場所がない場合、PASS
        // ランダムに選ぶ
        val hand = hands(randomInt(hands.size))
        Some(hand)
    }
    /** 0..max-1までの乱数 */
    private def randomInt(max:Int):Int = Math.floor(Math.random*max).toInt
    override def toString = "RandomPlayer"
}

/** 次の手で、最大の駒数になるように選ぶ、幼稚なプレイヤー */
class NextHandPlayer extends Player {
    def getHand(board:Board, bw:Char): Option[Pos] = {
        assert(board != null)
        assert(bw == Black || bw == White, bw)
        val hands = getMovableHands(board, bw)
        if (hands.size == 0) return None    // 置ける場所がない場合、PASS
        // 最大の手を捜す。
        var bestRate = -1000
        var bestHand:Pos = null
        for (hand <- hands) {
            val next_board = flipPieces(board, bw, hand)
            val rate = next_board.count(bw) // 自分の駒が多ければよい。
            if (bestRate < rate) {
                bestRate = rate
                bestHand = hand
            }
        }
        Some(bestHand)
    }
    override def toString = "NextHandPlayer"
}
/** MinMax法を使って選ぶ、多少マシなプレイヤー */
abstract class GenericMinMaxPlayer extends Player {
    /** 評価関数 */
    def eval(board:Board, bw:Char): Int

    def getHand(board:Board, bw:Char): Option[Pos] = {
        assert(board != null)
        assert(bw == Black || bw == White, bw)
        getMinMaxHand(board, bw, bw, 3)._1
    }
    /** PASSがあるので難しい・・・ */
    private def getMinMaxHand(board:Board, bw:Char, self:Char, depth:Int): (Option[Pos], Int) = {
        def minmax(list:List[(Pos,Int)]): (Pos,Int) = {
            var (bestHand,bestRate) = list.head
            for ((hand,rate) <- list.tail) {
                if ((bw==self && bestRate > rate) || (bw!=self && bestRate < rate)) {
                    bestHand = hand
                    bestRate = rate
                }
            }
            (bestHand, bestRate)
        }
        // 深さが1なら、次の手で最善の手を返す。
        if (depth==0) {
            val hands = getMovableHands(board, bw)
            if (hands.size == 0) return (None, eval(board, bw)) // 置ける場所がない場合、PASS
            val handratelist = hands.map(hand => {
                val next_board = flipPieces(board, bw, hand)
                val rate = eval(next_board, bw)
                (hand,rate)
            })
            val (bestHand,bestRate) = minmax(handratelist)
            (Some(bestHand),bestRate)
        }
        else {
            val hands = getMovableHands(board, bw)
            val opp = getOpponent(bw)
            // 置ける場所がない場合、パスして次の候補を探す。
            if (hands.size == 0) {
                val (pos,rate) = getMinMaxHand(board, opp, self, depth-1)
                return (None,rate)
            }
            // それぞれの盤面に対して評価値と手を取得し、最大の手を探す。
            val handratelist = hands.map(hand => {
                val next_board = flipPieces(board, bw, hand)
                val (pos,rate) = getMinMaxHand(next_board, opp, self, depth-1)
                (hand,rate)
            })
            val (bestHand,bestRate) = minmax(handratelist)
            (Some(bestHand),bestRate)
        }
    }
    override def toString = "GenericMinMaxPlayer"
}

/** MinMax法を使って選ぶ、多少マシなプレイヤー */
class SimpleMinMaxPlayer extends GenericMinMaxPlayer {
    /** 評価関数 */
    def eval(board:Board, bw:Char): Int =
        board.count(bw) // 自分の駒が多ければよい。
    override def toString = "SimpleMinMaxPlayer"
}

/** MinMax法を使って選ぶ、多少手強いプレイヤー */
class ClevarMinMaxPlayer extends GenericMinMaxPlayer {
    /** 重み付けテーブル */
    val Weight = Array(
        Array(120, -20, 20,   5,   5,  20, -20, 120),
        Array(-20, -40, -5,  -5,  -5,  -5, -40, -20),
        Array( 20,  -5, 15,   3,   3,  15,  -5,  20),
        Array(  5,  -5,  3,   3,   3,   3,  -5,   5),
        Array(  5,  -5,  3,   3,   3,   3,  -5,   5),
        Array( 20,  -5, 15,   3,   3,  15,  -5,  20),
        Array(-20, -40, -5,  -5,  -5,  -5, -40, -20),
        Array(120, -20, 20,   5,   5,  20, -20, 120))
    /**
     * 評価関数。
     * 参考 http://hitsujiai.blog48.fc2.com/blog-entry-26.html
     */
    def eval(board:Board, bw:Char): Int = {
        val opp = getOpponent(bw)
        var value = 0
        for (y <- 1 to BoardSize) {
            for (x <- 1 to BoardSize) {
                val col = board.get(x, y)
                if (col == bw)
                    value += Weight(y-1)(x-1)
                else if (col == opp)
                    value -= Weight(y-1)(x-1)
            }
        }
        value
    }
    override def toString = "ClevarMinMaxPlayer"
}

object ReversiPlay extends ReversiGame {
    def buttle10(player1:Player, player2:Player):Unit = {
        // 10回対戦してどちらのプレイヤーが強いか調べる。
        val ButtleCount = 4
        var count1 = 0
        var count2 = 0
        for (i <- 1 to ButtleCount/2) {
            val winner1 = play(player1, player2)
            winner1 match {
                case Some(`player1`)    =>  count1 += 1
                case Some(`player2`)    =>  count2 += 1
                case None               =>
            }
            // 先攻後攻を交替でもう一度
            val winner2 = play(player2, player1)
            winner2 match {
                case Some(`player1`)    =>  count1 += 1
                case Some(`player2`)    =>  count2 += 1
                case None               =>
            }
        }
        println("score "+player1+"="+count1+" "+player2+"="+count2+
                " draw="+(ButtleCount-count1-count2))
    }

    def main(args: Array[String]) {
        if (args.size > 0 && args(0)=="demo") {
            buttle10(new SimpleMinMaxPlayer(), new ClevarMinMaxPlayer())
        } else {
            val b_player = new ManPlayer()
            val w_player = new ClevarMinMaxPlayer()
            play(b_player, w_player)
        }
    }
}