├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.sbt ├── build.sh ├── build └── .gitignore ├── norhino.sbt ├── project ├── build.properties └── plugins.sbt ├── scalachess.js ├── src └── main │ └── scala │ ├── scalachess │ └── scalachessjs │ ├── Main.scala │ └── PgnDump.scala └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /project/project/ 3 | /project/target/ 4 | /target/ 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/scalachess"] 2 | path = submodules/scalachess 3 | url = https://github.com/veloce/scalachess.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vincent Velociter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scalachess.js 2 | 3 | scalachess.js is a chess library that runs in a webworker, with multi-variants support. 4 | 5 | It is based on the awesome [scalachess](https://github.com/ornicar/scalachess) library 6 | compiled to JavaScript, thanks to [Scala.js](https://www.scala-js.org/). 7 | 8 | It is currently used in production in [lichess.org](http://lichess.org) mobile 9 | application. So you can see a [real world usage](https://github.com/veloce/lichobile/blob/master/src/chess.ts) on [the mobile app repo](https://github.com/veloce/lichobile). 10 | 11 | ## Features 12 | 13 | * Fully asynchronous and does not block the main thread: as it is executed in a web worker, you can safely compute chess logic while running screen animations 14 | * Completely stateless: you send the complete game position in each request, 15 | either with FEN or PGN 16 | * Built from extensively tested code: the scalachess library is by itself well tested 17 | and powers all the chess logic of the thousands of games being played 18 | every day on [lichess.org](http://lichess.org). 19 | * Multi variants support: Chess 960, King Of The Hill, Three-check, Antichess, 20 | Atomic chess, Horde, Racing Kings, Crazyhouse! 21 | 22 | ## Build 23 | 24 | $ git submodule update --init 25 | $ ./build.sh 26 | 27 | Generated file will be in `build` dir. 28 | 29 | ## API 30 | 31 | ### Message Format 32 | 33 | The same format is used for requests and responses: 34 | 35 | ```typescript 36 | interface ScalachessMessage { 37 | topic: string 38 | payload?: Object 39 | reqid?: string 40 | } 41 | ``` 42 | 43 | The worker will always reply with the same `topic` field of the request. 44 | `payload` is either request or response data. The optional `reqid` 45 | can be used to differentiate responses if you are sending a lot of requests with 46 | the same topic at the same time. 47 | 48 | ### Topics 49 | 50 | * `init` will initialize the board for a given variant 51 | * `dests` will get possible destinations for a given position 52 | * `move` will play a move for a given position set by FEN; you can optionally 53 | pass a `pgnMoves` array so you can accumulate moves. Useful for other requests 54 | like `threefoldTest`. Pass an `uciMoves` array if you want to accumulate moves 55 | in the UCI format 56 | * `threefoldTest` test if current situation falls under the threefold repetition 57 | rule. Warning: it can be slow since it has to replay the whole game from the 58 | PGN moves 59 | * `pgnRead` parse a PGN string a returns the whole game history 60 | * `pgnDump` takes an initial FEN, a list of moves and returns a formatted PGN 61 | string 62 | 63 | An up-to-date and complete API documentation for all topics can be found in the [typescript interfaces](https://github.com/veloce/lichobile/blob/master/src/chess.ts#L34) of lichess mobile client. 64 | 65 | #### Init 66 | 67 | Request: 68 | 69 | ```js 70 | chessWorker.postMessage({ 71 | topic: 'init', 72 | payload: { 73 | variant: 'chess960' 74 | } 75 | }); 76 | ``` 77 | 78 | Response: 79 | 80 | ```js 81 | { 82 | "payload": { 83 | "setup": { 84 | "check": false, 85 | "dests": { 86 | "a2": [ "a3", "a4" ], "b2": [ "b3", "b4" ], "c1": [ "b3", "d3" ], "c2": [ "c3", "c4" ], "d2": [ "d3", "d4" ], "e2": [ "e3", "e4" ], "f2": [ "f3", "f4" ], "g2": [ "g3", "g4" ], "h1": [ "g3" ], "h2": [ "h3", "h4" ] 87 | }, 88 | "fen": "rbnkbqrn/pppppppp/8/8/8/8/PPPPPPPP/RBNKBQRN w KQkq - 0 1", 89 | "pgnMoves": [], 90 | "playable": true, 91 | "player": "white", 92 | "ply": 0 93 | }, 94 | "variant": { 95 | "key": "chess960", 96 | "name": "Chess960", 97 | "shortName": "960", 98 | "title": "Starting position of the home rank pieces is randomized." 99 | } 100 | }, 101 | "topic": "init" 102 | } 103 | ``` 104 | 105 | #### Dests 106 | 107 | Request: 108 | 109 | ```js 110 | chessWorker.postMessage({ 111 | topic: 'dests', 112 | payload: { 113 | fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 114 | variant: 'kingOfTheHill' 115 | } 116 | }); 117 | ``` 118 | 119 | Response: 120 | ```js 121 | { 122 | "payload": { 123 | "dests": { 124 | "a2": [ "a3", "a4" ], "b1": [ "a3", "c3" ], "b2": [ "b3", "b4" ], "c2": [ "c3", "c4" ], "d2": [ "d3", "d4" ], "e2": [ "e3", "e4" ], "f2": [ "f3", "f4" ], "g1": [ "f3", "h3" ], "g2": [ "g3", "g4" ], "h2": [ "h3", "h4" ] 125 | } 126 | }, 127 | "topic": "dests" 128 | } 129 | ``` 130 | 131 | #### Move 132 | 133 | Request: 134 | 135 | ```js 136 | worker.postMessage({ 137 | topic: 'move', 138 | payload: { 139 | fen: 'rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2 +0+0', 140 | variant: 'threeCheck', 141 | pgnMoves: ['e4', 'e5'], 142 | uciMoves: ['e2e4', 'e7e5'], 143 | orig: 'd2', 144 | dest: 'd4', 145 | path: '0' 146 | }, 147 | reqid: '1' 148 | }); 149 | ``` 150 | 151 | Response: 152 | ```js 153 | { 154 | "reqid": "1", 155 | "path": "0", 156 | "situation": { 157 | "check": false, 158 | "checkCount": { 159 | "black": 0, 160 | "white": 0 161 | }, 162 | "dests": { 163 | "a7": [ "a6", "a5" ], "b7": [ "b6", "b5" ], "b8": [ "a6", "c6" ], "c7": [ "c6", "c5" ], "d7": [ "d6", "d5" ], "d8": [ "e7", "f6", "g5", "h4" ], "e5": [ "d4" ], "e8": [ "e7" ], "f7": [ "f6", "f5" ], "f8": [ "e7", "d6", "c5", "b4", "a3" ], "g7": [ "g6", "g5" ], "g8": [ "e7", "f6", "h6" ], "h7": [ "h6", "h5" ] 164 | }, 165 | "end": false, 166 | "fen": "rnbqkbnr/pppp1ppp/8/4p3/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2 +0+0", 167 | "pgnMoves": [ 168 | "e4", "e5", "d4" 169 | ], 170 | "playable": true, 171 | "player": "black", 172 | "ply": 3, 173 | "uciMoves": [ 174 | "e2e4", "e7e5", "d2d4" 175 | ], 176 | "variant": "threeCheck" 177 | } 178 | } 179 | ``` 180 | 181 | 182 | #### pgnRead 183 | 184 | Request 185 | 186 | ```js 187 | 188 | var pgn = '[Event "Fischer - Spassky World Championship Match"]\n' + 189 | '[Site "Reykjavik ISL"]\n' + 190 | '[Date "1972.08.22"]\n' + 191 | '[EventDate "?"]\n' + 192 | '[Round "17"]\n' + 193 | '[Result "1/2-1/2"]\n' + 194 | '[White "Boris Spassky"]\n' + 195 | '[Black "Robert James Fischer"]\n' + 196 | '[ECO "B09"]\n' + 197 | '[WhiteElo "?"]\n' + 198 | '[BlackElo "?"]\n' + 199 | '[PlyCount "89"]\n' + 200 | '\n' + 201 | '1. e4 d6 2. d4 g6 3. Nc3 Nf6 4. f4 Bg7 5. Nf3 c5 6. dxc5 Qa5\n' + 202 | '7. Bd3 Qxc5 8. Qe2 O-O 9. Be3 Qa5 10. O-O Bg4 11. Rad1 Nc6\n' + 203 | '12. Bc4 Nh5 13. Bb3 Bxc3 14. bxc3 Qxc3 15. f5 Nf6 16. h3 Bxf3\n' + 204 | '17. Qxf3 Na5 18. Rd3 Qc7 19. Bh6 Nxb3 20. cxb3 Qc5+ 21. Kh1\n' + 205 | 'Qe5 22. Bxf8 Rxf8 23. Re3 Rc8 24. fxg6 hxg6 25. Qf4 Qxf4\n' + 206 | '26. Rxf4 Nd7 27. Rf2 Ne5 28. Kh2 Rc1 29. Ree2 Nc6 30. Rc2 Re1\n' + 207 | '31. Rfe2 Ra1 32. Kg3 Kg7 33. Rcd2 Rf1 34. Rf2 Re1 35. Rfe2 Rf1\n' + 208 | '36. Re3 a6 37. Rc3 Re1 38. Rc4 Rf1 39. Rdc2 Ra1 40. Rf2 Re1\n' + 209 | '41. Rfc2 g5 42. Rc1 Re2 43. R1c2 Re1 44. Rc1 Re2 45. R1c2\n' + 210 | '1/2-1/2'; 211 | 212 | worker.postMessage({ 213 | topic: 'pgnRead', 214 | payload: { 215 | pgn: pgn 216 | } 217 | }); 218 | ``` 219 | 220 | Response 221 | 222 | ```js 223 | { 224 | "replay": [ 225 | { 226 | "check": false, 227 | "checkCount": { 228 | "black": 0, 229 | "white": 0 230 | }, 231 | "dests": { 232 | "a2": [ "a3", "a4" ], "b1": [ "a3", "c3" ], "b2": [ "b3", "b4" ], "c2": [ "c3", "c4" ], "d2": [ "d3", "d4" ], "e2": [ "e3", "e4" ], "f2": [ "f3", "f4" ], "g1": [ "f3", "h3" ], "g2": [ "g3", "g4" ], "h2": [ "h3", "h4" ] 233 | }, 234 | "end": false, 235 | "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 236 | "pgnMoves": [], 237 | "playable": true, 238 | "player": "white", 239 | "ply": 0, 240 | "uciMoves": [], 241 | "variant": "standard" 242 | }, 243 | { 244 | "check": false, 245 | "checkCount": { 246 | "black": 0, 247 | "white": 0 248 | }, 249 | "dests": { 250 | "a7": [ "a6", "a5" ], "b7": [ "b6", "b5" ], "b8": [ "a6", "c6" ], "c7": [ "c6", "c5" ], "d7": [ "d6", "d5" ], "e7": [ "e6", "e5" ], "f7": [ "f6", "f5" ], "g7": [ "g6", "g5" ], "g8": [ "f6", "h6" ], "h7": [ "h6", "h5" ] 251 | }, 252 | "end": false, 253 | "fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 254 | "pgnMoves": [ 255 | "e4" 256 | ], 257 | "playable": true, 258 | "player": "black", 259 | "ply": 1, 260 | "uciMoves": [ 261 | "e2e4" 262 | ], 263 | "variant": "standard" 264 | }, 265 | { 266 | "check": false, 267 | "checkCount": { 268 | "black": 0, 269 | "white": 0 270 | }, 271 | "dests": { 272 | "a2": [ "a3", "a4" ], "b1": [ "a3", "c3" ], "b2": [ "b3", "b4" ], "c2": [ "c3", "c4" ], "d1": [ "e2", "f3", "g4", "h5" ], "d2": [ "d3", "d4" ], "e1": [ "e2" ], "e4": [ "e5" ], "f1": [ "e2", "d3", "c4", "b5", "a6" ], "f2": [ "f3", "f4" ], "g1": [ "f3", "h3", "e2" ], "g2": [ "g3", "g4" ], "h2": [ "h3", "h4" ] 273 | }, 274 | "end": false, 275 | "fen": "rnbqkbnr/ppp1pppp/3p4/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", 276 | "pgnMoves": [ 277 | "e4", 278 | "d6" 279 | ], 280 | "playable": true, 281 | "player": "white", 282 | "ply": 2, 283 | "uciMoves": [ 284 | "d7d6" 285 | ], 286 | "variant": "standard" 287 | }, 288 | { 289 | "check": false, 290 | "checkCount": { 291 | "black": 0, 292 | "white": 0 293 | }, 294 | "dests": { 295 | "a7": [ "a6", "a5" ], "b7": [ "b6", "b5" ], "b8": [ "d7", "a6", "c6" ], "c7": [ "c6", "c5" ], "c8": [ "d7", "e6", "f5", "g4", "h3" ], "d6": [ "d5" ], "d8": [ "d7" ], "e7": [ "e6", "e5" ], "e8": [ "d7" ], "f7": [ "f6", "f5" ], "g7": [ "g6", "g5" ], "g8": [ "f6", "h6" ], "h7": [ "h6", "h5" ] 296 | }, 297 | "end": false, 298 | "fen": "rnbqkbnr/ppp1pppp/3p4/8/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 2", 299 | "pgnMoves": [ 300 | "e4", 301 | "d6", 302 | "d4" 303 | ], 304 | "playable": true, 305 | "player": "black", 306 | "ply": 3, 307 | "uciMoves": [ 308 | "d2d4" 309 | ], 310 | "variant": "standard" 311 | }, 312 | ... // till up to ply 89 313 | ], 314 | "setup": { 315 | "check": false, 316 | "checkCount": { 317 | "black": 0, 318 | "white": 0 319 | }, 320 | "dests": { 321 | "a2": [ "a3", "a4" ], "b1": [ "a3", "c3" ], "b2": [ "b3", "b4" ], "c2": [ "c3", "c4" ], "d2": [ "d3", "d4" ], "e2": [ "e3", "e4" ], "f2": [ "f3", "f4" ], "g1": [ "f3", "h3" ], "g2": [ "g3", "g4" ], "h2": [ "h3", "h4" ] 322 | }, 323 | "end": false, 324 | "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 325 | "pgnMoves": [], 326 | "playable": true, 327 | "player": "white", 328 | "ply": 0, 329 | "uciMoves": [], 330 | "variant": "standard" 331 | }, 332 | "variant": { 333 | "key": "standard", 334 | "name": "Standard", 335 | "shortName": "Std", 336 | "title": "Standard rules of chess (FIDE)" 337 | } 338 | } 339 | ``` 340 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | enablePlugins(ScalaJSPlugin) 2 | 3 | name := "scalachessJs" 4 | 5 | version := "1.10" 6 | 7 | scalaVersion := "2.12.6" 8 | 9 | libraryDependencies ++= List( 10 | "org.scala-js" %%% "scalajs-dom" % "0.9.6", 11 | "org.scala-lang.modules" %%% "scala-parser-combinators" % "1.1.0", 12 | "org.scala-js" %%% "scalajs-java-time" % "0.2.5" 13 | ) 14 | 15 | resolvers ++= Seq( 16 | "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases") 17 | 18 | scalacOptions ++= Seq( 19 | "-deprecation", 20 | "-unchecked", 21 | "-Xlint", 22 | "-Ywarn-infer-any", 23 | "-Ywarn-dead-code", 24 | "-Ywarn-unused", 25 | "-Ywarn-unused-import", 26 | "-Ywarn-value-discard") 27 | 28 | emitSourceMaps := false 29 | scalaJSOutputWrapper := ("", "scalachessjs.Main().main();") 30 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fileName=scalachessjs-opt.js 4 | 5 | rm -f target/scala-*/$fileName 6 | 7 | sbt fullOptJS || exit $? 8 | 9 | cp target/scala-*/$fileName build/ 10 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /norhino.sbt: -------------------------------------------------------------------------------- 1 | scalaJSUseRhino in Global := false 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.18 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.26") 2 | -------------------------------------------------------------------------------- /src/main/scala/scalachess: -------------------------------------------------------------------------------- 1 | ../../../submodules/scalachess/src/main/scala -------------------------------------------------------------------------------- /src/main/scala/scalachessjs/Main.scala: -------------------------------------------------------------------------------- 1 | package scalachessjs 2 | 3 | import scala.scalajs.js.JSApp 4 | import scala.scalajs.js 5 | import org.scalajs.dom 6 | import js.Dynamic.{ /* global => g, newInstance => jsnew, */ literal => jsobj } 7 | import js.JSConverters._ 8 | import js.annotation._ 9 | 10 | import chess.{ Success, Failure, Game, Pos, Role, PromotableRole, Replay, Status, MoveOrDrop } 11 | import chess.variant.Variant 12 | import chess.format.{ UciCharPair, UciDump } 13 | import chess.format.pgn.Reader 14 | 15 | object Main extends JSApp { 16 | def main(): Unit = { 17 | 18 | val self = js.Dynamic.global 19 | 20 | self.addEventListener("message", { e: dom.MessageEvent => 21 | 22 | try { 23 | val data = e.data.asInstanceOf[Message] 24 | val reqidOpt = data.reqid.asInstanceOf[js.UndefOr[String]].toOption 25 | val payload = data.payload.asInstanceOf[js.Dynamic] 26 | val fen = payload.fen.asInstanceOf[js.UndefOr[String]].toOption 27 | val variantKey = payload.variant.asInstanceOf[js.UndefOr[String]].toOption 28 | val variant = variantKey.flatMap(Variant(_)) 29 | 30 | data.topic match { 31 | 32 | case "init" => { 33 | init(reqidOpt, variant, fen) 34 | } 35 | case "dests" => { 36 | val path = payload.path.asInstanceOf[js.UndefOr[String]].toOption 37 | fen.fold { 38 | sendError(reqidOpt, data.topic, "fen field is required for dests topic") 39 | } { fen => 40 | getDests(reqidOpt, variant, fen, path) 41 | } 42 | } 43 | case "situation" => { 44 | val path = payload.path.asInstanceOf[js.UndefOr[String]].toOption 45 | fen.fold { 46 | sendError(reqidOpt, data.topic, "fen field is required for situation topic") 47 | } { fen => 48 | val game = Game(variant, Some(fen)) 49 | self.postMessage(Message( 50 | reqid = reqidOpt, 51 | topic = "situation", 52 | payload = jsobj( 53 | "situation" -> gameSituation(game), 54 | "path" -> path.orUndefined 55 | ) 56 | )) 57 | 58 | () 59 | } 60 | } 61 | case "threefoldTest" => { 62 | val pgnMoves = payload.pgnMoves.asInstanceOf[js.Array[String]].toList 63 | val initialFen = payload.initialFen.asInstanceOf[js.UndefOr[String]].toOption 64 | Replay(pgnMoves, initialFen, variant getOrElse Variant.default) match { 65 | case Success(Reader.Result.Complete(replay)) => { 66 | self.postMessage(Message( 67 | reqid = reqidOpt, 68 | topic = "threefoldTest", 69 | payload = jsobj( 70 | "threefoldRepetition" -> replay.state.board.history.threefoldRepetition, 71 | "status" -> jsobj( 72 | "id" -> Status.Draw.id, 73 | "name" -> Status.Draw.name 74 | ) 75 | ) 76 | )) 77 | } 78 | case Success(Reader.Result.Incomplete(_, errors)) => sendError(reqidOpt, data.topic, errors.head) 79 | case Failure(errors) => sendError(reqidOpt, data.topic, errors.head) 80 | } 81 | } 82 | case "move" => { 83 | val promotion = payload.promotion.asInstanceOf[js.UndefOr[String]].toOption 84 | val origS = payload.orig.asInstanceOf[String] 85 | val destS = payload.dest.asInstanceOf[String] 86 | val pgnMovesOpt = payload.pgnMoves.asInstanceOf[js.UndefOr[js.Array[String]]].toOption 87 | val uciMovesOpt = payload.uciMoves.asInstanceOf[js.UndefOr[js.Array[String]]].toOption 88 | val pgnMoves = pgnMovesOpt.map(_.toVector).getOrElse(Vector.empty[String]) 89 | val uciMoves = uciMovesOpt.map(_.toList).getOrElse(List.empty[String]) 90 | val path = payload.path.asInstanceOf[js.UndefOr[String]].toOption 91 | (for { 92 | orig <- Pos.posAt(origS) 93 | dest <- Pos.posAt(destS) 94 | fen <- fen 95 | } yield (orig, dest, fen)) match { 96 | case Some((orig, dest, fen)) => 97 | move(reqidOpt, variant, fen, pgnMoves, uciMoves, orig, dest, Role.promotable(promotion), path) 98 | case None => 99 | sendError(reqidOpt, data.topic, s"step topic params: $origS, $destS, $fen are not valid") 100 | } 101 | } 102 | case "drop" => { 103 | val roleS = payload.role.asInstanceOf[String] 104 | val posS = payload.pos.asInstanceOf[String] 105 | val pgnMovesOpt = payload.pgnMoves.asInstanceOf[js.UndefOr[js.Array[String]]].toOption 106 | val uciMovesOpt = payload.uciMoves.asInstanceOf[js.UndefOr[js.Array[String]]].toOption 107 | val pgnMoves = pgnMovesOpt.map(_.toVector).getOrElse(Vector.empty[String]) 108 | val uciMoves = uciMovesOpt.map(_.toList).getOrElse(List.empty[String]) 109 | val path = payload.path.asInstanceOf[js.UndefOr[String]].toOption 110 | (for { 111 | pos <- Pos.posAt(posS) 112 | role <- Role.allByName get roleS 113 | fen <- fen 114 | } yield (pos, role, fen)) match { 115 | case Some((pos, role, fen)) => 116 | drop(reqidOpt, variant, fen, pgnMoves, uciMoves, role, pos, path) 117 | case None => 118 | sendError(reqidOpt, data.topic, s"step topic params: $posS, $roleS, $fen are not valid") 119 | } 120 | } 121 | case "pgnDump" => { 122 | val pgnMoves = payload.pgnMoves.asInstanceOf[js.Array[String]].toList 123 | val initialFen = payload.initialFen.asInstanceOf[js.UndefOr[String]].toOption 124 | val white = payload.white.asInstanceOf[js.UndefOr[String]].toOption 125 | val black = payload.black.asInstanceOf[js.UndefOr[String]].toOption 126 | val date = payload.date.asInstanceOf[js.UndefOr[String]].toOption 127 | Replay(pgnMoves, initialFen, variant getOrElse Variant.default) match { 128 | case Success(Reader.Result.Complete(replay)) => { 129 | val pgn = PgnDump(replay.state, initialFen, replay.setup.startedAtTurn, white, black, date) 130 | self.postMessage(Message( 131 | reqid = reqidOpt, 132 | topic = "pgnDump", 133 | payload = jsobj( 134 | "pgn" -> pgn.toString 135 | ) 136 | )) 137 | } 138 | case Success(Reader.Result.Incomplete(_, errors)) => sendError(reqidOpt, data.topic, errors.head) 139 | case Failure(errors) => sendError(reqidOpt, data.topic, errors.head) 140 | } 141 | } 142 | case _ => { 143 | sendError(reqidOpt, data.topic, "Invalid command.") 144 | } 145 | } 146 | } catch { 147 | case ex: Exception => { 148 | val data = e.data.asInstanceOf[Message] 149 | val reqidOpt = data.reqid.asInstanceOf[js.UndefOr[String]].toOption 150 | sendError(reqidOpt, data.topic, "Exception caught in scalachessjs: " + ex) 151 | } 152 | } 153 | }) 154 | 155 | def init(reqid: Option[String], variant: Option[Variant], fen: Option[String]): Unit = { 156 | val game = Game(variant, fen) 157 | self.postMessage(Message( 158 | reqid = reqid, 159 | topic = "init", 160 | payload = jsobj( 161 | "variant" -> new VariantInfo { 162 | val key = game.board.variant.key 163 | val name = game.board.variant.name 164 | val shortName = game.board.variant.shortName 165 | val title = game.board.variant.title 166 | }, 167 | "setup" -> gameSituation(game) 168 | ) 169 | )) 170 | 171 | () 172 | } 173 | 174 | def getDests(reqid: Option[String], variant: Option[Variant], fen: String, path: Option[String]): Unit = { 175 | val game = Game(variant, Some(fen)) 176 | val movable = !game.situation.end 177 | val dests = if (movable) possibleDests(game) else emptyDests 178 | self.postMessage(Message( 179 | reqid = reqid, 180 | topic = "dests", 181 | payload = jsobj( 182 | "dests" -> dests, 183 | "path" -> path.orUndefined 184 | ) 185 | )) 186 | 187 | () 188 | } 189 | 190 | def move(reqid: Option[String], variant: Option[Variant], fen: String, pgnMoves: Vector[String], uciMoves: List[String], orig: Pos, dest: Pos, promotion: Option[PromotableRole], path: Option[String]): Unit = { 191 | Game(variant, Some(fen))(orig, dest, promotion) match { 192 | case Success((newGame, move)) => { 193 | self.postMessage(Message( 194 | reqid = reqid, 195 | topic = "move", 196 | payload = jsobj( 197 | "situation" -> gameSituation(newGame.withPgnMoves(pgnMoves ++ newGame.pgnMoves), Some(Left(move)), uciMoves, promotion), 198 | "path" -> path.orUndefined 199 | ) 200 | )) 201 | 202 | () 203 | } 204 | case Failure(errors) => sendError(reqid, "move", errors.head) 205 | } 206 | } 207 | 208 | def drop(reqid: Option[String], variant: Option[Variant], fen: String, pgnMoves: Vector[String], uciMoves: List[String], role: Role, pos: Pos, path: Option[String]): Unit = { 209 | Game(variant, Some(fen)).drop(role, pos) match { 210 | case Success((newGame, drop)) => { 211 | self.postMessage(Message( 212 | reqid = reqid, 213 | topic = "drop", 214 | payload = jsobj( 215 | "situation" -> gameSituation(newGame.withPgnMoves(pgnMoves ++ newGame.pgnMoves), Some(Right(drop)), uciMoves), 216 | "path" -> path.orUndefined 217 | ) 218 | )) 219 | 220 | () 221 | } 222 | case Failure(errors) => sendError(reqid, "drop", errors.head) 223 | } 224 | } 225 | 226 | def sendError(reqid: Option[String], callerTopic: String, error: String): Unit = { 227 | self.postMessage(Message( 228 | reqid = reqid, 229 | topic = "error", 230 | payload = jsobj( 231 | "callerTopic" -> callerTopic, 232 | "error" -> error 233 | ) 234 | )) 235 | 236 | () 237 | } 238 | } 239 | 240 | private val emptyDests: js.Dictionary[js.Array[String]] = js.Dictionary() 241 | 242 | private def moveOrDropToUciCharPair(m: MoveOrDrop): UciCharPair = 243 | UciCharPair(m.fold(_.toUci, _.toUci)) 244 | 245 | private def gameSituation( 246 | game: Game, 247 | lastMoveOpt: Option[MoveOrDrop] = None, 248 | prevUciMoves: List[String] = List.empty[String], 249 | promotionRole: Option[PromotableRole] = None 250 | ): js.Object = { 251 | 252 | val lmUci = lastMoveOpt.map(UciDump.move(game.board.variant)(_)) 253 | 254 | val mergedUciMoves = lmUci.fold(prevUciMoves) { uci => 255 | prevUciMoves :+ uci 256 | } 257 | val movable = !game.situation.end 258 | 259 | new Situation { 260 | val id = lastMoveOpt.fold("")(moveOrDropToUciCharPair(_).toString) 261 | val variant = game.board.variant.key 262 | val fen = chess.format.Forsyth >> game 263 | val player = game.player.name 264 | val dests = if (movable) possibleDests(game) else emptyDests 265 | val drops = possibleDrops(game) 266 | val end = game.situation.end 267 | val playable = game.situation.playable(true) 268 | val winner = game.situation.winner.map(_.name).orUndefined 269 | val check = game.situation.check 270 | val checkCount = (if (game.board.variant.key == "threeCheck") Some(jsobj( 271 | "white" -> game.board.history.checkCount.white, 272 | "black" -> game.board.history.checkCount.black 273 | )) else None).orUndefined 274 | val uci = lmUci.orUndefined 275 | val san = game.pgnMoves.lastOption.orUndefined 276 | val pgnMoves = game.pgnMoves.toJSArray 277 | val uciMoves = mergedUciMoves.toJSArray 278 | val promotion = promotionRole.map(_.forsyth).map(_.toString).orUndefined 279 | val status = game.situation.status.map { s => 280 | jsobj( 281 | "id" -> s.id, 282 | "name" -> s.name 283 | ) 284 | }.orUndefined 285 | val crazyhouse = game.board.crazyData.map { d => 286 | jsobj( 287 | "pockets" -> js.Array( 288 | d.pockets.white.roles.map(_.name).groupBy(identity).mapValues(_.size).toJSDictionary, 289 | d.pockets.black.roles.map(_.name).groupBy(identity).mapValues(_.size).toJSDictionary 290 | ) 291 | ) 292 | }.orUndefined 293 | val ply = game.turns 294 | } 295 | } 296 | 297 | private def possibleDests(game: Game): js.Dictionary[js.Array[String]] = { 298 | game.situation.destinations.map { 299 | case (pos, dests) => (pos.toString -> dests.map(_.toString).toJSArray) 300 | }.toJSDictionary 301 | } 302 | 303 | private def possibleDrops(game: Game): js.UndefOr[js.Array[String]] = { 304 | game.situation.drops.map { drops => 305 | drops.map(_.toString).toJSArray 306 | }.orUndefined 307 | } 308 | } 309 | 310 | @js.native 311 | trait Message extends js.Object { 312 | val topic: String 313 | val payload: js.Any 314 | val reqid: js.UndefOr[String] 315 | } 316 | 317 | object Message { 318 | def apply(topic: String, payload: js.Any, reqid: Option[String]): Message = 319 | js.Dynamic.literal(topic = topic, payload = payload, reqid = reqid.orUndefined).asInstanceOf[Message] 320 | } 321 | 322 | @ScalaJSDefined 323 | trait VariantInfo extends js.Object { 324 | val key: String 325 | val name: String 326 | val shortName: String 327 | val title: String 328 | } 329 | 330 | @ScalaJSDefined 331 | trait Situation extends js.Object { 332 | val id: String 333 | val ply: Int 334 | val variant: String 335 | val fen: String 336 | val player: String 337 | val dests: js.Dictionary[js.Array[String]] 338 | val drops: js.UndefOr[js.Array[String]] 339 | val end: Boolean 340 | val playable: Boolean 341 | val status: js.UndefOr[js.Object] 342 | val winner: js.UndefOr[String] 343 | val check: Boolean 344 | val checkCount: js.UndefOr[js.Object] 345 | val pgnMoves: js.Array[String] 346 | val uciMoves: js.Array[String] 347 | val san: js.UndefOr[String] 348 | val uci: js.UndefOr[String] 349 | val promotion: js.UndefOr[String] 350 | val crazyhouse: js.UndefOr[js.Object] 351 | } 352 | -------------------------------------------------------------------------------- /src/main/scala/scalachessjs/PgnDump.scala: -------------------------------------------------------------------------------- 1 | package scalachessjs 2 | 3 | import chess.format.pgn.{ Pgn, Tag, Tags } 4 | import chess.format.{ pgn => chessPgn } 5 | import chess.Game 6 | 7 | import scala.scalajs.js 8 | import js.Dynamic.{ global => g, newInstance => jsnew } 9 | 10 | object PgnDump { 11 | 12 | def apply( 13 | game: Game, 14 | initialFen: Option[String], 15 | startedAtTurn: Int, 16 | white: Option[String] = None, 17 | black: Option[String] = None, 18 | date: Option[String] = None): Pgn = { 19 | val ts = tags(game, initialFen, white, black, date) 20 | Pgn(ts, turns(game.pgnMoves, startedAtTurn)) 21 | } 22 | 23 | private def tags( 24 | game: Game, 25 | initialFen: Option[String], 26 | white: Option[String] = None, 27 | black: Option[String] = None, 28 | date: Option[String] = None): Tags = { 29 | val d = jsnew(g.Date)() 30 | Tags(List( 31 | Tag(_.Event, "Casual Game"), 32 | Tag(_.Site, "https://lichess.org"), 33 | Tag(_.Date, date getOrElse d.toLocaleString()), 34 | Tag(_.White, white getOrElse "Anonymous"), 35 | Tag(_.Black, black getOrElse "Anonymous"), 36 | Tag(_.Result, result(game)), 37 | Tag("PlyCount", game.turns), 38 | Tag(_.FEN, initialFen getOrElse "?"), 39 | Tag(_.Variant, game.board.variant.name.capitalize), 40 | Tag(_.Termination, game.situation.status.fold("?")(s => s.name)) 41 | )) 42 | } 43 | 44 | private def turns(moves: Vector[String], from: Int): List[chessPgn.Turn] = { 45 | val paddedMoves = if (from % 2 == 1) ".." +: moves else moves 46 | (paddedMoves grouped 2).zipWithIndex.toList map { 47 | case (moves, index) => chessPgn.Turn( 48 | number = index + 1 + (from / 2), 49 | white = moves.headOption.filter(".." !=).map(s => chessPgn.Move(s)), 50 | black = moves.lift(1).map(s => chessPgn.Move(s))) 51 | } filterNot (_.isEmpty) 52 | } 53 | 54 | private def result(game: Game) = game.situation.status.fold("*") { _ => 55 | game.situation.winner.fold("1/2-1/2")(c => c.fold("1-0", "0-1")) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |