Skip to content

Commit aa6c64e

Browse files
authored
Merge pull request #10 from Daenyth/playtak-backend
Begin playtak backend module
2 parents 37df454 + b86daf5 commit aa6c64e

File tree

12 files changed

+442
-33
lines changed

12 files changed

+442
-33
lines changed

build.sbt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,23 @@ lazy val takcli = (project in file("takcli"))
2929
lazy val tpsserver = (project in file("tpsserver"))
3030
.dependsOn(taklib)
3131
.settings(commonSettings, name := "tpsserver")
32+
lazy val opentak = (project in file("opentak"))
33+
.dependsOn(taklib)
34+
.settings(commonSettings, name := "opentak")
3235

3336
// Remove these options in 'sbt console' because they're not nice for interactive usage
3437
scalacOptions in (taklib, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains))
3538
scalacOptions in (takcli, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains))
3639
scalacOptions in (tpsserver, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains))
40+
scalacOptions in (opentak, Compile, console) ~= (_.filterNot(Set("-Xfatal-warnings", "-Ywarn-unused-import").contains))
3741

3842
resolvers += Resolver.sonatypeRepo("releases")
3943

4044
val scalazVersion = "7.2.8"
45+
val parserCombinators = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5"
4146
val dependencies = Seq(
4247
"org.scalaz" %% "scalaz-core" % scalazVersion,
43-
"org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.5",
48+
parserCombinators,
4449
"org.scala-graph" %% "graph-core" % "1.11.4"
4550
)
4651
val testDependencies = Seq(
@@ -74,6 +79,10 @@ libraryDependencies in tpsserver ++= Seq(
7479
"ch.qos.logback" % "logback-classic" % "1.2.1"
7580
) ++ testDependencies
7681

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

7988
coverageEnabled in taklib := true
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.github.daenyth.opentak.protocol
2+
3+
import com.github.daenyth.taklib.{BoardIndex, GameEndResult, PlayStone, Player}
4+
5+
// See protocol description at https://github.com/chaitu236/TakServer
6+
object Playtak {
7+
case class GameNumber(value: Int) extends AnyVal
8+
case class Username(value: String) extends AnyVal {
9+
override def toString: String = value
10+
}
11+
case class RoomName(value: String) extends AnyVal {
12+
override def toString: String = value
13+
}
14+
15+
sealed trait Incoming
16+
17+
object Incoming {
18+
19+
case class Client(version: String) extends Incoming
20+
case class Register(username: Username, email: String) extends Incoming
21+
22+
trait Login extends Incoming
23+
case class UserLogin(username: Username, password: String) extends Login
24+
case object GuestLogin extends Login
25+
case object Logout extends Incoming
26+
27+
case class Seek(size: Int, time: Int, increment: Int, asPlayer: Option[Player])
28+
extends Incoming
29+
case class Accept(gameNumber: GameNumber) extends Incoming
30+
31+
/** Commands available to players in a game */
32+
sealed trait GameCommand extends Incoming {
33+
def gameNumber: GameNumber
34+
}
35+
case class Place(gameNumber: GameNumber, playStone: PlayStone) extends GameCommand
36+
case class Move(gameNumber: GameNumber, start: BoardIndex, end: BoardIndex, drops: Vector[Int])
37+
extends GameCommand
38+
case class OfferDraw(gameNumber: GameNumber) extends GameCommand
39+
case class RescindDrawOffer(gameNumber: GameNumber) extends GameCommand
40+
case class Resign(gameNumber: GameNumber) extends GameCommand
41+
case class Show(gameNumber: GameNumber) extends GameCommand
42+
case class RequestUndo(gameNumber: GameNumber) extends GameCommand
43+
case class RescindUndoRequest(gameNumber: GameNumber) extends GameCommand
44+
45+
case object ListSeeks extends Incoming
46+
case object ListGames extends Incoming
47+
48+
case class Subscribe(gameNumber: GameNumber) extends Incoming
49+
case class Unsubscribe(gameNumber: GameNumber) extends Incoming
50+
51+
case class Shout(msg: String) extends Incoming
52+
case class JoinRoom(name: RoomName) extends Incoming
53+
case class ShoutRoom(name: RoomName, msg: String) extends Incoming
54+
case class LeaveRoom(name: RoomName) extends Incoming
55+
case class Tell(username: Username, msg: String) extends Incoming
56+
57+
case object Ping extends Incoming
58+
}
59+
60+
sealed trait Outgoing
61+
62+
object Outgoing {
63+
case object Welcome extends Outgoing
64+
case object LoginOrRegisterNow extends Outgoing
65+
case class WelcomeUser(username: Username) extends Outgoing
66+
67+
sealed trait GameEvent extends Outgoing {
68+
def gameNumber: GameNumber
69+
}
70+
case class GameListAdd(gameNumber: GameNumber,
71+
whitePlayerusername: Username,
72+
blackPlayerusername: Username,
73+
size: Int,
74+
time: Int,
75+
increment: Int,
76+
halfMovesPlayed: Int,
77+
playerToMove: Player)
78+
extends GameEvent
79+
case class GameListRemove(gameNumber: GameNumber,
80+
whitePlayerusername: Username,
81+
blackPlayerusername: Username,
82+
size: Int,
83+
time: Int,
84+
increment: Int,
85+
halfMovesPlayed: Int,
86+
playerToMove: Player)
87+
extends GameEvent
88+
case class GameStart(gameNumber: GameNumber,
89+
size: Int,
90+
whitePlayerusername: Username,
91+
blackPlayerusername: Username,
92+
yourColor: Player)
93+
extends GameEvent
94+
case class Place(gameNumber: GameNumber, playStone: PlayStone) extends GameEvent
95+
case class Move(gameNumber: GameNumber, start: BoardIndex, end: BoardIndex, drops: Vector[Int])
96+
extends GameEvent
97+
case class UpdateTime(gameNumber: GameNumber, whiteTime: String, blackTime: String)
98+
extends GameEvent // TODO check what type the time values are
99+
case class GameOver(gameNumber: GameNumber, result: GameEndResult) extends GameEvent
100+
case class DrawOffered(gameNumber: GameNumber) extends GameEvent
101+
case class DrawOfferRescinded(gameNumber: GameNumber) extends GameEvent
102+
case class UndoRequested(gameNumber: GameNumber) extends GameEvent
103+
case class UndoRequestRescinded(gameNumber: GameNumber) extends GameEvent
104+
case class PerformUndo(gameNumber: GameNumber) extends GameEvent
105+
case class GameAbandoned(gameNumber: GameNumber) extends GameEvent
106+
case class SeekAdded(gameNumber: GameNumber,
107+
username: Username,
108+
size: Int,
109+
time: String,
110+
asPlayer: Option[Player])
111+
extends GameEvent
112+
case class SeekRemoved(gameNumber: GameNumber,
113+
username: Username,
114+
size: Int,
115+
time: String,
116+
asPlayer: Option[Player])
117+
extends GameEvent
118+
case class ObserveGame(gameNumber: GameNumber,
119+
whitePlayerusername: Username,
120+
blackPlayerusername: Username,
121+
size: Int,
122+
time: String,
123+
halfMovesPlayed: Int,
124+
playerToMove: Player)
125+
extends GameEvent
126+
case class Shout(username: Username, msg: String) extends Outgoing
127+
case class RoomJoined(name: RoomName) extends Outgoing
128+
case class RoomLeft(name: RoomName) extends Outgoing
129+
case class ShoutRoom(name: RoomName, username: Username, msg: String) extends Outgoing
130+
case class Tell(username: Username, msg: String) extends Outgoing
131+
case class Told(username: Username, msg: String) extends Outgoing
132+
case class ServerMessage(msg: String) extends Outgoing
133+
case class Error(msg: String) extends Outgoing
134+
case class OnlineUsers(count: Int) extends Outgoing
135+
case object NOK extends Outgoing
136+
case object OK extends Outgoing
137+
}
138+
139+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package com.github.daenyth.opentak.protocol
2+
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
8+
9+
/**
10+
* Playtak protocol encoding/decoding between the wire
11+
* representation (string) and in-library representation (case classes)
12+
*
13+
* See PlaytakCodec#encode and PlaytakCodec#decode
14+
*/
15+
object PlaytakCodec {
16+
def encode(outgoing: Playtak.Outgoing): String =
17+
Outgoing.encode(outgoing)
18+
def decode(input: String): Either[String, Playtak.Incoming] =
19+
Incoming.parseEither(Incoming.incoming, input).toEither
20+
21+
/* Abandon hope all ye who scroll below here */
22+
23+
object Incoming extends RegexParsers with RichParsing {
24+
def decode(input: String): Either[String, Playtak.Incoming] =
25+
parseEither(incoming, input).toEither
26+
27+
import Playtak.Incoming._
28+
29+
val client: Parser[Client] = "Client" ~ "([A-Za-z-.0-9]{4,15})".r ^^ {
30+
case _ ~ s =>
31+
Client(s)
32+
}
33+
34+
val msg: Parser[String] = """[^\n\r]{1,256}""".r
35+
val num: Parser[Int] = """\d""".r ^^ { _.toInt }
36+
val nums: Parser[Int] = """\d+""".r ^^ { _.toInt }
37+
val username: Parser[Username] = "[a-zA-Z][a-zA-Z0-9_]{3,15}".r ^^ Username.apply
38+
val email: Parser[String] = "[A-Za-z.0-9_+!#$%&'*^?=-]{1,30}@[A-Za-z.0-9-]{3,30}".r
39+
val password: Parser[String] = """[^\n\r\s]{6,50}""".r
40+
val gameNumber: Parser[GameNumber] = "Game#" ~ nums ^^ { case _ ~ n => GameNumber(n) }
41+
val boardIndex: Parser[BoardIndex] = "([ABCDEFGH])([12345678])".r ^^ { str =>
42+
val fileChr = str.charAt(0)
43+
val file = "_ABCDEFGH".toCharArray.indexOf(fileChr)
44+
val rank = str.charAt(1).toString.toInt
45+
BoardIndex(file, rank)
46+
}
47+
val roomName: Parser[RoomName] = """[^\n\r\\s]{4,15}""".r ^^ RoomName
48+
def simpleGameMessage[A](str: String, gameMsg: GameNumber => A): Parser[A] =
49+
gameNumber <~ str ^^ gameMsg
50+
51+
val register: Parser[Register] = "Register" ~> username ~ email ^^ {
52+
case username ~ email =>
53+
Register(username, email)
54+
}
55+
val userLogin: Parser[UserLogin] = "Login" ~> username ~ password ^^ {
56+
case username ~ password =>
57+
UserLogin(username, password)
58+
}
59+
val guestLogin: Parser[GuestLogin.type] = "Login Guest" ^^^ GuestLogin
60+
val logout: Parser[Logout.type] = "^quit$".r ^^^ Logout
61+
val seek: Parser[Seek] = "Seek" ~> num ~ nums ~ nums ~ "[WB]?".r ^^ {
62+
case size ~ time ~ increment ~ color =>
63+
val asPlayer = color match {
64+
case "W" => Some(White)
65+
case "B" => Some(Black)
66+
case _ => None
67+
}
68+
Seek(size, time, increment, asPlayer)
69+
}
70+
val accept: Parser[Accept] = "Accept" ~> nums ^^ { n =>
71+
Accept(GameNumber(n))
72+
}
73+
val place: Parser[Place] = gameNumber ~ " P " ~ boardIndex ~ "[CW]?" ^^ {
74+
case gameNumber ~ _ ~ idx ~ stoneType =>
75+
val playStone = stoneType match {
76+
case "C" => PlayCapstone(idx)
77+
case "W" => PlayStanding(idx)
78+
case _ => PlayFlat(idx)
79+
}
80+
Place(gameNumber, playStone)
81+
}
82+
val move: Parser[Move] = gameNumber ~ " M " ~ boardIndex ~ boardIndex ~ rep(num) ^^ {
83+
case n ~ _ ~ start ~ end ~ drops => Move(n, start, end, drops.toVector)
84+
}
85+
val offerDraw: Parser[OfferDraw] = simpleGameMessage("OfferDraw", OfferDraw)
86+
val rescindDrawOffer: Parser[RescindDrawOffer] =
87+
simpleGameMessage("RemoveDraw", RescindDrawOffer)
88+
val resign: Parser[Resign] = simpleGameMessage("Resign", Resign)
89+
val show: Parser[Show] = simpleGameMessage("Show", Show)
90+
val requestUndo: Parser[RequestUndo] = simpleGameMessage("RequestUndo", RequestUndo)
91+
val rescindUndoRequest: Parser[RescindUndoRequest] =
92+
simpleGameMessage("RemoveUndo", RescindUndoRequest)
93+
val listSeeks: Parser[ListSeeks.type] = "^List$".r ^^^ ListSeeks
94+
val listGames: Parser[ListGames.type] = "^GameList$".r ^^^ ListGames
95+
val subscribe: Parser[Subscribe] = "Observe" ~> nums ^^ { n =>
96+
Subscribe(GameNumber(n))
97+
}
98+
val unsubscribe: Parser[Unsubscribe] = "Unobserve" ~> nums ^^ { n =>
99+
Unsubscribe(GameNumber(n))
100+
}
101+
val shout: Parser[Shout] = "Shout" ~> msg ^^ Shout
102+
val joinRoom: Parser[JoinRoom] = "JoinRoom" ~> roomName ^^ JoinRoom
103+
val leaveRoom: Parser[LeaveRoom] = "LeaveRoom" ~> roomName ^^ LeaveRoom
104+
val shoutRoom: Parser[ShoutRoom] = "ShoutRoom" ~> roomName ~ msg ^^ {
105+
case room ~ msg => ShoutRoom(room, msg)
106+
}
107+
val tell: Parser[Tell] = "Tell" ~> username ~ msg ^^ { case user ~ msg => Tell(user, msg) }
108+
val ping: Parser[Ping.type] = "^PING$".r ^^^ Ping
109+
110+
val incoming: Parser[Playtak.Incoming] =
111+
(client | register | userLogin | guestLogin
112+
| logout | seek | accept | place | move | offerDraw | rescindDrawOffer
113+
| resign | show | requestUndo | rescindUndoRequest | listSeeks | listGames
114+
| subscribe | unsubscribe | shout | joinRoom | shoutRoom | leaveRoom | tell | ping)
115+
116+
}
117+
118+
object Outgoing {
119+
def encode(outgoing: Playtak.Outgoing): String = {
120+
import Playtak.Outgoing._
121+
outgoing match {
122+
case Welcome => "Welcome!"
123+
case LoginOrRegisterNow => "Login or Register"
124+
case WelcomeUser(username) => s"Welcome $username"
125+
case ge: GameEvent => encodeGameEvent(ge)
126+
case Shout(username, msg) => s"Shout <$username> $msg"
127+
case RoomJoined(name) => s"Joined room $name"
128+
case RoomLeft(name) => s"Left room $name"
129+
case ShoutRoom(name, username, msg) => s"ShoutRoom $name <$username> $msg"
130+
case Tell(username, msg) => s"Tell <$username> $msg"
131+
case Told(username, msg) => s"Told <$username> $msg"
132+
case ServerMessage(msg) => s"Message $msg"
133+
case Error(msg) => s"Error $msg"
134+
case OnlineUsers(count) => s"Online $count"
135+
case NOK => "NOK"
136+
case OK => "OK"
137+
}
138+
}
139+
140+
private def encodeGameEvent(ge: Playtak.Outgoing.GameEvent): String = {
141+
import Playtak.Outgoing._
142+
ge match {
143+
case a: GameListAdd =>
144+
import a._
145+
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
146+
s"GameList Add Game#$gameNumber $whitePlayerusername vs $blackPlayerusername," +
147+
s" ${size}x${size}, $time, $increment, $halfMovesPlayed half-moves played, $nextPlayer to move"
148+
case r: GameListRemove =>
149+
import r._
150+
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
151+
s"GameList Remove Game#$gameNumber $whitePlayerusername vs $blackPlayerusername," +
152+
s" ${size}x${size}, $time, $increment, $halfMovesPlayed half-moves played, $nextPlayer to move"
153+
case s: GameStart =>
154+
import s._
155+
s"Game Start $gameNumber $size $whitePlayerusername vs $blackPlayerusername $yourColor"
156+
case Place(gameNumber, playStone) =>
157+
import Stone._
158+
val stoneType = playStone.stone match {
159+
case _: Capstone => "C"
160+
case _: StandingStone => "W"
161+
case _: FlatStone => ""
162+
}
163+
s"Game#$gameNumber P ${playStone.at.name} $stoneType"
164+
case m: Move =>
165+
val start = m.start.name
166+
val end = m.end.name
167+
val drops = m.drops.mkString(" ")
168+
s"Game#${m.gameNumber} M $start $end $drops"
169+
case UpdateTime(gameNumber, whiteTime, blackTime) =>
170+
s"Game#$gameNumber Time $whiteTime $blackTime"
171+
case o: GameOver =>
172+
import GameEndResult._
173+
val result = o.result match {
174+
case RoadWin(player) => player.fold("0-R", "R-0")
175+
case DoubleRoad =>
176+
"R-R" // Not actually supported by playtak or default rules, but different result sets can treat it differently.
177+
case FlatWin(player) => player.fold("0-F", "F-0")
178+
case Draw => "1/2-1/2"
179+
case WinByResignation(player) =>
180+
player.fold("0-1", "1-0") // Again not supported by playtak; this is PTN format
181+
}
182+
s"Game#${o.gameNumber} Over $result"
183+
case DrawOffered(gameNumber) =>
184+
s"Game#$gameNumber OfferDraw"
185+
case DrawOfferRescinded(gameNumber) =>
186+
s"Game#$gameNumber RemoveDraw"
187+
case UndoRequested(gameNumber) =>
188+
s"Game#$gameNumber RequestUndo"
189+
case UndoRequestRescinded(gameNumber) =>
190+
s"Game#$gameNumber RemoveUndo"
191+
case PerformUndo(gameNumber) =>
192+
s"Game#$gameNumber Undo"
193+
case GameAbandoned(gameNumber) =>
194+
s"Game#$gameNumber Abandoned"
195+
case a: SeekAdded =>
196+
import a._
197+
val player = asPlayer.map(_.fold("B", "W")).getOrElse("")
198+
s"Seek new $gameNumber $username $size $time $player"
199+
case r: SeekRemoved =>
200+
import r._
201+
val player = asPlayer.map(_.fold("B", "W")).getOrElse("")
202+
s"Seek remove $gameNumber $username $size $time $player"
203+
case o: ObserveGame =>
204+
import o._
205+
val nextPlayer = playerToMove.fold(blackPlayerusername, whitePlayerusername)
206+
s"Observe Game#$gameNumber $whitePlayerusername vs $blackPlayerusername, ${size}x${size}," +
207+
s" $time, $halfMovesPlayed half-moves played, $nextPlayer to move"
208+
}
209+
}
210+
}
211+
}

0 commit comments

Comments
 (0)