Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ A scala library for the [Tak](http://cheapass.com/tak/) board game

```scala
import com.github.daenyth.taklib._
import scalaz.\/
```

```scala
val invalid: String \/ Game = Game.fromTps("invalid")
// invalid: scalaz.\/[String,com.github.daenyth.taklib.Game] = -\/(`[' expected but `i' found)
val invalid: Either[String, Game] = Game.fromTps("invalid")
// invalid: Either[String,com.github.daenyth.taklib.Game] = Left(`[' expected but `i' found)
val game = Game.fromTps("[ 1,2,1,2,1/2,1,2,1,2/1,2,1,2,1/2,1,2,1,2/1,2,1,2,1 12 2 ]").getOrElse(throw new Exception)
// game: com.github.daenyth.taklib.Game = com.github.daenyth.taklib.Game@78c4cfdd
val winner = game.winner
Expand Down Expand Up @@ -90,15 +89,15 @@ Create a RuleSet to make new games with

```
scala> Game.ofSize(7, DefaultRules)
res0: scalaz.\/[String,com.github.daenyth.taklib.Game] = -\/(Bad game size: 7)
res0: Either[String,com.github.daenyth.taklib.Game] = scala.Left(Bad game size: 7)

// A new variant with a size-7 board that has 40 flatstones and 7 capstones per player!
scala> Game.ofSize(7, new RuleSet {
| val rules = DefaultRules.rules
| val expectedStoneColor = DefaultRules.expectedStoneColor
| val stoneCounts = DefaultRules.stoneCounts + ((7, (40, 7)))
| })
res1: scalaz.\/[String,com.github.daenyth.taklib.Game] = \/-(com.github.daenyth.taklib.Game@517564bf)
res1: Either[String,com.github.daenyth.taklib.Game] = scala.Right(com.github.daenyth.taklib.Game@517564bf)
```

## Goals
Expand All @@ -111,7 +110,7 @@ res1: scalaz.\/[String,com.github.daenyth.taklib.Game] = \/-(com.github.daenyth.
- Not aiming to be the fastest runtime - I'm not benchmarking anything until the project is much more stable.
- Stable API - for now. This is a new library, and so the api can change without notice as I find better ways to do things.
- Supporting scala.js - for now. It should be possible with little effort but it's not a priority. Patches welcome
- Cats support. Taklib will use scalaz only for the near future
- Scalaz usage. Taklib will only support cats

## Testing

Expand Down
9 changes: 4 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,23 @@ scalacOptions in (opentak, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnin

resolvers += Resolver.sonatypeRepo("releases")

val scalazVersion = "7.2.8"
val catsVersion = "0.9.0"
val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5"
val dependencies = Seq(
"org.scalaz" %% "scalaz-core" % scalazVersion,
"org.typelevel" %% "cats" % catsVersion,
parserCombinators,
"org.scala-graph" %% "graph-core" % "1.11.4"
)
val testDependencies = Seq(
"org.scalatest" %% "scalatest" % "3.0.1" % "test",
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
"org.typelevel" %% "scalaz-scalatest" % "1.1.1" % "test",
"org.scalaz" %% "scalaz-scalacheck-binding" % scalazVersion % "test"
"com.ironcorelabs" %% "cats-scalatest" % "2.2.0" % "test"
)

libraryDependencies in taklib ++= dependencies
libraryDependencies in taklib ++= testDependencies
libraryDependencies in takcli ++= Seq(
"org.scalaz" %% "scalaz-concurrent" % scalazVersion
"org.typelevel" %% "cats-effect" % "0.2"
)

resolvers in tpsserver += Resolver.sonatypeRepo("snapshots")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ object PlaytakCodec {
def encode(outgoing: Playtak.Outgoing): String =
Outgoing.encode(outgoing)
def decode(input: String): Either[String, Playtak.Incoming] =
Incoming.parseEither(Incoming.incoming, input).toEither
Incoming.parseEither(Incoming.incoming, input)

/* Abandon hope all ye who scroll below here */

object Incoming extends RegexParsers with RichParsing {
def decode(input: String): Either[String, Playtak.Incoming] =
parseEither(incoming, input).toEither
parseEither(incoming, input)

import Playtak.Incoming._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PlaytakCodecTest extends FlatSpec with Matchers with EitherValues {
import Playtak.Incoming._

def parse[A](parser: PlaytakCodec.Incoming.Parser[A], s: String): Either[String, A] =
PlaytakCodec.Incoming.parseEither(parser, s).toEither
PlaytakCodec.Incoming.parseEither(parser, s)

"client" should "parse" in {
parse(client, "Client Foop") shouldBe Right(Client("Foop"))
Expand Down
70 changes: 37 additions & 33 deletions takcli/src/main/scala/com/github/daenyth/takcli/Main.scala
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
package com.github.daenyth.takcli

import cats.effect.IO
import com.github.daenyth.taklib._

import scala.io.StdIn
import scala.util.control.NoStackTrace
import scalaz.concurrent.Task
import scalaz.syntax.monad._
import scalaz.{-\/, \/-}
import cats.syntax.flatMap._
import cats.syntax.applicativeError._

object Main {

def main(args: Array[String]): Unit =
mainT.handleWith {
case CleanExit => Task.now(println("Exiting"))
}.unsafePerformSync
mainT
.recoverWith {
case CleanExit => IO(println("Exiting"))
}
.unsafeRunSync()

def mainT: Task[Unit] = printStartup *> getInitialGame >>= runGameLoop
def mainT: IO[Unit] = printStartup >> getInitialGame >>= runGameLoop

def getInitialGame: Task[Game] = promptSize.flatMap {
def getInitialGame: IO[Game] = promptSize.flatMap {
Game.ofSize(_) match {
case -\/(err) => Task.now(println(err)) *> getInitialGame
case \/-(game) => Task.now(game)
case scala.Left(err) => IO(println(err)) >> getInitialGame
case scala.Right(game) => IO(game)
}
}

def runGameLoop(g: Game): Task[Unit] = runGameTurn(g).flatMap {
def runGameLoop(g: Game): IO[Unit] = runGameTurn(g).flatMap {
case OkMove(nextState) =>
runGameLoop(nextState)
case InvalidMove(reason) =>
Task.now(println(s"Bad move: $reason")) *> runGameLoop(g)
IO(println(s"Bad move: $reason")) >> runGameLoop(g)
case GameOver(result, finalState) =>
printGame(finalState) *> endGame(result)
printGame(finalState) >> endGame(result)
}

def endGame(end: GameEndResult): Task[Unit] = Task {
def endGame(end: GameEndResult): IO[Unit] = IO {
println("Game over!")
println(end)
}

def runGameTurn(g: Game): Task[MoveResult[Game]] =
printGame(g) *> promptAction.map(g.takeTurn)
def runGameTurn(g: Game): IO[MoveResult[Game]] =
printGame(g) >> promptAction.map(g.takeTurn)

def printGame(g: Game) = Task {
def printGame(g: Game) = IO {
val nextPlayInfo = g.turnNumber match {
case 1 => "White to play (Black stone)"
case 2 => "Black to play (White stone)"
Expand All @@ -52,35 +55,36 @@ object Main {
println()
}

def printStartup = Task {
println("TakCLI")
}
def printStartup = IO(println("TakCLI"))

def promptSize: Task[Int] =
Task {
def promptSize: IO[Int] =
IO {
print("Game size?\n > ")
StdIn.readInt()
}.handleWith {
case n: NumberFormatException => Task(println(s"Bad size: $n")) *> promptSize
}.recoverWith {
case n: NumberFormatException => IO(println(s"Bad size: $n")) >> promptSize
}

def pretty(g: Game): String = {
g.currentBoard.boardPositions.map(_.reverse).transpose.map(_.map(_.toTps).mkString("\t")).mkString("\n")
}
def pretty(g: Game): String =
g.currentBoard.boardPositions
.map(_.reverse)
.transpose
.map(_.map(_.toTps).mkString("\t"))
.mkString("\n")

def promptAction: Task[TurnAction] =
Task(StdIn.readLine("Your Move?\n > "))
def promptAction: IO[TurnAction] =
IO(StdIn.readLine("Your Move?\n > "))
.flatMap { input =>
if (input == null) { throw CleanExit } else
PtnParser
.parseEither(PtnParser.turnAction, input)
.fold(
err => Task.fail(PtnParseError(err)),
Task.now
err => IO.raiseError(PtnParseError(err)),
ta => IO.pure(ta)
)
}
.handleWith {
case PtnParseError(err) => Task(println(s"Bad move: $err")) *> promptAction
.recoverWith {
case PtnParseError(err) => IO(println(s"Bad move: $err")) >> promptAction
}
}

Expand Down
25 changes: 14 additions & 11 deletions taklib/src/main/scala/com/github/daenyth/taklib/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ import com.github.daenyth.taklib.Stone._

import scala.annotation.tailrec
import scala.collection.immutable.IndexedSeq
import scalaz.std.vector._
import scalaz.syntax.either._
import scalaz.syntax.monoid._
import scalaz.{Equal, \/}
import cats.syntax.either._
import cats.syntax.monoid._
import cats.instances.vector._
import cats.{Eq => Equal}

import scala.util.Try

object Board {
type \/[A, B] = Either[A, B]

type BoardLayout = Vector[Vector[Stack]]

/** Build a board from Tak Positional System; -\/ if tps is invalid */
def fromTps(tps: String): String \/ Board = TpsParser.parse(TpsParser.tps, tps) match {
case TpsParser.Success((board, _, _), _) => board.right
case err: TpsParser.NoSuccess => err.msg.left
/** Build a board from Tak Positional System; scala.Left if tps is invalid */
def fromTps(tps: String): Either[String, Board] = TpsParser.parse(TpsParser.tps, tps) match {
case TpsParser.Success((board, _, _), _) => board.asRight
case err: TpsParser.NoSuccess => err.msg.asLeft
}

def ofSize(size: Int): Board =
Expand All @@ -34,7 +37,7 @@ object Board {
index: BoardIndex,
stack: Stack): MoveResult[BoardLayout] = {
val (i, j) = (index.file - 1, index.rank - 1)
val stackAtIdx: MoveResult[Stack] = \/.fromTryCatchNonFatal(positions(i)(j))
val stackAtIdx: MoveResult[Stack] = Either.fromTry(Try(positions(i)(j)))
.fold(_ => InvalidMove(s"$index is not on the board"), OkMove.apply)
val newStack: MoveResult[Stack] = stackAtIdx.flatMap {
case Stack(Vector()) => OkMove(stack)
Expand Down Expand Up @@ -151,7 +154,7 @@ case class Board(size: Int, boardPositions: BoardLayout) {
}

def stackAt(index: BoardIndex): MoveResult[Stack] =
\/.fromTryCatchNonFatal(boardPositions(index.file - 1)(index.rank - 1)).fold(
Either.fromTry(Try(boardPositions(index.file - 1)(index.rank - 1))).fold(
_ => InvalidMove(s"$index is not on the board"),
OkMove(_)
)
Expand Down Expand Up @@ -224,7 +227,7 @@ case class Stack(pieces: Vector[Stone]) {

object Player {
implicit val playerInstance: Equal[Player] = new Equal[Player] {
override def equal(a1: Player, a2: Player): Boolean = a1 == a2
override def eqv(a1: Player, a2: Player): Boolean = a1 == a2
}
}
sealed trait Player {
Expand Down
50 changes: 25 additions & 25 deletions taklib/src/main/scala/com/github/daenyth/taklib/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import com.github.daenyth.taklib.RuleSet.GameRule
import scala.annotation.tailrec
import scalax.collection.GraphEdge.UnDiEdge
import scalax.collection.immutable.Graph
import scalaz.Ordering.{EQ, GT, LT}
import scalaz.std.anyVal.intInstance
import scalaz.std.option._
import scalaz.std.vector._
import scalaz.syntax.either._
import scalaz.syntax.foldable._
import scalaz.syntax.order._
import scalaz.syntax.semigroup._
import scalaz.{Equal, NonEmptyList, Semigroup, \/}
import cats.instances.int._
import cats.instances.option._
import cats.syntax.either._
import cats.syntax.order._
import cats.syntax.list._
import cats.syntax.semigroup._
import cats.{Semigroup, Eq => Equal}
import cats.data.NonEmptyList
import cats.kernel.Comparison.{EqualTo, GreaterThan, LessThan}

sealed trait GameEndResult
object GameEndResult {
implicit val gerInstance: Semigroup[GameEndResult] with Equal[GameEndResult] =
new Semigroup[GameEndResult] with Equal[GameEndResult] {
override def append(f1: GameEndResult, f2: => GameEndResult) = (f1, f2) match {
override def combine(f1: GameEndResult, f2: GameEndResult): GameEndResult = (f1, f2) match {
case (DoubleRoad, _) => DoubleRoad
case (_, DoubleRoad) => DoubleRoad
case (Draw, r: RoadWin) => r
Expand All @@ -36,7 +36,7 @@ object GameEndResult {
case (_, w: WinByResignation) => w
}

override def equal(a1: GameEndResult, a2: GameEndResult): Boolean = (a1, a2) match {
override def eqv(a1: GameEndResult, a2: GameEndResult): Boolean = (a1, a2) match {
case (DoubleRoad, DoubleRoad) => true
case (Draw, Draw) => true
case (RoadWin(p1), RoadWin(p2)) => p1 == p2
Expand Down Expand Up @@ -151,15 +151,15 @@ object DefaultRules extends RuleSet {

object Game {

def ofSize(size: Int): String \/ Game = ofSize(size, DefaultRules)
def ofSize(size: Int): Either[String, Game] = ofSize(size, DefaultRules)

def ofSize(size: Int, rules: RuleSet): String \/ Game =
def ofSize(size: Int, rules: RuleSet): Either[String, Game] =
rules.stoneCounts.keySet
.contains(size)
.guard(s"Bad game size: $size")
.map { _ =>
val b = Board.ofSize(size)
new Game(size, 1, rules, NonEmptyList((StartGameWithBoard(b), b)))
new Game(size, 1, rules, NonEmptyList((StartGameWithBoard(b), b), Nil))
}

// Start at turn 3 to make the "play opponent's stone" rule easier
Expand All @@ -168,19 +168,19 @@ object Game {
board.size,
turnNumber,
DefaultRules,
NonEmptyList((StartGameWithBoard(board), board))
NonEmptyList((StartGameWithBoard(board), board), Nil)
)

def fromPtn(ptn: String): String \/ MoveResult[Game] =
def fromPtn(ptn: String): Either[String, MoveResult[Game]] =
PtnParser.parseEither(PtnParser.ptn(DefaultRules), ptn).map(_._2)

def fromTps(tps: String): String \/ Game =
def fromTps(tps: String): Either[String, Game] =
TpsParser.parse(TpsParser.tps, tps) match {
case TpsParser.Success((board, turn, nextPlayer), _) =>
// We use one turn for each player's action, Tps uses turn as a move for both players with a move counter between them
val turnNumber = (2 * turn) + nextPlayer.fold(1, 0)
Game.fromBoard(board, turnNumber).right
case err: TpsParser.NoSuccess => err.msg.left
Game.fromBoard(board, turnNumber).asRight
case err: TpsParser.NoSuccess => err.msg.asLeft
}

}
Expand Down Expand Up @@ -208,7 +208,7 @@ class Game private (val size: Int,
def takeTurn(action: TurnAction): MoveResult[Game] =
rules.check(this, action).getOrElse {
currentBoard.applyAction(nextPlayer, action).flatMap { nextState =>
val newHistory = (action, nextState) <:: history
val newHistory = (action, nextState) :: history
val game = new Game(size, turnNumber + 1, rules, newHistory)
game.winner match {
case Some(gameEnd) => GameOver(gameEnd, game)
Expand All @@ -235,7 +235,7 @@ class Game private (val size: Int,
}

def winner: Option[GameEndResult] =
(roads: Vector[GameEndResult]).suml1Opt |+| flatWin
Semigroup[GameEndResult].combineAllOption(roads) |+| flatWin

private def roads: Vector[RoadWin] = {
def mkGraph(xs: Set[BoardIndex]): Graph[BoardIndex, UnDiEdge] = {
Expand Down Expand Up @@ -297,10 +297,10 @@ class Game private (val size: Int,
if (!emptySpaceAvailable
|| whiteCount == reserve
|| blackCount == reserve) {
Some(whiteFlats cmp blackFlats match {
case LT => FlatWin(Black)
case EQ => Draw
case GT => FlatWin(White)
Some(whiteFlats comparison blackFlats match {
case LessThan => FlatWin(Black)
case EqualTo => Draw
case GreaterThan => FlatWin(White)
})
} else None
case stack :: rest =>
Expand Down
Loading