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: 10 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,23 @@ lazy val takcli = (project in file("takcli"))
lazy val tpsserver = (project in file("tpsserver"))
.dependsOn(taklib)
.settings(commonSettings, name := "tpsserver")
lazy val opentak = (project in file("opentak"))
.dependsOn(taklib)
.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))

resolvers += Resolver.sonatypeRepo("releases")

val scalazVersion = "7.2.8"
val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5"
val dependencies = Seq(
"org.scalaz" %% "scalaz-core" % scalazVersion,
"org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5",
parserCombinators,
"org.scala-graph" %% "graph-core" % "1.11.4"
)
val testDependencies = Seq(
Expand Down Expand Up @@ -74,6 +79,10 @@ libraryDependencies in tpsserver ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.2.1"
) ++ testDependencies

libraryDependencies in opentak += parserCombinators
libraryDependencies in opentak ++= testDependencies


initialCommands in (taklib, console) += "import com.github.daenyth.taklib._"

coverageEnabled in taklib := true
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.github.daenyth.opentak.protocol

import com.github.daenyth.taklib.{BoardIndex, GameEndResult, PlayStone, Player}

// See protocol description at https://github.com/chaitu236/TakServer
object Playtak {
case class GameNumber(value: Int) extends AnyVal
case class Username(value: String) extends AnyVal {
override def toString: String = value
}
case class RoomName(value: String) extends AnyVal {
override def toString: String = value
}

sealed trait Incoming

object Incoming {

case class Client(version: String) extends Incoming
case class Register(username: Username, email: String) extends Incoming

trait Login extends Incoming
case class UserLogin(username: Username, password: String) extends Login
case object GuestLogin extends Login
case object Logout extends Incoming

case class Seek(size: Int, time: Int, increment: Int, asPlayer: Option[Player])
extends Incoming
case class Accept(gameNumber: GameNumber) extends Incoming

/** Commands available to players in a game */
sealed trait GameCommand extends Incoming {
def gameNumber: GameNumber
}
case class Place(gameNumber: GameNumber, playStone: PlayStone) extends GameCommand
case class Move(gameNumber: GameNumber, start: BoardIndex, end: BoardIndex, drops: Vector[Int])
extends GameCommand
case class OfferDraw(gameNumber: GameNumber) extends GameCommand
case class RescindDrawOffer(gameNumber: GameNumber) extends GameCommand
case class Resign(gameNumber: GameNumber) extends GameCommand
case class Show(gameNumber: GameNumber) extends GameCommand
case class RequestUndo(gameNumber: GameNumber) extends GameCommand
case class RescindUndoRequest(gameNumber: GameNumber) extends GameCommand

case object ListSeeks extends Incoming
case object ListGames extends Incoming

case class Subscribe(gameNumber: GameNumber) extends Incoming
case class Unsubscribe(gameNumber: GameNumber) extends Incoming

case class Shout(msg: String) extends Incoming
case class JoinRoom(name: RoomName) extends Incoming
case class ShoutRoom(name: RoomName, msg: String) extends Incoming
case class LeaveRoom(name: RoomName) extends Incoming
case class Tell(username: Username, msg: String) extends Incoming

case object Ping extends Incoming
}

sealed trait Outgoing

object Outgoing {
case object Welcome extends Outgoing
case object LoginOrRegisterNow extends Outgoing
case class WelcomeUser(username: Username) extends Outgoing

sealed trait GameEvent extends Outgoing {
def gameNumber: GameNumber
}
case class GameListAdd(gameNumber: GameNumber,
whitePlayerusername: Username,
blackPlayerusername: Username,
size: Int,
time: Int,
increment: Int,
halfMovesPlayed: Int,
playerToMove: Player)
extends GameEvent
case class GameListRemove(gameNumber: GameNumber,
whitePlayerusername: Username,
blackPlayerusername: Username,
size: Int,
time: Int,
increment: Int,
halfMovesPlayed: Int,
playerToMove: Player)
extends GameEvent
case class GameStart(gameNumber: GameNumber,
size: Int,
whitePlayerusername: Username,
blackPlayerusername: Username,
yourColor: Player)
extends GameEvent
case class Place(gameNumber: GameNumber, playStone: PlayStone) extends GameEvent
case class Move(gameNumber: GameNumber, start: BoardIndex, end: BoardIndex, drops: Vector[Int])
extends GameEvent
case class UpdateTime(gameNumber: GameNumber, whiteTime: String, blackTime: String)
extends GameEvent // TODO check what type the time values are
case class GameOver(gameNumber: GameNumber, result: GameEndResult) extends GameEvent
case class DrawOffered(gameNumber: GameNumber) extends GameEvent
case class DrawOfferRescinded(gameNumber: GameNumber) extends GameEvent
case class UndoRequested(gameNumber: GameNumber) extends GameEvent
case class UndoRequestRescinded(gameNumber: GameNumber) extends GameEvent
case class PerformUndo(gameNumber: GameNumber) extends GameEvent
case class GameAbandoned(gameNumber: GameNumber) extends GameEvent
case class SeekAdded(gameNumber: GameNumber,
username: Username,
size: Int,
time: String,
asPlayer: Option[Player])
extends GameEvent
case class SeekRemoved(gameNumber: GameNumber,
username: Username,
size: Int,
time: String,
asPlayer: Option[Player])
extends GameEvent
case class ObserveGame(gameNumber: GameNumber,
whitePlayerusername: Username,
blackPlayerusername: Username,
size: Int,
time: String,
halfMovesPlayed: Int,
playerToMove: Player)
extends GameEvent
case class Shout(username: Username, msg: String) extends Outgoing
case class RoomJoined(name: RoomName) extends Outgoing
case class RoomLeft(name: RoomName) extends Outgoing
case class ShoutRoom(name: RoomName, username: Username, msg: String) extends Outgoing
case class Tell(username: Username, msg: String) extends Outgoing
case class Told(username: Username, msg: String) extends Outgoing
case class ServerMessage(msg: String) extends Outgoing
case class Error(msg: String) extends Outgoing
case class OnlineUsers(count: Int) extends Outgoing
case object NOK extends Outgoing
case object OK extends Outgoing
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package com.github.daenyth.opentak.protocol

import com.github.daenyth.opentak.protocol.Playtak.{GameNumber, RoomName, Username}
import com.github.daenyth.taklib.Implicits.RichParsing
import com.github.daenyth.taklib._

import scala.util.parsing.combinator.RegexParsers

/**
* 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)
def decode(input: String): Either[String, Playtak.Incoming] =
Incoming.parseEither(Incoming.incoming, input).toEither

/* 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

import Playtak.Incoming._

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
val num: Parser[Int] = """\d""".r ^^ { _.toInt }
val nums: Parser[Int] = """\d+""".r ^^ { _.toInt }
val username: Parser[Username] = "[a-zA-Z][a-zA-Z0-9_]{3,15}".r ^^ Username.apply
val email: Parser[String] = "[A-Za-z.0-9_+!#$%&'*^?=-]{1,30}@[A-Za-z.0-9-]{3,30}".r
val password: Parser[String] = """[^\n\r\s]{6,50}""".r
val gameNumber: Parser[GameNumber] = "Game#" ~ nums ^^ { case _ ~ n => GameNumber(n) }
val boardIndex: Parser[BoardIndex] = "([ABCDEFGH])([12345678])".r ^^ { str =>
val fileChr = str.charAt(0)
val file = "_ABCDEFGH".toCharArray.indexOf(fileChr)
val rank = str.charAt(1).toString.toInt
BoardIndex(file, rank)
}
val roomName: Parser[RoomName] = """[^\n\r\\s]{4,15}""".r ^^ RoomName
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 userLogin: Parser[UserLogin] = "Login" ~> username ~ password ^^ {
case username ~ password =>
UserLogin(username, password)
}
val guestLogin: Parser[GuestLogin.type] = "Login Guest" ^^^ GuestLogin
val logout: Parser[Logout.type] = "^quit$".r ^^^ Logout
val seek: Parser[Seek] = "Seek" ~> num ~ nums ~ nums ~ "[WB]?".r ^^ {
case size ~ time ~ increment ~ color =>
val asPlayer = color match {
case "W" => Some(White)
case "B" => Some(Black)
case _ => None
}
Seek(size, time, increment, asPlayer)
}
val accept: Parser[Accept] = "Accept" ~> nums ^^ { n =>
Accept(GameNumber(n))
}
val place: Parser[Place] = gameNumber ~ " P " ~ boardIndex ~ "[CW]?" ^^ {
case gameNumber ~ _ ~ idx ~ stoneType =>
val playStone = stoneType match {
case "C" => PlayCapstone(idx)
case "W" => PlayStanding(idx)
case _ => PlayFlat(idx)
}
Place(gameNumber, playStone)
}
val move: Parser[Move] = gameNumber ~ " M " ~ boardIndex ~ boardIndex ~ rep(num) ^^ {
case n ~ _ ~ start ~ end ~ drops => Move(n, start, end, drops.toVector)
}
val offerDraw: Parser[OfferDraw] = simpleGameMessage("OfferDraw", OfferDraw)
val rescindDrawOffer: Parser[RescindDrawOffer] =
simpleGameMessage("RemoveDraw", RescindDrawOffer)
val resign: Parser[Resign] = simpleGameMessage("Resign", Resign)
val show: Parser[Show] = simpleGameMessage("Show", Show)
val requestUndo: Parser[RequestUndo] = simpleGameMessage("RequestUndo", RequestUndo)
val rescindUndoRequest: Parser[RescindUndoRequest] =
simpleGameMessage("RemoveUndo", RescindUndoRequest)
val listSeeks: Parser[ListSeeks.type] = "^List$".r ^^^ ListSeeks
val listGames: Parser[ListGames.type] = "^GameList$".r ^^^ ListGames
val subscribe: Parser[Subscribe] = "Observe" ~> nums ^^ { n =>
Subscribe(GameNumber(n))
}
val unsubscribe: Parser[Unsubscribe] = "Unobserve" ~> nums ^^ { n =>
Unsubscribe(GameNumber(n))
}
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 tell: Parser[Tell] = "Tell" ~> username ~ msg ^^ { case user ~ msg => Tell(user, msg) }
val ping: Parser[Ping.type] = "^PING$".r ^^^ Ping

val incoming: Parser[Playtak.Incoming] =
(client | register | userLogin | guestLogin
| logout | seek | accept | place | move | offerDraw | rescindDrawOffer
| resign | show | requestUndo | rescindUndoRequest | listSeeks | listGames
| subscribe | unsubscribe | shout | joinRoom | shoutRoom | leaveRoom | tell | ping)

}

object Outgoing {
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 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"
}
}

private def encodeGameEvent(ge: Playtak.Outgoing.GameEvent): String = {
import Playtak.Outgoing._
ge match {
case a: GameListAdd =>
import a._
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
s"GameList Add Game#$gameNumber $whitePlayerusername vs $blackPlayerusername," +
s" ${size}x${size}, $time, $increment, $halfMovesPlayed half-moves played, $nextPlayer to move"
case r: GameListRemove =>
import r._
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
s"GameList Remove Game#$gameNumber $whitePlayerusername vs $blackPlayerusername," +
s" ${size}x${size}, $time, $increment, $halfMovesPlayed half-moves played, $nextPlayer to move"
case s: GameStart =>
import s._
s"Game Start $gameNumber $size $whitePlayerusername vs $blackPlayerusername $yourColor"
case Place(gameNumber, playStone) =>
import Stone._
val stoneType = playStone.stone match {
case _: Capstone => "C"
case _: StandingStone => "W"
case _: FlatStone => ""
}
s"Game#$gameNumber P ${playStone.at.name} $stoneType"
case m: Move =>
val start = m.start.name
val end = m.end.name
val drops = m.drops.mkString(" ")
s"Game#${m.gameNumber} M $start $end $drops"
case UpdateTime(gameNumber, whiteTime, blackTime) =>
s"Game#$gameNumber Time $whiteTime $blackTime"
case o: GameOver =>
import GameEndResult._
val result = o.result match {
case RoadWin(player) => player.fold("0-R", "R-0")
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 WinByResignation(player) =>
player.fold("0-1", "1-0") // Again not supported by playtak; this is PTN format
}
s"Game#${o.gameNumber} Over $result"
case DrawOffered(gameNumber) =>
s"Game#$gameNumber OfferDraw"
case DrawOfferRescinded(gameNumber) =>
s"Game#$gameNumber RemoveDraw"
case UndoRequested(gameNumber) =>
s"Game#$gameNumber RequestUndo"
case UndoRequestRescinded(gameNumber) =>
s"Game#$gameNumber RemoveUndo"
case PerformUndo(gameNumber) =>
s"Game#$gameNumber Undo"
case GameAbandoned(gameNumber) =>
s"Game#$gameNumber Abandoned"
case a: SeekAdded =>
import a._
val player = asPlayer.map(_.fold("B", "W")).getOrElse("")
s"Seek new $gameNumber $username $size $time $player"
case r: SeekRemoved =>
import r._
val player = asPlayer.map(_.fold("B", "W")).getOrElse("")
s"Seek remove $gameNumber $username $size $time $player"
case o: ObserveGame =>
import o._
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
s"Observe Game#$gameNumber $whitePlayerusername vs $blackPlayerusername, ${size}x${size}," +
s" $time, $halfMovesPlayed half-moves played, $nextPlayer to move"
}
}
}
}
Loading