Last active
October 3, 2015 14:48
-
-
Save kiritsuku/2472666 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 | |
import scala.util.Try | |
/* | |
* Compiles only with 2.10 | |
*/ | |
object Othello extends App with InputHandler { | |
private[this] var game = Game.empty | |
run() | |
def run() { | |
val reader = Iterator.continually(readLine("othello> ").trim) | |
reader takeWhile (c => c != "quit" && c != "q") filter (_.nonEmpty) foreach handleInput | |
} | |
private def handleInput(input: String) { | |
val s = System.nanoTime | |
val result = Try { | |
if (!input.contains(" ")) | |
handleCommand(input, "") | |
else { | |
val (command, args) = input splitAt (input indexOf " ") | |
handleCommand(command, args.tail) | |
} | |
} | |
val msg = result match { | |
case util.Failure(e) => s"Error! ${e.getMessage}" | |
case util.Success(_) => s"time: ${(System.nanoTime-s) / 1e6}ms" | |
} | |
println(msg) | |
} | |
private def handleCommand(command: String, args: String) = command match { | |
case "newGame" | "n" => createNewGame(args) | |
case "hole" | "h" => createHole(args) | |
case "move" | "m" => move(args) | |
case "print" | "p" => print() | |
case "abort" | "a" => abort() | |
case "possibleMoves" | "pm" => showPossibleMoves() | |
case _ => println(s"command '$command' not found") | |
} | |
private def createNewGame(args: String) { | |
require(game.mode == GameOverMode, "there is already an active game") | |
game = (args parseWith 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 parseWith 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 parseWith 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(s"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) | |
val output = | |
if (numOfWhite == numOfBlack) "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 | |
s"Game Over! $winner has won ($max:$min)" | |
} | |
println(output) | |
} | |
} | |
trait InputHandler extends JavaTokenParsers { | |
import scala.language.postfixOps | |
implicit final class ParseHandler(in: String) { | |
def parseWith[A, B](p: Parser[A])(f: A => B): B = | |
parseAll(p, in) match { | |
case NoSuccess(msg, input) => error(msg, input) | |
case Success(a, _) => f(a) | |
} | |
} | |
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 >> { n => | |
Try(n.toInt).toOption.fold(failure("invalid number"): Parser[Int])(success) | |
} | |
lazy val config: Parser[List[List[Cell]]] = | |
data | success(Nil) | |
lazy val data: Parser[List[List[Cell]]] = | |
repsep(cell*, ",") ^^ { | |
case List(Nil) => Nil | |
case in => in | |
} | |
lazy val cell: Parser[Cell] = | |
"[WB#-]".r ^^ (Cell asCell _.head) | |
private def error(msg: String, input: Input) = { | |
val pos = input.pos.column-1 | |
val xs = Seq(msg, "\n\t", input.source, "\n\t", " "*pos, "^") | |
sys.error(xs.mkString) | |
} | |
} | |
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 of input data") | |
require(data forall (_.size == width), "invalid width of input data") | |
(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, s"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), s"can't add hole ($from:$to). It is not empty") | |
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 { | |
import scala.language.implicitConversions | |
implicit def tuple2Position(t: (Int, Int)): Position = | |
Position(t._1, t._2) | |
def apply(x: String, y: String): Position = | |
Position(x.charAt(0).toInt-'A'+1, y.toInt) | |
implicit val ordering: Ordering[Position] = new Ordering[Position] { | |
def compare(p1: Position, p2: Position) = { | |
val comp = p1.x-p2.x | |
if (comp == 0) p1.y-p2.y else comp | |
} | |
} | |
} | |
case class Position(x: Int, y: Int) { | |
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