Created
April 23, 2012 17:51
-
-
Save kiritsuku/2472665 to your computer and use it in GitHub Desktop.
Othello/Reversi implementation in Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.util.parsing.combinator.JavaTokenParsers | |
object Shell extends App with InputHandler { | |
var game = Game.empty | |
run() | |
def run() { | |
val reader = Iterator.continually(readLine("othello> ").trim) | |
reader takeWhile ("quit" !=) filter (_.nonEmpty) foreach handleInput | |
} | |
private def handleInput(input: String) { | |
val s = System.nanoTime | |
val result = catchable { | |
if (input contains " ") { | |
val (command, args) = input splitAt (input indexOf " ") | |
handleCommand(command, args) | |
} | |
else handleCommand(input, "") | |
} | |
if (result.isLeft) | |
println("Error! "+result.left.get.getMessage) | |
println("time: " + (System.nanoTime-s)/1e6 + "ms") | |
} | |
private def handleCommand(command: String, args: String) = command match { | |
case "newGame" => createNewGame(args) | |
case "hole" => createHole(args) | |
case "move" => move(args) | |
case "print" => print() | |
case "abort" => abort() | |
case "possibleMoves" => showPossibleMoves() | |
case _ => println("command not found") | |
} | |
private def createNewGame(args: String) { | |
require(game.mode == GameOverMode, "there is already an active game") | |
game = (args parseAs int ~ int ~ config) { | |
case width ~ height ~ config => | |
Game(width, height, config) | |
} | |
if (!game.canMove) | |
calculatePass() | |
} | |
private def createHole(args: String) { | |
require(game.mode == NewMode, "can't add hole area. there is no game yet or the game is already started") | |
game = (args parseAs hole) { | |
case from ~ to => | |
require(!game.board.containsCells(from, to), "can't add hole. it is not empty") | |
game.addHole(from, to) | |
} | |
if (!game.canMove) | |
calculatePass() | |
} | |
private def move(args: String) { | |
requireGameStarted() | |
game = (args parseAs position) { | |
case pos => | |
require(game.possibleMoves contains pos, "move not possible") | |
game moveTo pos | |
} | |
if (!game.canMove) | |
calculatePass() | |
} | |
private def print() { | |
requireGameStarted() | |
val board = Array.fill(game.board.height, game.board.width)('-') | |
for ((Position(x, y), cell) <- game.board.cells) | |
board(y - 1)(x - 1) = cell.sign | |
println(board map (_.mkString) mkString "\n") | |
println("turn: "+game.curPlayer) | |
} | |
private def abort() { | |
requireGameStarted() | |
game = game.endGame | |
calculateWinner() | |
} | |
private def showPossibleMoves() { | |
requireGameStarted() | |
println("Possible moves: "+game.possibleMoves.sorted.mkString(",")) | |
} | |
private def requireGameStarted() { | |
require(game.mode == NewMode || game.mode == ActiveMode, "game not started") | |
} | |
private def calculatePass() { | |
val passed = game.passMove | |
if (!passed.canMove) calculateWinner() | |
else { | |
println(game.curPlayer+" passes") | |
game = passed | |
} | |
} | |
private def calculateWinner() { | |
game = game.endGame | |
val (white, black) = game.board.cells.values filter { Hole != } partition { White == } | |
val (numOfWhite, numOfBlack) = (white.size, black.size) | |
if (numOfWhite == numOfBlack) | |
println("Game has ended in a draw.") | |
else { | |
val winner = if (numOfWhite > numOfBlack) White else Black | |
val max = numOfWhite max numOfBlack | |
val min = numOfWhite min numOfBlack | |
println("Game Over! %s has won (%d:%d)!" format (winner, max, min)) | |
} | |
} | |
private def catchable[A](f: => A): Either[Exception, A] = | |
try Right(f) catch { | |
case e: Exception => Left(e) | |
} | |
} | |
trait InputHandler extends JavaTokenParsers { | |
class ParseHandler(in: String) { | |
def parseAs[A](p: Parser[A])(f: A => Game): Game = | |
parseAll(p, in) match { | |
case NoSuccess(msg, _) => sys.error(msg) | |
case Success(a, _) => f(a) | |
} | |
} | |
implicit def ParseHandler(in: String): ParseHandler = | |
new ParseHandler(in) | |
lazy val position: Parser[Position] = | |
"[A-Z]".r ~ "[0-9]{1,2}".r ^^ { case a ~ b => Position(a, b) } | |
lazy val hole: Parser[Position ~ Position] = | |
(position <~ ":") ~ position | |
lazy val int: Parser[Int] = | |
wholeNumber ^^ { | |
try _.toInt catch { | |
case e: NumberFormatException => sys.error("invalid number") | |
} | |
} | |
lazy val config = data | success(Nil) | |
lazy val data = repsep(cell*, ",") ^^ { | |
case List(Nil) => Nil | |
case in => in | |
} | |
lazy val spaces = elem(' ')+ | |
lazy val cell = "[WB#-]".r ^^ (Cell asCell _.head) | |
} | |
object Cell { | |
private val dataCells = List(White, Black, Hole) map (_.sign) mkString | |
val isCell: Char => Boolean = | |
dataCells contains _ | |
val isEmpty: Char => Boolean = | |
EmptyCell.sign == | |
val asCell: Char => Cell = { | |
case Black.sign => Black | |
case White.sign => White | |
case Hole.sign => Hole | |
case EmptyCell.sign => EmptyCell | |
} | |
} | |
abstract class Cell(val sign: Char) | |
case object White extends Cell('W') | |
case object Black extends Cell('B') | |
case object Hole extends Cell('#') | |
case object EmptyCell extends Cell('-') | |
abstract class GameMode | |
object NewMode extends GameMode | |
object ActiveMode extends GameMode | |
object GameOverMode extends GameMode | |
object Game { | |
val Directions = List[(Int, Int) => Position]( | |
(x, y) => (x + 1, y), (x, y) => (x + 1, y + 1), (x, y) => (x, y + 1), | |
(x, y) => (x - 1, y + 1), (x, y) => (x - 1, y), (x, y) => (x - 1, y - 1), | |
(x, y) => (x, y - 1), (x, y) => (x + 1, y - 1) | |
) | |
val StartPositions = Map[(Int, Int), Cell]( | |
0 -> 0 -> White, 1 -> 0 -> Black, 0 -> 1 -> Black, 1 -> 1 -> White | |
) | |
def empty: Game = | |
Game(GameOverMode, Board(0, 0, Map.empty), Black) | |
def apply(width: Int, height: Int, data: Seq[Seq[Cell]]): Game = { | |
def middleCells: Map[Position, Cell] = { | |
val (xMiddle, yMiddle) = (width / 2, height / 2) | |
StartPositions map { | |
case ((x, y), cell) => Position(x + xMiddle, y + yMiddle) -> cell | |
} | |
} | |
def calculateCells: Map[Position, Cell] = { | |
require(data.size == height, "invalid height") | |
require(data forall (_.size == width), "invalid width") | |
(for { | |
y <- 0 until height | |
x <- 0 until width | |
if data(y)(x) != EmptyCell | |
} yield Position(x+1, y+1) -> data(y)(x))(collection.breakOut) | |
} | |
def isValidSize(int: Int, from: Int, to: Int): Boolean = | |
int >= from && int <= to && int % 2 == 0 | |
import Board._ | |
require(isValidSize(width, MinWidth, MaxWidth), "invalid width") | |
require(isValidSize(height, MinHeight, MaxHeight), "invalid height") | |
val cells = if (data.isEmpty) middleCells else calculateCells | |
Game(NewMode, Board(width, height, cells), Black) | |
} | |
} | |
case class Game private (mode: GameMode, board: Board, curPlayer: Cell) { | |
lazy val nextPlayer: Cell = | |
if (curPlayer == White) Black else White | |
lazy val possibleMoves: List[Position] = { | |
def possiblePositions(pos: Position) = | |
Game.Directions flatMap { checkDirection(pos, _) } | |
def checkDirection(pos: Position, f: (Int, Int) => Position): Option[Position] = { | |
def isInvalid(pos: Position): Boolean = | |
!(board isInRange pos) || board.isOfPlayer(pos, curPlayer) || (board isHole pos) | |
val newPos = f(pos.x, pos.y) | |
val isCurPlayer = !board.isOfPlayer(newPos, nextPlayer) | |
if (isCurPlayer) None | |
else { | |
def loop(pos: Position): Option[Position] = | |
if (isInvalid(pos)) None | |
else if (board isFree pos) Some(pos) | |
else loop(f(pos.x, pos.y)) | |
loop(f(newPos.x, newPos.y)) | |
} | |
} | |
val cellsToProof = board.cells collect { | |
case (pos, player) if player == curPlayer => pos | |
} | |
(cellsToProof flatMap possiblePositions).toSet.toList | |
} | |
lazy val canMove: Boolean = | |
!possibleMoves.isEmpty | |
def moveTo(pos: Position): Game = { | |
def transformBy(f: (Int, Int) => Position) = { | |
def loop(pos: Position, xs: List[Position]): List[Position] = | |
if (board.isOfPlayer(pos, nextPlayer)) loop(f(pos.x, pos.y), pos :: xs) | |
else if (board.isOfPlayer(pos, curPlayer)) xs | |
else Nil | |
loop(f(pos.x, pos.y), Nil) | |
} | |
require(possibleMoves contains pos, "it is impossible to move to position "+pos) | |
val positions = pos :: (Game.Directions flatMap transformBy) | |
val newBoard = board.transformBy(positions, curPlayer) | |
copy(mode = ActiveMode, board = newBoard, curPlayer = nextPlayer) | |
} | |
def addHole(from: Position, to: Position): Game = { | |
require(!board.containsCells(from, to), "can't add hole (%s:%s). It is not empty" format (from, to)) | |
val positions = | |
for (y <- from.y to to.y; x <- from.x to to.x) | |
yield Position(x, y) | |
val newBoard = board.transformBy(positions, Hole) | |
copy(board = newBoard) | |
} | |
def passMove: Game = | |
copy(curPlayer = nextPlayer) | |
def endGame: Game = | |
copy(mode = GameOverMode) | |
} | |
object Board { | |
val MaxWidth = 26 | |
val MaxHeight = 98 | |
val MinWidth = 2 | |
val MinHeight = 2 | |
} | |
case class Board(width: Int, height: Int, cells: Map[Position, Cell]) { | |
def isFree(p: Position): Boolean = | |
isInRange(p) && !(cells contains p) | |
def isInRange(p: Position): Boolean = | |
p.x > 0 && p.x <= width && p.y > 0 && p.y <= height | |
def isOfPlayer(p: Position, player: Cell): Boolean = | |
cells.getOrElse(p, EmptyCell) == player | |
def isHole(p: Position): Boolean = | |
cells.getOrElse(p, EmptyCell) == Hole | |
def containsCells(from: Position, to: Position): Boolean = | |
if (from == to) cells.getOrElse(from, Hole) != Hole | |
else { | |
val cellsToCheck = | |
for { | |
y <- from.y until to.y | |
x <- from.x until to.x | |
pos = Position(x, y) | |
} yield cells.getOrElse(pos, Hole) | |
cellsToCheck exists (Hole !=) | |
} | |
def transformBy(positions: Seq[Position], cell: Cell): Board = { | |
val newCells = (cells /: positions) { | |
(cells, pos) => cells + (pos -> cell) | |
} | |
copy(cells = newCells) | |
} | |
} | |
object Position { | |
implicit def tuple2Position(t: (Int, Int)): Position = | |
Position(t._1, t._2) | |
def apply(x: String, y: String): Position = | |
Position(x(0).toInt - 'A' + 1, y.toInt) | |
} | |
case class Position(x: Int, y: Int) extends Ordered[Position] { | |
def compare(p: Position) = { | |
val comp = x - p.x | |
if (comp == 0) y - p.y else comp | |
} | |
override val toString = | |
('A' + x - 1).toChar + y.toString | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment