Skip to content

Instantly share code, notes, and snippets.

@kiritsuku
Created April 23, 2012 17:51
Show Gist options
  • Save kiritsuku/2472665 to your computer and use it in GitHub Desktop.
Save kiritsuku/2472665 to your computer and use it in GitHub Desktop.
Othello/Reversi implementation in Scala
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