From 7fcf2a01518bc42b95498cc3625a7c27307fdb2a Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Sat, 20 May 2017 10:19:46 -0400 Subject: [PATCH 1/5] Convert taklib scalaz to cats --- README.md | 11 ++-- build.sbt | 9 ++-- .../com/github/daenyth/takcli/Main.scala | 4 +- .../com/github/daenyth/taklib/Board.scala | 25 ++++++---- .../com/github/daenyth/taklib/Game.scala | 50 +++++++++---------- .../com/github/daenyth/taklib/Implicits.scala | 26 +++++----- .../com/github/daenyth/taklib/Move.scala | 19 +++++-- .../com/github/daenyth/taklib/PtnParser.scala | 31 ++++++------ .../com/github/daenyth/taklib/TpsParser.scala | 5 +- .../com/github/daenyth/taklib/GameTest.scala | 2 +- 10 files changed, 98 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 6a8f64c..eed2990 100644 --- a/README.md +++ b/README.md @@ -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 @@ -90,7 +89,7 @@ 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 { @@ -98,7 +97,7 @@ scala> Game.ofSize(7, new RuleSet { | 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 @@ -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 diff --git a/build.sbt b/build.sbt index eebd5f3..47c8e42 100644 --- a/build.sbt +++ b/build.sbt @@ -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") diff --git a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala index 74f7878..a2779f1 100644 --- a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala +++ b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala @@ -18,8 +18,8 @@ object Main { def getInitialGame: Task[Game] = promptSize.flatMap { Game.ofSize(_) match { - case -\/(err) => Task.now(println(err)) *> getInitialGame - case \/-(game) => Task.now(game) + case scala.Left(err) => Task.now(println(err)) *> getInitialGame + case scala.Right(game) => Task.now(game) } } diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala index 8577701..ca47e3c 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala @@ -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 = @@ -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) @@ -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(_) ) @@ -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 { diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala index bed94f2..64c73f7 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala @@ -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 @@ -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 @@ -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 @@ -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 } } @@ -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) @@ -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] = { @@ -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 => diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala index 84daa8f..16d0c8f 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala @@ -1,13 +1,12 @@ package com.github.daenyth.taklib import scala.util.parsing.combinator.RegexParsers -import scalaz.{-\/, \/, \/-} -import scalaz.syntax.either._ +import cats.syntax.either._ object Implicits { implicit class RichBoolean(b: Boolean) { - def guard[E](whenFalse: => E): E \/ Unit = - if (b) ().right else whenFalse.left + def guard[E](whenFalse: => E): Either[E, Unit] = + if (b) ().asRight else whenFalse.asLeft def orElse[A](ifTrue: => A): Option[A] = if (!b) Some(ifTrue) else None @@ -16,23 +15,23 @@ object Implicits { trait RichParsing { this: RegexParsers => // Credit to http://stackoverflow.com/a/27513509/350351 implicit class RichParser[+T](p: Parser[T]) { - def ^^?[U](f: T => (String \/ U)): Parser[U] = new Parser[U] { + def ^^?[U](f: T => (Either[String, U])): Parser[U] = new Parser[U] { def apply(in: Input) = p(in) match { case Success(x, in1) => f(x) match { - case -\/(error) => Failure(error, in1) - case \/-(x1) => Success(x1, in1) + case scala.Left(error) => Failure(error, in1) + case scala.Right(x1) => Success(x1, in1) } case failure: Failure => failure case error: Error => error } } - def >>=?[U](f: T => (String \/ Parser[U])): Parser[U] = p.flatMap { x => + def >>=?[U](f: T => (Either[String, Parser[U]])): Parser[U] = p.flatMap { x => f(x) match { - case -\/(err) => + case scala.Left(err) => new Parser[U] { def apply(in: Input): ParseResult[U] = Failure(err, in) } - case \/-(parser) => parser + case scala.Right(parser) => parser } } } @@ -54,11 +53,10 @@ object Implicits { def ???>[T](p: Parser[T]) = new DebugParser(name, p) } - def parseEither[T](parser: this.Parser[T], input: String): String \/ T = + def parseEither[T](parser: this.Parser[T], input: String): Either[String, T] = this.parse(parser, input) match { - case Success(result, _) => \/.right(result) - case err: NoSuccess => - \/.left(err.msg) + case Success(result, _) => result.asRight + case err: NoSuccess => err.msg.asLeft } } } diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala b/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala index ac12cd8..21460d7 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala @@ -1,7 +1,9 @@ package com.github.daenyth.taklib import scala.util.control.NoStackTrace -import scalaz.Monad +import cats.Monad + +import scala.annotation.tailrec sealed trait GameAction @@ -66,10 +68,21 @@ object MoveResult { override def map[A, B](fa: MoveResult[A])(f: (A) => B): MoveResult[B] = fa.map(f) - override def bind[A, B](fa: MoveResult[A])(f: (A) => MoveResult[B]): MoveResult[B] = + override def flatMap[A, B](fa: MoveResult[A])(f: (A) => MoveResult[B]): MoveResult[B] = fa.flatMap(f) - override def point[A](a: => A): MoveResult[A] = OkMove(a) + override def pure[A](a: A): MoveResult[A] = OkMove(a) + + @tailrec + override def tailRecM[A, B](a: A)(f: (A) => MoveResult[Either[A, B]]): MoveResult[B] = + f(a) match { + case OkMove(nextState) => nextState match { + case scala.Left(newA) => tailRecM(newA)(f) + case scala.Right(b) => OkMove(b) + } + case o: GameOver => o + case i: InvalidMove => i + } } } sealed trait MoveResult[+A] { diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala b/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala index d487bde..01f21ad 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/PtnParser.scala @@ -1,13 +1,15 @@ package com.github.daenyth.taklib +import cats.free.Free import com.github.daenyth.taklib.GameEndResult._ import com.github.daenyth.taklib.Implicits.RichParsing import scala.collection.immutable.VectorBuilder import scala.util.parsing.combinator.RegexParsers -import scalaz.std.vector._ -import scalaz.syntax.foldable._ -import scalaz.{-\/, \/, \/-} +import cats.syntax.either._ +import cats.instances.vector._ + +import scala.util.Try object PtnParser extends RegexParsers with RichParsing { @@ -77,7 +79,7 @@ object PtnParser extends RegexParsers with RichParsing { var nextTurnNumber = startingTurn val iter = fullturns.iterator val history = new VectorBuilder[TurnAction] - \/.fromTryCatchNonFatal { + Either.fromTry {Try { while (iter.hasNext) { val (turnNumber, whiteAction, blackAction) = iter.next() require( @@ -106,7 +108,8 @@ object PtnParser extends RegexParsers with RichParsing { blackAction.foreach(history += _) } history.result() - }.leftMap(_.getMessage) + } + }.leftMap(_.getMessage) } val roadWin: Parser[RoadWin] = "R-0" ^^ { _ => @@ -132,19 +135,19 @@ object PtnParser extends RegexParsers with RichParsing { def ptn(ruleSet: RuleSet): Parser[(PtnHeaders, MoveResult[Game])] = headers >>=? { gameHeaders => gameHeaders.get("TPS") match { case None => - \/-(gameHistoryFromTurn(ruleSet, gameHeaders, 1, skipFirst = false, Game.ofSize)) + scala.Right(gameHistoryFromTurn(ruleSet, gameHeaders, 1, skipFirst = false, Game.ofSize)) case Some(tps) => TpsParser.parseEither(TpsParser.tps, tps).flatMap { case (board, turnNumber, nextPlayer) => - def getGame(size: Int, ruleSet: RuleSet): String \/ Game = { + def getGame(size: Int, ruleSet: RuleSet): Either[String, Game] = { val gameTurnNumber = (2 * turnNumber) + nextPlayer.fold(1, 0) val game = Game.fromBoard(board, gameTurnNumber) if (game.size != size) - -\/(s"Game headers declared size $size but TPS contained size ${game.size}") - else \/-(game) + scala.Left(s"Game headers declared size $size but TPS contained size ${game.size}") + else scala.Right(game) } - \/-( + scala.Right( gameHistoryFromTurn( ruleSet, gameHeaders, @@ -162,16 +165,16 @@ object PtnParser extends RegexParsers with RichParsing { gameHeaders: PtnHeaders, startingTurn: Int, skipFirst: Boolean, - getInitialGame: (Int, RuleSet) => String \/ Game + getInitialGame: (Int, RuleSet) => Either[String, Game] ): Parser[(PtnHeaders, MoveResult[Game])] = gameHistory(startingTurn, skipFirst) ^^? { history => for { - size <- \/.fromTryCatchNonFatal(gameHeaders("Size").toInt) + size <- Either.fromTry(Try(gameHeaders("Size").toInt)) .leftMap(ex => s"Unable to parse game size from header ${ex.getMessage}") initialGame <- getInitialGame(size, ruleSet) } yield { - val finalGame = history.zipWithIndex - .foldLeftM[MoveResult, Game](initialGame) { + val finalGame = + Free.foldLeftM(history.zipWithIndex, initialGame) { case (game, (action, actionIdx)) => game.takeTurn(action).noteInvalid(r => s"(Move #${actionIdx + 1}) $r") } diff --git a/taklib/src/main/scala/com/github/daenyth/taklib/TpsParser.scala b/taklib/src/main/scala/com/github/daenyth/taklib/TpsParser.scala index 8c9d486..d282bcb 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/TpsParser.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/TpsParser.scala @@ -6,7 +6,6 @@ import com.github.daenyth.taklib.Stone._ import scala.annotation.tailrec import scala.util.Try import scala.util.parsing.combinator.RegexParsers -import scalaz.{-\/, \/-} object TpsParser extends RegexParsers with RichParsing { override val skipWhitespace = false @@ -50,14 +49,14 @@ object TpsParser extends RegexParsers with RichParsing { val ranksize = pieces.size if (! (for (file <- pieces) yield file.size).forall(_ == ranksize)) { - -\/("Board size is not square") + scala.Left("Board size is not square") } else { val player = np match { case "1" => White case "2" => Black } - \/-((Board(ranksize, pieces), t.toInt, player)) + scala.Right((Board(ranksize, pieces), t.toInt, player)) } } } diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala index db6bfdf..0efac3d 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala @@ -118,7 +118,7 @@ class GameTest "A long game" should "be playable without a problem" in { // https://www.playtak.com/games/153358/view - val maybeMoves: Vector[String \/ TurnAction] = Vector( + val maybeMoves: Vector[Either[String, TurnAction]] = Vector( "a6", "a1", "c3", From 7258640d8e86a8e057aca27b9482ae764617a044 Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Sat, 20 May 2017 20:22:04 -0400 Subject: [PATCH 2/5] Convert taklib tests to cats --- .../com/github/daenyth/taklib/GameTest.scala | 51 +++++++++++-------- .../github/daenyth/taklib/PtnParserTest.scala | 7 ++- .../github/daenyth/taklib/TpsParserTest.scala | 4 +- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala index 0efac3d..f757bd5 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala @@ -1,16 +1,18 @@ package com.github.daenyth.taklib +import cats.free.Free import com.github.daenyth.taklib.GameEndResult._ import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.{FlatSpec, Matchers, OptionValues} -import org.typelevel.scalatest.DisjunctionValues - -import scalaz.\/ -import scalaz.scalacheck.ScalazProperties -import scalaz.std.vector._ -import scalaz.syntax.traverse._ - -object GameTest { +import org.scalatest._ +import cats.scalatest.EitherValues +import cats.instances.vector._ +import cats.instances.either._ +import cats.kernel.laws.GroupLaws +import cats.syntax.traverse._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks +import org.typelevel.discipline.scalatest.Discipline + +object GameEndResultLawsTest { implicit val arbRoad: Arbitrary[RoadWin] = Arbitrary { Gen.oneOf(White, Black).map(RoadWin) } implicit val arbFlat: Arbitrary[FlatWin] = Arbitrary { Gen.oneOf(White, Black).map(FlatWin) } implicit val arbGer: Arbitrary[GameEndResult] = Arbitrary { @@ -23,18 +25,21 @@ object GameTest { } } +class GameEndResultLawsTest + extends FunSuite + with Discipline + with Matchers + with GeneratorDrivenPropertyChecks { + import GameEndResultLawsTest._ + checkAll("GameEndResult", GroupLaws[GameEndResult].semigroup) +} + class GameTest extends FlatSpec with Matchers - with PropertyCheckers with OptionValues - with DisjunctionValues + with EitherValues with MoveResultValues { - import GameTest._ - - "GameEndResult" should "be a lawful semigroup" in { - check(ScalazProperties.semigroup.laws[GameEndResult]) - } "A full board" should "have a game end result" in { 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 2 13").value @@ -61,8 +66,8 @@ class GameTest "Four flats and a capstone" should "have a road win" in { val board = Board.ofSize(5) val moves = (1 to 4).map(n => Black -> PlayFlat(BoardIndex(1, n))) ++ Vector( - Black -> PlayCapstone(BoardIndex(1, 5)) - ) + Black -> PlayCapstone(BoardIndex(1, 5)) + ) val roadBoard = board.applyActions(moves) val game = Game.fromBoard(roadBoard.value) game.winner.value shouldBe RoadWin(Black) @@ -71,8 +76,8 @@ class GameTest "Four flats and a standing stone" should "not be a win" in { val board = Board.ofSize(5) val moves = (1 to 4).map(n => Black -> PlayFlat(BoardIndex(1, n))) ++ Vector( - Black -> PlayStanding(BoardIndex(1, 5)) - ) + Black -> PlayStanding(BoardIndex(1, 5)) + ) val roadBoard = board.applyActions(moves) val game = Game.fromBoard(roadBoard.value) game.winner shouldBe None @@ -216,12 +221,14 @@ class GameTest "e3" ).map { PtnParser.parseEither(PtnParser.turnAction, _) } val actions: Vector[TurnAction] = maybeMoves.sequenceU.value - val game = actions.foldLeftM[MoveResult, Game](Game.ofSize(6).value) { (game, action) => game.takeTurn(action) } + val game = Free.foldLeftM(actions, Game.ofSize(6).value) { (game, action) => + game.takeTurn(action) + } game should matchPattern { case GameOver(FlatWin(White), _) => () } } } -class GamePtnTest extends FlatSpec with Matchers with DisjunctionValues { +class GamePtnTest extends FlatSpec with Matchers with EitherValues { def roundTripPtn(g: Game): MoveResult[Game] = Game.fromPtn(g.toPtn).value diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala index 562d0b4..7d87d92 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala @@ -3,21 +3,20 @@ package com.github.daenyth.taklib import com.github.daenyth.taklib.GameEndResult._ import com.github.daenyth.taklib.PtnParser.PtnHeaders import org.scalatest.{FlatSpec, Matchers} -import org.typelevel.scalatest.DisjunctionValues +import cats.scalatest.EitherValues import scala.io.Source -import scalaz.\/ object PtnParserTest { def readPtn(ptnFileName: String): String = Source.fromResource(s"ptn/$ptnFileName.ptn").getLines.mkString("\n") - def parsePtn(ptn: String): (String \/ (PtnHeaders, MoveResult[Game])) = + def parsePtn(ptn: String): Either[String, (PtnHeaders, MoveResult[Game])] = PtnParser.parseEither(PtnParser.ptn(DefaultRules), ptn) } -class PtnParserTest extends FlatSpec with Matchers with DisjunctionValues with MoveResultValues { +class PtnParserTest extends FlatSpec with Matchers with EitherValues with MoveResultValues { import PtnParserTest._ "BoardIndex names" should "round trip ptn parsing" in { diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala index 87019f5..45f3563 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala @@ -1,9 +1,9 @@ package com.github.daenyth.taklib +import cats.scalatest.EitherValues import org.scalatest.{FlatSpec, Matchers} -import org.typelevel.scalatest.DisjunctionValues -class TpsParserTest extends FlatSpec with Matchers with DisjunctionValues { +class TpsParserTest extends FlatSpec with Matchers with EitherValues { "A size 6 board empty board" should "be parsed" in { Board.fromTps("x6/x6/x6/x6/x6/x6 1 1").value shouldEqual Board.ofSize(6) } From 18f33e525d105979671c5fee1e09f1d946fabe25 Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Sat, 20 May 2017 21:13:20 -0400 Subject: [PATCH 3/5] Convert Takcli to cats --- .../com/github/daenyth/takcli/Main.scala | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala index a2779f1..50412de 100644 --- a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala +++ b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala @@ -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 scala.Left(err) => Task.now(println(err)) *> getInitialGame - case scala.Right(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)" @@ -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 } } From 8fb447ab10546c4c2a1344a706eca53535dc4f8f Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Sat, 20 May 2017 21:35:01 -0400 Subject: [PATCH 4/5] Convert tpsserver to cats --- .../com/github/daenyth/tpsserver/TpsServer.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala index b3019e0..4e0334b 100644 --- a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala +++ b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala @@ -1,5 +1,8 @@ package com.github.daenyth.tpsserver +import cats.data.ValidatedNel +import cats.syntax.either._ +import cats.syntax.cartesian._ import com.github.daenyth.taklib.{Game, MoveResult, PtnParser} import fs2.Task import io.circe.generic.auto._ @@ -10,19 +13,16 @@ import org.http4s.circe._ import org.http4s.dsl._ import org.http4s.{HttpService, Response} -import scalaz.ValidationNel -import scalaz.syntax.applicative._ - case class TpsMove(tps: String, move: String) object TpsServer { - def takeTurn(move: TpsMove): ValidationNel[String, MoveResult[Game]] = { - val gameE = Game.fromTps(move.tps).leftMap(err => s"TPS: $err").validationNel + def takeTurn(move: TpsMove): ValidatedNel[String, MoveResult[Game]] = { + val gameE = Game.fromTps(move.tps).leftMap(err => s"TPS: $err").toValidatedNel val moveE = PtnParser .parseEither(PtnParser.turnAction, move.move) .leftMap(err => s"Move: $err") - .validationNel - (gameE |@| moveE) apply { + .toValidatedNel + (gameE |@| moveE) map { case (game, action) => game.takeTurn(action) } } @@ -30,7 +30,7 @@ object TpsServer { private def runTpsMove(move: TpsMove): Task[Response] = takeTurn(move) .fold( - err => BadRequest(Json.obj("errors" -> err.list.toVector.asJson)), + err => BadRequest(Json.obj("errors" -> err.toList.toVector.asJson)), ok => Ok(ok.asJson) ) From e854ef877fd1f63c647793a3ab5297090daad022 Mon Sep 17 00:00:00 2001 From: Gavin Bisesi Date: Sat, 20 May 2017 21:35:54 -0400 Subject: [PATCH 5/5] Convert opentak to cats --- .../com/github/daenyth/opentak/protocol/PlaytakCodec.scala | 4 ++-- .../github/daenyth/opentak/protocol/PlaytakCodecTest.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opentak/src/main/scala/com/github/daenyth/opentak/protocol/PlaytakCodec.scala b/opentak/src/main/scala/com/github/daenyth/opentak/protocol/PlaytakCodec.scala index c831323..5587ab4 100644 --- a/opentak/src/main/scala/com/github/daenyth/opentak/protocol/PlaytakCodec.scala +++ b/opentak/src/main/scala/com/github/daenyth/opentak/protocol/PlaytakCodec.scala @@ -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._ diff --git a/opentak/src/test/scala/com/github/daenyth/opentak/protocol/PlaytakCodecTest.scala b/opentak/src/test/scala/com/github/daenyth/opentak/protocol/PlaytakCodecTest.scala index ec6bb50..19888dc 100644 --- a/opentak/src/test/scala/com/github/daenyth/opentak/protocol/PlaytakCodecTest.scala +++ b/opentak/src/test/scala/com/github/daenyth/opentak/protocol/PlaytakCodecTest.scala @@ -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"))