diff --git a/.gitignore b/.gitignore index 92322c4..6110fcb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ .idea/ target/ +.bloop/ +.metals/ +metals.sbt +.vscode/settings.json \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index ac82dcf..9c2ef8c 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,7 @@ +version = "3.0.5" align.openParenCallSite = false -danglingParentheses = true +danglingParentheses.preset = true maxColumn = 100 -project.git = true +# project.git = true rewrite.rules = [RedundantBraces, RedundantParens, SortImports, PreferCurlyFors] -docstrings = JavaDoc +# docstrings = JavaDoc diff --git a/build.sbt b/build.sbt index 47c8e42..6367c30 100644 --- a/build.sbt +++ b/build.sbt @@ -2,23 +2,7 @@ name := "taklib" lazy val commonSettings = Seq( version := "0.2.0", - scalaVersion := "2.12.1", - scalacOptions ++= Seq( - "-deprecation", - "-encoding", "UTF-8", // yes, this is 2 args - "-feature", - "-unchecked", - "-Xfatal-warnings", - "-Xlint", - "-Yno-adapted-args", - // "-Ywarn-dead-code", // N.B. doesn't work well with the ??? hole - "-Ywarn-numeric-widen", - "-Ywarn-value-discard", - "-Ywarn-unused", - "-Ywarn-unused-import", - "-Xfuture", - "-Ypartial-unification" - ) + scalaVersion := "2.13.8" ) lazy val taklib = (project in file("taklib")) @@ -34,54 +18,63 @@ lazy val opentak = (project in file("opentak")) .settings(commonSettings, name := "opentak") // Remove these options in 'sbt console' because they're not nice for interactive usage -scalacOptions in (taklib, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains)) -scalacOptions in (takcli, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains)) -scalacOptions in (tpsserver, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains)) -scalacOptions in (opentak, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains)) +scalacOptions in (taklib, Compile, console) ~= (_.filterNot( + Set("-Xfatal-warnings", "-Ywarn-unused-import").contains +)) +scalacOptions in (takcli, Compile, console) ~= (_.filterNot( + Set("-Xfatal-warnings", "-Ywarn-unused-import").contains +)) +scalacOptions in (tpsserver, Compile, console) ~= (_.filterNot( + Set("-Xfatal-warnings", "-Ywarn-unused-import").contains +)) +scalacOptions in (opentak, Compile, console) ~= (_.filterNot( + Set("-Xfatal-warnings", "-Ywarn-unused-import").contains +)) resolvers += Resolver.sonatypeRepo("releases") -val catsVersion = "0.9.0" -val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5" +val catsVersion = "2.7.0" +val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "2.1.0" val dependencies = Seq( - "org.typelevel" %% "cats" % catsVersion, + "org.typelevel" %% "cats-core" % catsVersion, parserCombinators, - "org.scala-graph" %% "graph-core" % "1.11.4" + "org.scala-graph" %% "graph-core" % "1.13.4" ) val testDependencies = Seq( - "org.scalatest" %% "scalatest" % "3.0.1" % "test", - "org.scalacheck" %% "scalacheck" % "1.13.4" % "test", - "com.ironcorelabs" %% "cats-scalatest" % "2.2.0" % "test" + "org.scalatest" %% "scalatest" % "3.2.11" % "test", + "org.scalacheck" %% "scalacheck" % "1.15.4" % "test", + "com.ironcorelabs" %% "cats-scalatest" % "3.1.1" % "test", + "org.typelevel" %% "discipline-core" % "1.4.0" % "test", + "org.typelevel" %% "discipline-scalatest" % "2.0.0", + "org.typelevel" %% "cats-kernel-laws" % catsVersion % "test", + "org.scalatestplus" %% "scalacheck-1-15" % "3.2.11.0" % "test" ) -libraryDependencies in taklib ++= dependencies -libraryDependencies in taklib ++= testDependencies -libraryDependencies in takcli ++= Seq( - "org.typelevel" %% "cats-effect" % "0.2" +taklib / libraryDependencies ++= dependencies +taklib / libraryDependencies ++= testDependencies +takcli / libraryDependencies ++= Seq( + "org.typelevel" %% "cats-effect" % "3.3.5" ) resolvers in tpsserver += Resolver.sonatypeRepo("snapshots") -val http4sVersion = "0.17.0-M1" -val circeVersion = "0.7.0" -libraryDependencies in tpsserver ++= Seq( - "io.circe" %% "circe-core" % circeVersion, +val http4sVersion = "0.23.10" +val circeVersion = "0.14.1" +tpsserver / libraryDependencies ++= Seq( + "io.circe" %% "circe-core" % circeVersion, "io.circe" %% "circe-generic" % circeVersion, - "io.circe" %% "circe-parser" % circeVersion, + "io.circe" %% "circe-parser" % circeVersion, "io.circe" %% "circe-optics" % circeVersion % "test", - "org.http4s" %% "http4s-blaze-server" % http4sVersion, - "org.http4s" %% "http4s-circe" % http4sVersion, - "org.http4s" %% "http4s-dsl" % http4sVersion, + "org.http4s" %% "http4s-circe" % http4sVersion, + "org.http4s" %% "http4s-dsl" % http4sVersion, "org.http4s" %% "http4s-blaze-client" % http4sVersion % "test", - "org.http4s" %% "http4s-client" % http4sVersion % "test", - + "org.http4s" %% "http4s-client" % http4sVersion % "test", "ch.qos.logback" % "logback-classic" % "1.2.1" ) ++ testDependencies -libraryDependencies in opentak += parserCombinators -libraryDependencies in opentak ++= testDependencies - +opentak / libraryDependencies += parserCombinators +opentak / libraryDependencies ++= testDependencies initialCommands in (taklib, console) += "import com.github.daenyth.taklib._" -coverageEnabled in taklib := true +taklib / coverageEnabled := true 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 5587ab4..8c217fc 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 @@ -5,13 +5,13 @@ import com.github.daenyth.taklib.Implicits.RichParsing import com.github.daenyth.taklib._ import scala.util.parsing.combinator.RegexParsers +import scala.annotation.nowarn -/** - * Playtak protocol encoding/decoding between the wire - * representation (string) and in-library representation (case classes) - * - * See PlaytakCodec#encode and PlaytakCodec#decode - */ +/** Playtak protocol encoding/decoding between the wire representation (string) and in-library + * representation (case classes) + * + * See PlaytakCodec#encode and PlaytakCodec#decode + */ object PlaytakCodec { def encode(outgoing: Playtak.Outgoing): String = Outgoing.encode(outgoing) @@ -26,9 +26,8 @@ object PlaytakCodec { import Playtak.Incoming._ - val client: Parser[Client] = "Client" ~ "([A-Za-z-.0-9]{4,15})".r ^^ { - case _ ~ s => - Client(s) + val client: Parser[Client] = "Client" ~ "([A-Za-z-.0-9]{4,15})".r ^^ { case _ ~ s => + Client(s) } val msg: Parser[String] = """[^\n\r]{1,256}""".r @@ -48,9 +47,8 @@ object PlaytakCodec { def simpleGameMessage[A](str: String, gameMsg: GameNumber => A): Parser[A] = gameNumber <~ str ^^ gameMsg - val register: Parser[Register] = "Register" ~> username ~ email ^^ { - case username ~ email => - Register(username, email) + val register: Parser[Register] = "Register" ~> username ~ email ^^ { case username ~ email => + Register(username, email) } val userLogin: Parser[UserLogin] = "Login" ~> username ~ password ^^ { case username ~ password => @@ -63,7 +61,7 @@ object PlaytakCodec { val asPlayer = color match { case "W" => Some(White) case "B" => Some(Black) - case _ => None + case _ => None } Seek(size, time, increment, asPlayer) } @@ -75,7 +73,7 @@ object PlaytakCodec { val playStone = stoneType match { case "C" => PlayCapstone(idx) case "W" => PlayStanding(idx) - case _ => PlayFlat(idx) + case _ => PlayFlat(idx) } Place(gameNumber, playStone) } @@ -101,8 +99,8 @@ object PlaytakCodec { val shout: Parser[Shout] = "Shout" ~> msg ^^ Shout val joinRoom: Parser[JoinRoom] = "JoinRoom" ~> roomName ^^ JoinRoom val leaveRoom: Parser[LeaveRoom] = "LeaveRoom" ~> roomName ^^ LeaveRoom - val shoutRoom: Parser[ShoutRoom] = "ShoutRoom" ~> roomName ~ msg ^^ { - case room ~ msg => ShoutRoom(room, msg) + val shoutRoom: Parser[ShoutRoom] = "ShoutRoom" ~> roomName ~ msg ^^ { case room ~ msg => + ShoutRoom(room, msg) } val tell: Parser[Tell] = "Tell" ~> username ~ msg ^^ { case user ~ msg => Tell(user, msg) } val ping: Parser[Ping.type] = "^PING$".r ^^^ Ping @@ -119,21 +117,21 @@ object PlaytakCodec { def encode(outgoing: Playtak.Outgoing): String = { import Playtak.Outgoing._ outgoing match { - case Welcome => "Welcome!" - case LoginOrRegisterNow => "Login or Register" - case WelcomeUser(username) => s"Welcome $username" - case ge: GameEvent => encodeGameEvent(ge) - case Shout(username, msg) => s"Shout <$username> $msg" - case RoomJoined(name) => s"Joined room $name" - case RoomLeft(name) => s"Left room $name" + case Welcome => "Welcome!" + case LoginOrRegisterNow => "Login or Register" + case WelcomeUser(username) => s"Welcome $username" + case ge: GameEvent => encodeGameEvent(ge) + case Shout(username, msg) => s"Shout <$username> $msg" + case RoomJoined(name) => s"Joined room $name" + case RoomLeft(name) => s"Left room $name" case ShoutRoom(name, username, msg) => s"ShoutRoom $name <$username> $msg" - case Tell(username, msg) => s"Tell <$username> $msg" - case Told(username, msg) => s"Told <$username> $msg" - case ServerMessage(msg) => s"Message $msg" - case Error(msg) => s"Error $msg" - case OnlineUsers(count) => s"Online $count" - case NOK => "NOK" - case OK => "OK" + case Tell(username, msg) => s"Tell <$username> $msg" + case Told(username, msg) => s"Told <$username> $msg" + case ServerMessage(msg) => s"Message $msg" + case Error(msg) => s"Error $msg" + case OnlineUsers(count) => s"Online $count" + case NOK => "NOK" + case OK => "OK" } } @@ -155,10 +153,12 @@ object PlaytakCodec { s"Game Start $gameNumber $size $whitePlayerusername vs $blackPlayerusername $yourColor" case Place(gameNumber, playStone) => import Stone._ + // TODO this is just totally broken + @nowarn val stoneType = playStone.stone match { - case _: Capstone => "C" + case _: Capstone => "C" case _: StandingStone => "W" - case _: FlatStone => "" + case _: FlatStone => "" } s"Game#$gameNumber P ${playStone.at.name} $stoneType" case m: Move => @@ -175,7 +175,7 @@ object PlaytakCodec { case DoubleRoad => "R-R" // Not actually supported by playtak or default rules, but different result sets can treat it differently. case FlatWin(player) => player.fold("0-F", "F-0") - case Draw => "1/2-1/2" + case Draw => "1/2-1/2" case WinByResignation(player) => player.fold("0-1", "1-0") // Again not supported by playtak; this is PTN format } 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 19888dc..9e27f4d 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 @@ -2,9 +2,11 @@ package com.github.daenyth.opentak.protocol import com.github.daenyth.opentak.protocol.Playtak.{GameNumber, Username} import com.github.daenyth.taklib.White -import org.scalatest.{EitherValues, FlatSpec, Matchers} +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class PlaytakCodecTest extends FlatSpec with Matchers with EitherValues { +class PlaytakCodecTest extends AnyFlatSpec with Matchers with EitherValues { import PlaytakCodec.Incoming._ import Playtak.Incoming._ diff --git a/project/build.properties b/project/build.properties index e0cbc71..50fd0c7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.13 \ No newline at end of file +sbt.version = 1.6.1 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 6eeb4c2..d0cab0d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ -logLevel := Level.Warn +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.20") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.1.0") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") 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 50412de..1a80956 100644 --- a/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala +++ b/takcli/src/main/scala/com/github/daenyth/takcli/Main.scala @@ -5,23 +5,24 @@ import com.github.daenyth.taklib._ import scala.io.StdIn import scala.util.control.NoStackTrace -import cats.syntax.flatMap._ -import cats.syntax.applicativeError._ +import cats.syntax.all._ +import cats.effect.IOApp +import cats.effect.ExitCode -object Main { +object Main extends IOApp { - def main(args: Array[String]): Unit = + def run(args: List[String]): IO[ExitCode] = mainT - .recoverWith { - case CleanExit => IO(println("Exiting")) + .recoverWith { case CleanExit => + IO.println("Exiting") } - .unsafeRunSync() + .as(ExitCode.Success) def mainT: IO[Unit] = printStartup >> getInitialGame >>= runGameLoop def getInitialGame: IO[Game] = promptSize.flatMap { Game.ofSize(_) match { - case scala.Left(err) => IO(println(err)) >> getInitialGame + case scala.Left(err) => IO(println(err)) >> getInitialGame case scala.Right(game) => IO(game) } } @@ -45,10 +46,10 @@ object Main { def printGame(g: Game) = IO { val nextPlayInfo = g.turnNumber match { - case 1 => "White to play (Black stone)" - case 2 => "Black to play (White stone)" + case 1 => "White to play (Black stone)" + case 2 => "Black to play (White stone)" case n if n % 2 == 0 => "Black to play" - case _ => "White to play" + case _ => "White to play" } println(s"Move ${g.turnNumber} - $nextPlayInfo") print(pretty(g)) @@ -61,8 +62,8 @@ object Main { IO { print("Game size?\n > ") StdIn.readInt() - }.recoverWith { - case n: NumberFormatException => IO(println(s"Bad size: $n")) >> promptSize + }.recoverWith { case n: NumberFormatException => + IO(println(s"Bad size: $n")) >> promptSize } def pretty(g: Game): String = @@ -75,7 +76,8 @@ object Main { def promptAction: IO[TurnAction] = IO(StdIn.readLine("Your Move?\n > ")) .flatMap { input => - if (input == null) { throw CleanExit } else + if (input == null) { throw CleanExit } + else PtnParser .parseEither(PtnParser.turnAction, input) .fold( @@ -83,8 +85,8 @@ object Main { ta => IO.pure(ta) ) } - .recoverWith { - case PtnParseError(err) => IO(println(s"Bad move: $err")) >> promptAction + .recoverWith { case PtnParseError(err) => + IO(println(s"Bad move: $err")) >> promptAction } } 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 ca47e3c..6a43a08 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Board.scala @@ -6,9 +6,7 @@ import com.github.daenyth.taklib.Stone._ import scala.annotation.tailrec import scala.collection.immutable.IndexedSeq -import cats.syntax.either._ -import cats.syntax.monoid._ -import cats.instances.vector._ +import cats.syntax.all._ import cats.{Eq => Equal} import scala.util.Try 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 64c73f7..0a2c94e 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Game.scala @@ -7,12 +7,7 @@ import com.github.daenyth.taklib.RuleSet.GameRule import scala.annotation.tailrec import scalax.collection.GraphEdge.UnDiEdge import scalax.collection.immutable.Graph -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.syntax.all._ import cats.{Semigroup, Eq => Equal} import cats.data.NonEmptyList import cats.kernel.Comparison.{EqualTo, GreaterThan, LessThan} 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 16d0c8f..6fc11ee 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Implicits.scala @@ -1,7 +1,7 @@ package com.github.daenyth.taklib import scala.util.parsing.combinator.RegexParsers -import cats.syntax.either._ +import cats.syntax.all._ object Implicits { implicit class RichBoolean(b: Boolean) { 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 21460d7..aaa3314 100644 --- a/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala +++ b/taklib/src/main/scala/com/github/daenyth/taklib/Move.scala @@ -5,31 +5,30 @@ import cats.Monad import scala.annotation.tailrec - sealed trait GameAction case class StartGameWithBoard(board: Board) extends GameAction sealed trait TurnAction extends GameAction { def ptn: String = this match { - case PlayFlat(at) => at.name - case PlayStanding(at) => s"S${at.name}" - case PlayCapstone(at) => s"C${at.name}" + case PlayFlat(at) => at.name + case PlayStanding(at) => s"S${at.name}" + case PlayCapstone(at) => s"C${at.name}" case Move(from, direction, count, drops) => // Omit count+drops if moving whole stack or one piece val num = drops match { case Some(ds) if ds.length > 1 => count.map(_.toString).getOrElse("") - case _ => "" + case _ => "" } val dropSequence = drops match { case Some(ds) if ds.length > 1 => ds.mkString("") - case _ => "" + case _ => "" } s"$num${from.name}${direction.name}$dropSequence" } } object PlayStone { - def unapply(p: PlayStone): Option[(BoardIndex, Player => Stone)] = + def unapply(p: PlayStone): Some[(BoardIndex, Player => Stone)] = Some((p.at, p.stone)) } sealed trait PlayStone extends TurnAction { @@ -45,23 +44,23 @@ case class PlayStanding(at: BoardIndex) extends PlayStone { case class PlayCapstone(at: BoardIndex) extends PlayStone { val stone = Stone.Capstone.apply _ } -case class Move(from: BoardIndex, - direction: MoveDirection, - count: Option[Int], - drops: Option[Vector[Int]]) - extends TurnAction { +case class Move( + from: BoardIndex, + direction: MoveDirection, + count: Option[Int], + drops: Option[Vector[Int]] +) extends TurnAction { def finalPosition: BoardIndex = { val moveDistance = drops.map(_.length).getOrElse(1) direction match { - case Left => from.copy(file = from.file - moveDistance) + case Left => from.copy(file = from.file - moveDistance) case Right => from.copy(file = from.file + moveDistance) - case Up => from.copy(rank = from.rank + moveDistance) - case Down => from.copy(rank = from.rank - moveDistance) + case Up => from.copy(rank = from.rank + moveDistance) + case Down => from.copy(rank = from.rank - moveDistance) } } } - object MoveResult { implicit val moveResultInstance: Monad[MoveResult] = new Monad[MoveResult] { @@ -76,11 +75,12 @@ object MoveResult { @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 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 } } @@ -88,31 +88,29 @@ object MoveResult { sealed trait MoveResult[+A] { def map[B](f: A => B): MoveResult[B] = this match { case OkMove(nextState) => OkMove(f(nextState)) - case o: GameOver => o - case i: InvalidMove => i + case o: GameOver => o + case i: InvalidMove => i } def flatMap[B](f: A => MoveResult[B]): MoveResult[B] = this match { case OkMove(nextState) => f(nextState) - case o: GameOver => o - case i: InvalidMove => i + case o: GameOver => o + case i: InvalidMove => i } def noteInvalid(t: TurnAction): MoveResult[A] = noteInvalid(r => s"(${t.ptn}) $r") def noteInvalid(addNote: String => String): MoveResult[A] = this match { case InvalidMove(reason) => InvalidMove(addNote(reason)) - case other => other + case other => other } } case class OkMove[A](nextState: A) extends MoveResult[A] case class GameOver(result: GameEndResult, finalState: Game) extends MoveResult[Nothing] case class InvalidMove(reason: String) - extends Exception(reason) + extends Exception(reason) with MoveResult[Nothing] with NoStackTrace - - sealed trait MoveDirection { def name: String } case object Left extends MoveDirection { val name = "<" } case object Right extends MoveDirection { val name = ">" } 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 01f21ad..abdc73b 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,13 @@ 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 cats.syntax.either._ -import cats.instances.vector._ +import cats.syntax.all._ + +import annotation.nowarn import scala.util.Try @@ -23,8 +23,12 @@ object PtnParser extends RegexParsers with RichParsing { BoardIndex(rank, file) } val playFlat: Parser[PlayFlat] = boardIndex ^^ { idx => PlayFlat(idx) } - val playStanding: Parser[PlayStanding] = "S".r ~ boardIndex ^^ { case _ ~ idx => PlayStanding(idx) } - val playCapstone: Parser[PlayCapstone] = "C".r ~ boardIndex ^^ { case _ ~ idx => PlayCapstone(idx) } + val playStanding: Parser[PlayStanding] = "S".r ~ boardIndex ^^ { case _ ~ idx => + PlayStanding(idx) + } + val playCapstone: Parser[PlayCapstone] = "C".r ~ boardIndex ^^ { case _ ~ idx => + PlayCapstone(idx) + } val playStone: Parser[PlayStone] = playFlat | playStanding | playCapstone val moveDirection: Parser[MoveDirection] = "[-+<>]".r ^^ { @@ -39,17 +43,17 @@ object PtnParser extends RegexParsers with RichParsing { val drops = "[12345678]+".r ^^ { _.toVector.map(_.toString.toInt) } (count.? ~ boardIndex ~ moveDirection ~ drops.?) ^^ { case (count: Option[Int]) ~ - (idx: BoardIndex) ~ - (direction: MoveDirection) ~ - (drops: Option[Vector[Int]]) => - Move(idx, direction, count, drops) + (idx: BoardIndex) ~ + (direction: MoveDirection) ~ + (drops: Option[Vector[Int]]) => + Move(idx, direction, count, drops) } } val infoMark: Parser[String] = "'{1,2}".r | "[!?]{1,2}".r - val turnAction: Parser[TurnAction] = (moveStones | playStone) ~ infoMark.? ^^ { - case action ~ _ => action + val turnAction: Parser[TurnAction] = (moveStones | playStone) ~ infoMark.? ^^ { case action ~ _ => + action } val headerLine: Parser[(String, String)] = "[" ~ """\S+""".r ~ "\".*\"".r ~ "]" ^^ { @@ -63,39 +67,39 @@ object PtnParser extends RegexParsers with RichParsing { val fullTurnLine: Parser[(Int, TurnAction, TurnAction)] = """\d+\.""".r ~ turnAction ~ turnAction ~ comment.? ^^ { - case turnNumber ~ whiteAction ~ blackAction ~ _comment => + case turnNumber ~ whiteAction ~ blackAction ~ _ => (turnNumber.dropRight(1).toInt, whiteAction, blackAction) } val lastTurnLine: Parser[(Int, TurnAction, Option[TurnAction])] = """\d+\.""".r ~ turnAction ~ turnAction.? ~ comment.? ^^ { - case turnNumber ~ whiteAction ~ blackAction ~ _comment => + case turnNumber ~ whiteAction ~ blackAction ~ _ => (turnNumber.dropRight(1).toInt, whiteAction, blackAction) } def gameHistory(startingTurn: Int, skipFirst: Boolean): Parser[Vector[TurnAction]] = - rep(fullTurnLine) ~ lastTurnLine.? ^^? { - case fullturns ~ lastTurn => - var nextTurnNumber = startingTurn - val iter = fullturns.iterator - val history = new VectorBuilder[TurnAction] - Either.fromTry {Try { - while (iter.hasNext) { - val (turnNumber, whiteAction, blackAction) = iter.next() - require( - turnNumber == nextTurnNumber, - s"Turn numbers out of order; expected $nextTurnNumber, got $turnNumber" - ) - nextTurnNumber += 1 - if (skipFirst && startingTurn == turnNumber) { - // do nothing - } else { - history += whiteAction + rep(fullTurnLine) ~ lastTurnLine.? ^^? { case fullturns ~ lastTurn => + var nextTurnNumber = startingTurn + val iter = fullturns.iterator + val history = new VectorBuilder[TurnAction] + Either + .fromTry { + Try { + while (iter.hasNext) { + val (turnNumber, whiteAction, blackAction) = iter.next() + require( + turnNumber == nextTurnNumber, + s"Turn numbers out of order; expected $nextTurnNumber, got $turnNumber" + ) + nextTurnNumber += 1 + if (skipFirst && startingTurn == turnNumber) { + // do nothing + } else { + history += whiteAction + } + history += blackAction } - history += blackAction - } - lastTurn.foreach { - case (turnNumber, whiteAction, blackAction) => + lastTurn.foreach { case (turnNumber, whiteAction, blackAction) => require( turnNumber == nextTurnNumber, s"Turn numbers out of order; expected $nextTurnNumber, got $turnNumber" @@ -106,10 +110,11 @@ object PtnParser extends RegexParsers with RichParsing { history += whiteAction } blackAction.foreach(history += _) + } + history.result() } - history.result() } - }.leftMap(_.getMessage) + .leftMap(_.getMessage) } val roadWin: Parser[RoadWin] = "R-0" ^^ { _ => @@ -137,25 +142,27 @@ object PtnParser extends RegexParsers with RichParsing { case None => 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): Either[String, Game] = { - val gameTurnNumber = (2 * turnNumber) + nextPlayer.fold(1, 0) - val game = Game.fromBoard(board, gameTurnNumber) - if (game.size != size) - scala.Left(s"Game headers declared size $size but TPS contained size ${game.size}") - else scala.Right(game) - } + TpsParser.parseEither(TpsParser.tps, tps).flatMap { case (board, turnNumber, nextPlayer) => + def getGame( + size: Int, + @nowarn("msg=never used") ruleSet: RuleSet + ): Either[String, Game] = { + val gameTurnNumber = (2 * turnNumber) + nextPlayer.fold(1, 0) + val game = Game.fromBoard(board, gameTurnNumber) + if (game.size != size) + scala.Left(s"Game headers declared size $size but TPS contained size ${game.size}") + else scala.Right(game) + } - scala.Right( - gameHistoryFromTurn( - ruleSet, - gameHeaders, - turnNumber, - skipFirst = nextPlayer == Black, - getGame - ) + scala.Right( + gameHistoryFromTurn( + ruleSet, + gameHeaders, + turnNumber, + skipFirst = nextPlayer == Black, + getGame ) + ) } } } @@ -169,14 +176,14 @@ object PtnParser extends RegexParsers with RichParsing { ): Parser[(PtnHeaders, MoveResult[Game])] = gameHistory(startingTurn, skipFirst) ^^? { history => for { - size <- Either.fromTry(Try(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 = - Free.foldLeftM(history.zipWithIndex, initialGame) { - case (game, (action, actionIdx)) => - game.takeTurn(action).noteInvalid(r => s"(Move #${actionIdx + 1}) $r") + history.zipWithIndex.foldLeftM(initialGame) { case (game, (action, actionIdx)) => + game.takeTurn(action).noteInvalid(r => s"(Move #${actionIdx + 1}) $r") } (gameHeaders, finalGame) } diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/BoardTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/BoardTest.scala index 98212da..99bd04e 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/BoardTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/BoardTest.scala @@ -1,8 +1,9 @@ package com.github.daenyth.taklib -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class BoardTest extends FlatSpec with Matchers { +class BoardTest extends AnyFlatSpec with Matchers { "An edge BoardIndex" should "be opposite one side" in { val idx = BoardIndex(1, 2) val opposites = idx.oppositeIndexes(5).toSet 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 f757bd5..1c2b543 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/GameTest.scala @@ -1,20 +1,20 @@ package com.github.daenyth.taklib -import cats.free.Free import com.github.daenyth.taklib.GameEndResult._ import org.scalacheck.{Arbitrary, Gen} 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 +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import cats.syntax.all._ +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import cats.kernel.laws.discipline.SemigroupTests +import org.typelevel.discipline.scalatest.FunSuiteDiscipline 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 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 { Gen.oneOf( Gen.const(DoubleRoad), @@ -26,16 +26,16 @@ object GameEndResultLawsTest { } class GameEndResultLawsTest - extends FunSuite - with Discipline + extends AnyFunSuite + with FunSuiteDiscipline with Matchers - with GeneratorDrivenPropertyChecks { + with ScalaCheckDrivenPropertyChecks { import GameEndResultLawsTest._ - checkAll("GameEndResult", GroupLaws[GameEndResult].semigroup) + checkAll("GameEndResult", SemigroupTests[GameEndResult].semigroup) } class GameTest - extends FlatSpec + extends AnyFlatSpec with Matchers with OptionValues with EitherValues @@ -94,7 +94,8 @@ class GameTest val i = BoardIndex(1, 1) val board = Board.ofSize(5).applyAction(Black, PlayFlat(i)).value val game = Game.fromBoard(board) - DefaultRules.actingPlayerControlsStack(game, Move(i, Right, None, None)) shouldBe 'nonEmpty + DefaultRules.actingPlayerControlsStack(game, Move(i, Right, None, None)) shouldBe + Symbol("nonEmpty") } "The first move" should "be taken with a black flatstone" in { @@ -219,16 +220,16 @@ class GameTest "e4", "5c1>14", "e3" - ).map { PtnParser.parseEither(PtnParser.turnAction, _) } - val actions: Vector[TurnAction] = maybeMoves.sequenceU.value - val game = Free.foldLeftM(actions, Game.ofSize(6).value) { (game, action) => + ).map(PtnParser.parseEither(PtnParser.turnAction, _)) + val actions: Vector[TurnAction] = maybeMoves.sequence.value + val game = actions.foldLeftM(Game.ofSize(6).value) { (game, action) => game.takeTurn(action) } game should matchPattern { case GameOver(FlatWin(White), _) => () } } } -class GamePtnTest extends FlatSpec with Matchers with EitherValues { +class GamePtnTest extends AnyFlatSpec 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/MoveResultValues.scala b/taklib/src/test/scala/com/github/daenyth/taklib/MoveResultValues.scala index 072bcfc..4bde7d8 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/MoveResultValues.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/MoveResultValues.scala @@ -1,7 +1,5 @@ package com.github.daenyth.taklib -import scala.language.implicitConversions - import org.scalactic.source import org.scalatest.exceptions.{StackDepthException, TestFailedException} @@ -13,9 +11,17 @@ trait MoveResultValues { def value: A = m match { case OkMove(ok) => ok case o: GameOver => - throw new TestFailedException((_: StackDepthException) => Some(s"Got $o, expected OkMove."), None, pos) + throw new TestFailedException( + (_: StackDepthException) => Some(s"Got $o, expected OkMove."), + None, + pos + ) case i: InvalidMove => - throw new TestFailedException((_: StackDepthException) => Some(s"Got $i, expected OkMove."), None, pos) + throw new TestFailedException( + (_: StackDepthException) => Some(s"Got $i, expected OkMove."), + None, + pos + ) } } } diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala b/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala index dd92eb0..619baf1 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/MoveTest.scala @@ -1,9 +1,10 @@ package com.github.daenyth.taklib import com.github.daenyth.taklib.Stone._ -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class MoveTest extends FlatSpec with Matchers with MoveResultValues { +class MoveTest extends AnyFlatSpec with Matchers with MoveResultValues { "Moving to capture" should "make a stack" in { val board = Board.ofSize(5) diff --git a/taklib/src/test/scala/com/github/daenyth/taklib/PropertyCheckers.scala b/taklib/src/test/scala/com/github/daenyth/taklib/PropertyCheckers.scala index e442202..fbc52de 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/PropertyCheckers.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/PropertyCheckers.scala @@ -1,10 +1,9 @@ package com.github.daenyth.taklib import org.scalacheck.{Prop, Properties} -import org.scalatest.prop.Checkers - -import scala.language.implicitConversions +import org.scalatestplus.scalacheck.Checkers trait PropertyCheckers extends Checkers { - implicit def propertiesToProp(properties: Properties): Prop = Prop.all(properties.properties.map(_._2): _*) + implicit def propertiesToProp(properties: Properties): Prop = + Prop.all(properties.properties.map(_._2).toSeq: _*) } 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 7d87d92..c5cd57c 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/PtnParserTest.scala @@ -2,21 +2,22 @@ package com.github.daenyth.taklib import com.github.daenyth.taklib.GameEndResult._ import com.github.daenyth.taklib.PtnParser.PtnHeaders -import org.scalatest.{FlatSpec, Matchers} -import cats.scalatest.EitherValues +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import scala.io.Source object PtnParserTest { def readPtn(ptnFileName: String): String = - Source.fromResource(s"ptn/$ptnFileName.ptn").getLines.mkString("\n") + Source.fromResource(s"ptn/$ptnFileName.ptn").getLines().mkString("\n") def parsePtn(ptn: String): Either[String, (PtnHeaders, MoveResult[Game])] = PtnParser.parseEither(PtnParser.ptn(DefaultRules), ptn) } -class PtnParserTest extends FlatSpec with Matchers with EitherValues with MoveResultValues { +class PtnParserTest extends AnyFlatSpec with Matchers with EitherValues with MoveResultValues { import PtnParserTest._ "BoardIndex names" should "round trip ptn parsing" in { @@ -49,16 +50,16 @@ class PtnParserTest extends FlatSpec with Matchers with EitherValues with MoveRe "An 8x8 game" should "work" in { val ptn = readPtn("8x8") val (_, result: MoveResult[Game]) = parsePtn(ptn).value - result should matchPattern { - case GameOver(RoadWin(White), _) => () + result should matchPattern { case GameOver(RoadWin(White), _) => + () } } "A game with annotation comments" should "parse correctly" in { val ptn = readPtn("annot1") val (_, result) = parsePtn(ptn).value - result should matchPattern { - case GameOver(RoadWin(White), _) => () + result should matchPattern { case GameOver(RoadWin(White), _) => + () } } } 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 45f3563..7997355 100644 --- a/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala +++ b/taklib/src/test/scala/com/github/daenyth/taklib/TpsParserTest.scala @@ -1,14 +1,15 @@ package com.github.daenyth.taklib -import cats.scalatest.EitherValues -import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers -class TpsParserTest extends FlatSpec with Matchers with EitherValues { +class TpsParserTest extends AnyFlatSpec 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) } "A board with a stack" should "be parsed" in { - Board.fromTps("x3/x3/121,x2 2 1") shouldBe 'right + Board.fromTps("x3/x3/121,x2 2 1") shouldBe Symbol("right") } } diff --git a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/HttpMain.scala b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/HttpMain.scala index 45f6dca..6a2be4d 100644 --- a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/HttpMain.scala +++ b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/HttpMain.scala @@ -1,13 +1,14 @@ package com.github.daenyth.tpsserver -import fs2.{Stream, Task} -import org.http4s.server.blaze.BlazeBuilder -import org.http4s.util.StreamApp +import org.http4s.blaze.server.BlazeServerBuilder +import cats.effect.IOApp +import cats.effect.IO -object HttpMain extends StreamApp { - override def main(args: List[String]): Stream[Task, Nothing] = - BlazeBuilder +object HttpMain extends IOApp.Simple { + override def run: IO[Unit] = + BlazeServerBuilder[IO] .bindHttp(8080, "localhost") - .mountService(TpsServer.tpsService, "/") - .serve + .withHttpApp(TpsServer.tpsService.orNotFound) + .resource + .useForever } 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 4e0334b..9f31d37 100644 --- a/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala +++ b/tpsserver/src/main/scala/com/github/daenyth/tpsserver/TpsServer.scala @@ -1,17 +1,19 @@ package com.github.daenyth.tpsserver import cats.data.ValidatedNel -import cats.syntax.either._ -import cats.syntax.cartesian._ +import cats.syntax.all._ import com.github.daenyth.taklib.{Game, MoveResult, PtnParser} -import fs2.Task -import io.circe.generic.auto._ -import io.circe.generic.semiauto.deriveEncoder +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.syntax._ import io.circe.{Encoder, Json} import org.http4s.circe._ -import org.http4s.dsl._ -import org.http4s.{HttpService, Response} +import org.http4s.dsl.io._ +import org.http4s._ +import cats.effect.IO +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.circe.CirceEntityDecoder._ +import io.circe.Decoder +import scala.annotation.nowarn case class TpsMove(tps: String, move: String) @@ -22,23 +24,25 @@ object TpsServer { .parseEither(PtnParser.turnAction, move.move) .leftMap(err => s"Move: $err") .toValidatedNel - (gameE |@| moveE) map { - case (game, action) => game.takeTurn(action) - } + (gameE, moveE).mapN((game, action) => game.takeTurn(action)) } - private def runTpsMove(move: TpsMove): Task[Response] = + private def runTpsMove(move: TpsMove): IO[Response[IO]] = takeTurn(move) .fold( err => BadRequest(Json.obj("errors" -> err.toList.toVector.asJson)), ok => Ok(ok.asJson) ) - val tpsService = HttpService { - case req @ POST -> Root / "tpsMove" => - req.as(jsonOf[TpsMove]).flatMap(runTpsMove) + val tpsService = HttpRoutes.of[IO] { case req @ POST -> Root / "tpsMove" => + req.as[TpsMove].flatMap(runTpsMove) } - implicit val gameEncoder: Encoder[Game] = Encoder[String].contramap(game => game.toTps) - implicit def moveResultEncoder[A: Encoder]: Encoder[MoveResult[A]] = deriveEncoder + private implicit val tpsMoveDecoder: Decoder[TpsMove] = deriveDecoder + private implicit val gameEncoder: Encoder[Game] = Encoder[String].contramap(game => game.toTps) + @nowarn("msg=never used") + private implicit def moveResultEncoder[A: Encoder]: Encoder[MoveResult[A]] = { + import io.circe.generic.auto._ + deriveEncoder + } } diff --git a/tpsserver/src/test/scala/com/github/daenyth/tpsserver/TpsServerTest.scala b/tpsserver/src/test/scala/com/github/daenyth/tpsserver/TpsServerTest.scala index 52d545b..0bb42c9 100644 --- a/tpsserver/src/test/scala/com/github/daenyth/tpsserver/TpsServerTest.scala +++ b/tpsserver/src/test/scala/com/github/daenyth/tpsserver/TpsServerTest.scala @@ -5,22 +5,26 @@ import io.circe.Json import io.circe.generic.auto._ import io.circe.optics.JsonPath.root import io.circe.syntax._ -import org.http4s.Uri.uri +import org.http4s.syntax.all._ import org.http4s.circe.{jsonDecoder, jsonEncoder} import org.http4s.{Method, Request} -import org.scalatest.{AsyncFlatSpec, Matchers, OptionValues} +import org.scalatest.OptionValues +import org.scalatest.flatspec.AsyncFlatSpec +import org.scalatest.matchers.should.Matchers +import cats.effect.IO +import cats.effect.unsafe.implicits.global class TpsServerTest extends AsyncFlatSpec with Matchers with OptionValues { "TpsService" should "Run a valid request" in { val move = TpsMove("x6/x6/x6/x6/x6/x6 1 1", "a1").asJson + val request = Request[IO](Method.POST, uri"/tpsMove").withEntity(move) val task = for { - request <- Request(Method.POST, uri("/tpsMove")).withBody(move) - respose <- TpsServer.tpsService.run(request).map(_.orNotFound) + respose <- TpsServer.tpsService.orNotFound.run(request) js <- respose.as[Json] } yield { val newTps = root.OkMove.nextState.string.getOption(js).value - TpsParser.parseEither(TpsParser.tps, newTps) shouldBe 'right + TpsParser.parseEither(TpsParser.tps, newTps) shouldBe Symbol("right") } - task.unsafeRunAsyncFuture() + task.unsafeToFuture() } }