Skip to content

Instantly share code, notes, and snippets.

@kiritsuku
Last active October 3, 2015 14:48
Show Gist options
  • Save kiritsuku/2472666 to your computer and use it in GitHub Desktop.
Save kiritsuku/2472666 to your computer and use it in GitHub Desktop.
Othello/Reversi implementation in Scala
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