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