Skip to content

Commit f11d9ac

Browse files
committed
Add parsers for Incoming protocol messages
1 parent df99d86 commit f11d9ac

File tree

4 files changed

+145
-8
lines changed

4 files changed

+145
-8
lines changed

build.sbt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ scalacOptions in (opentak, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnin
4242
resolvers += Resolver.sonatypeRepo("releases")
4343

4444
val scalazVersion = "7.2.8"
45+
val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5"
4546
val dependencies = Seq(
4647
"org.scalaz" %% "scalaz-core" % scalazVersion,
47-
"org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5",
48+
parserCombinators,
4849
"org.scala-graph" %% "graph-core" % "1.11.4"
4950
)
5051
val testDependencies = Seq(
@@ -78,6 +79,10 @@ libraryDependencies in tpsserver ++= Seq(
7879
"ch.qos.logback" % "logback-classic" % "1.2.1"
7980
) ++ testDependencies
8081

82+
libraryDependencies in opentak += parserCombinators
83+
libraryDependencies in opentak ++= testDependencies
84+
85+
8186
initialCommands in (taklib, console) += "import com.github.daenyth.taklib._"
8287

8388
coverageEnabled in taklib := true

opentak/src/main/scala/com/github/daenyth/opentak/protocol/Playtak.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ object Playtak {
2222
trait Login extends Incoming
2323
case class UserLogin(username: Username, password: String) extends Login
2424
case object GuestLogin extends Login
25-
case object Logout
25+
case object Logout extends Incoming
2626

2727
case class Seek(size: Int, time: Int, increment: Int, asPlayer: Option[Player])
2828
extends Incoming
@@ -54,7 +54,7 @@ object Playtak {
5454
case class LeaveRoom(name: RoomName) extends Incoming
5555
case class Tell(username: Username, msg: String) extends Incoming
5656

57-
case object Ping
57+
case object Ping extends Incoming
5858
}
5959

6060
sealed trait Outgoing

opentak/src/main/scala/com/github/daenyth/opentak/protocol/PlaytakCodec.scala

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,106 @@
11
package com.github.daenyth.opentak.protocol
22

3-
import com.github.daenyth.taklib.{GameEndResult, Stone}
3+
import com.github.daenyth.opentak.protocol.Playtak.{GameNumber, RoomName, Username}
4+
import com.github.daenyth.taklib.Implicits.RichParsing
5+
import com.github.daenyth.taklib._
6+
7+
import scala.util.parsing.combinator.RegexParsers
48

59
object PlaytakCodec {
610

7-
object Incoming {
8-
def decode(incoming: String): Either[String, Playtak.Incoming] = ???
11+
object Incoming extends RegexParsers with RichParsing {
12+
def decode(input: String): Either[String, Playtak.Incoming] =
13+
parseEither(incoming, input).toEither
14+
15+
import Playtak.Incoming._
16+
17+
val client: Parser[Client] = "Client" ~ "([A-Za-z-.0-9]{4,15})".r ^^ {
18+
case _ ~ s =>
19+
Client(s)
20+
}
21+
22+
val msg: Parser[String] = """[^\n\r]{1,256}""".r
23+
val num: Parser[Int] = """\d""".r ^^ { _.toInt }
24+
val nums: Parser[Int] = """\d+""".r ^^ { _.toInt }
25+
val username: Parser[Username] = "[a-zA-Z][a-zA-Z0-9_]{3,15}".r ^^ Username.apply
26+
val email: Parser[String] = "[A-Za-z.0-9_+!#$%&'*^?=-]{1,30}@[A-Za-z.0-9-]{3,30}".r
27+
val password: Parser[String] = """[^\n\r\s]{6,50}""".r
28+
val gameNumber: Parser[GameNumber] = "Game#" ~ nums ^^ { case _ ~ n => GameNumber(n) }
29+
val boardIndex: Parser[BoardIndex] = "([ABCDEFGH])([12345678])".r ^^ { str =>
30+
val fileChr = str.charAt(0)
31+
val file = "_ABCDEFGH".toCharArray.indexOf(fileChr)
32+
val rank = str.charAt(1).toString.toInt
33+
BoardIndex(file, rank)
34+
}
35+
val roomName: Parser[RoomName] = """[^\n\r\\s]{4,15}""".r ^^ RoomName
36+
def simpleGameMessage[A](str: String, gameMsg: GameNumber => A): Parser[A] =
37+
gameNumber <~ str ^^ gameMsg
38+
39+
val register: Parser[Register] = "Register" ~> username ~ email ^^ {
40+
case username ~ email =>
41+
Register(username, email)
42+
}
43+
val userLogin: Parser[UserLogin] = "Login" ~> username ~ password ^^ {
44+
case username ~ password =>
45+
UserLogin(username, password)
46+
}
47+
val guestLogin: Parser[GuestLogin.type] = "Login Guest" ^^^ GuestLogin
48+
val logout: Parser[Logout.type] = "^quit$".r ^^^ Logout
49+
val seek: Parser[Seek] = "Seek" ~> num ~ nums ~ nums ~ "[WB]?".r ^^ {
50+
case size ~ time ~ increment ~ color =>
51+
val asPlayer = color match {
52+
case "W" => Some(White)
53+
case "B" => Some(Black)
54+
case _ => None
55+
}
56+
Seek(size, time, increment, asPlayer)
57+
}
58+
val accept: Parser[Accept] = "Accept" ~> nums ^^ { n =>
59+
Accept(GameNumber(n))
60+
}
61+
val place: Parser[Place] = gameNumber ~ " P " ~ boardIndex ~ "[CW]?" ^^ {
62+
case gameNumber ~ _ ~ idx ~ stoneType =>
63+
val playStone = stoneType match {
64+
case "C" => PlayCapstone(idx)
65+
case "W" => PlayStanding(idx)
66+
case _ => PlayFlat(idx)
67+
}
68+
Place(gameNumber, playStone)
69+
}
70+
val move: Parser[Move] = gameNumber ~ " M " ~ boardIndex ~ boardIndex ~ rep(num) ^^ {
71+
case n ~ _ ~ start ~ end ~ drops => Move(n, start, end, drops.toVector)
72+
}
73+
val offerDraw: Parser[OfferDraw] = simpleGameMessage("OfferDraw", OfferDraw)
74+
val rescindDrawOffer: Parser[RescindDrawOffer] =
75+
simpleGameMessage("RemoveDraw", RescindDrawOffer)
76+
val resign: Parser[Resign] = simpleGameMessage("Resign", Resign)
77+
val show: Parser[Show] = simpleGameMessage("Show", Show)
78+
val requestUndo: Parser[RequestUndo] = simpleGameMessage("RequestUndo", RequestUndo)
79+
val rescindUndoRequest: Parser[RescindUndoRequest] =
80+
simpleGameMessage("RemoveUndo", RescindUndoRequest)
81+
val listSeeks: Parser[ListSeeks.type] = "^List$".r ^^^ ListSeeks
82+
val listGames: Parser[ListGames.type] = "^GameList$".r ^^^ ListGames
83+
val subscribe: Parser[Subscribe] = "Observe" ~> nums ^^ { n =>
84+
Subscribe(GameNumber(n))
85+
}
86+
val unsubscribe: Parser[Unsubscribe] = "Unobserve" ~> nums ^^ { n =>
87+
Unsubscribe(GameNumber(n))
88+
}
89+
val shout: Parser[Shout] = "Shout" ~> msg ^^ Shout
90+
val joinRoom: Parser[JoinRoom] = "JoinRoom" ~> roomName ^^ JoinRoom
91+
val leaveRoom: Parser[LeaveRoom] = "LeaveRoom" ~> roomName ^^ LeaveRoom
92+
val shoutRoom: Parser[ShoutRoom] = "ShoutRoom" ~> roomName ~ msg ^^ {
93+
case room ~ msg => ShoutRoom(room, msg)
94+
}
95+
val tell: Parser[Tell] = "Tell" ~> username ~ msg ^^ { case user ~ msg => Tell(user, msg) }
96+
val ping: Parser[Ping.type] = "^PING$".r ^^^ Ping
97+
98+
val incoming: Parser[Playtak.Incoming] =
99+
(client | register | userLogin | guestLogin
100+
| logout | seek | accept | place | move | offerDraw | rescindDrawOffer
101+
| resign | show | requestUndo | rescindUndoRequest | listSeeks | listGames
102+
| subscribe | unsubscribe | shout | joinRoom | shoutRoom | leaveRoom | tell | ping)
103+
9104
}
10105

11106
object Outgoing {
@@ -65,10 +160,12 @@ object PlaytakCodec {
65160
import GameEndResult._
66161
val result = o.result match {
67162
case RoadWin(player) => player.fold("0-R", "R-0")
68-
case DoubleRoad => "R-R" // Not actually supported by playtak or default rules, but different result sets can treat it differently.
163+
case DoubleRoad =>
164+
"R-R" // Not actually supported by playtak or default rules, but different result sets can treat it differently.
69165
case FlatWin(player) => player.fold("0-F", "F-0")
70166
case Draw => "1/2-1/2"
71-
case WinByResignation(player) => player.fold("0-1", "1-0") // Again not supported by playtak; this is PTN format
167+
case WinByResignation(player) =>
168+
player.fold("0-1", "1-0") // Again not supported by playtak; this is PTN format
72169
}
73170
s"Game#${o.gameNumber} Over $result"
74171
case DrawOffered(gameNumber) =>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.github.daenyth.opentak.protocol
2+
3+
import com.github.daenyth.opentak.protocol.Playtak.{GameNumber, Username}
4+
import com.github.daenyth.taklib.White
5+
import org.scalatest.{EitherValues, FlatSpec, Matchers}
6+
7+
class PlaytakCodecTest extends FlatSpec with Matchers with EitherValues {
8+
import PlaytakCodec.Incoming._
9+
import Playtak.Incoming._
10+
11+
def parse[A](parser: PlaytakCodec.Incoming.Parser[A], s: String): Either[String, A] =
12+
PlaytakCodec.Incoming.parseEither(parser, s).toEither
13+
14+
"client" should "parse" in {
15+
parse(client, "Client Foop") shouldBe Right(Client("Foop"))
16+
}
17+
18+
"register" should "parse" in {
19+
parse(register, "Register Daenyth daenyth@gmail.com") shouldBe Right(
20+
Register(
21+
Username("Daenyth"),
22+
"daenyth@gmail.com"
23+
)
24+
)
25+
}
26+
27+
"seek" should "parse" in {
28+
parse(seek, "Seek 5 600 0") shouldBe Right(Seek(5, 600, 0, None))
29+
parse(seek, "Seek 5 600 0 W") shouldBe Right(Seek(5, 600, 0, Some(White)))
30+
}
31+
32+
"accept" should "parse" in {
33+
parse(accept, "Accept 123456") shouldBe Right(Accept(GameNumber(123456)))
34+
}
35+
}

0 commit comments

Comments
 (0)