From 775cf9761e4dc5811a256e70a4ceb8935ec6278d Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 29 Nov 2016 14:15:46 +0100 Subject: [PATCH] puzzle solution --- app/controllers/Export.scala | 2 +- modules/puzzle/src/main/Daily.scala | 2 +- modules/puzzle/src/main/Export.scala | 2 +- modules/puzzle/src/main/JsonView.scala | 28 +++++++++++++++++++++- modules/puzzle/src/main/Line.scala | 5 +++- modules/puzzle/src/main/PngExport.scala | 18 -------------- modules/puzzle/src/main/Puzzle.scala | 6 ++--- public/stylesheets/puzzle.css | 17 +++++++++++++- ui/puzzle/src/ctrl.js | 18 +++++++++++++- ui/puzzle/src/feedbackView.js | 31 ++++++++++++++++++------- ui/puzzle/src/ground.js | 2 +- ui/puzzle/src/solution.js | 10 ++++++++ 12 files changed, 104 insertions(+), 37 deletions(-) delete mode 100644 modules/puzzle/src/main/PngExport.scala create mode 100644 ui/puzzle/src/solution.js diff --git a/app/controllers/Export.scala b/app/controllers/Export.scala index 76a9820de3..0d1115dc04 100644 --- a/app/controllers/Export.scala +++ b/app/controllers/Export.scala @@ -89,7 +89,7 @@ object Export extends LilaController { OptionFuResult(Env.puzzle.api.puzzle find id) { puzzle => env.pngExport( fen = chess.format.FEN(puzzle.fenAfterInitialMove | puzzle.fen), - lastMove = puzzle.initialMove.some, + lastMove = puzzle.initialMove.uci.some, check = none, orientation = puzzle.color.some, logHint = s"puzzle $id" diff --git a/modules/puzzle/src/main/Daily.scala b/modules/puzzle/src/main/Daily.scala index 55dd548101..7a6e78cc98 100644 --- a/modules/puzzle/src/main/Daily.scala +++ b/modules/puzzle/src/main/Daily.scala @@ -35,7 +35,7 @@ private[puzzle] final class Daily( private def makeDaily(puzzle: Puzzle): Fu[Option[DailyPuzzle]] = { import makeTimeout.short ~puzzle.fenAfterInitialMove.map { fen => - renderer ? RenderDaily(puzzle, fen, puzzle.initialMove) map { + renderer ? RenderDaily(puzzle, fen, puzzle.initialMove.uci) map { case html: play.twirl.api.Html => DailyPuzzle(html, puzzle.color, puzzle.id).some } } diff --git a/modules/puzzle/src/main/Export.scala b/modules/puzzle/src/main/Export.scala index fdc35f380c..1360d8aece 100644 --- a/modules/puzzle/src/main/Export.scala +++ b/modules/puzzle/src/main/Export.scala @@ -16,7 +16,7 @@ object Export { "id" -> puzzle.id, "fen" -> puzzle.fen, "color" -> puzzle.color.name, - "move" -> puzzle.initialMove, + "move" -> puzzle.initialMove.uci, "lines" -> lila.puzzle.Line.toJson(puzzle.lines)) }) s""""$encoded"""" -> puzzle.vote.sum diff --git a/modules/puzzle/src/main/JsonView.scala b/modules/puzzle/src/main/JsonView.scala index 9decf41dd3..4f7b74e4d2 100644 --- a/modules/puzzle/src/main/JsonView.scala +++ b/modules/puzzle/src/main/JsonView.scala @@ -5,6 +5,7 @@ import play.api.libs.json._ import lila.common.PimpedJson._ import lila.game.Game import lila.puzzle._ +import lila.tree object JsonView { @@ -27,10 +28,11 @@ object JsonView { "attempts" -> puzzle.attempts, "fen" -> puzzle.fen, "color" -> puzzle.color.name, - "initialMove" -> puzzle.initialMove, + "initialMove" -> puzzle.initialMove.uci, "initialPly" -> puzzle.initialPly, "gameId" -> puzzle.gameId, "lines" -> lila.puzzle.Line.toJson(puzzle.lines), + "branch" -> makeBranch(puzzle), "enabled" -> puzzle.enabled, "vote" -> puzzle.vote.sum ), @@ -83,4 +85,28 @@ object JsonView { ) }).noNull } + + private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = { + import chess.format._ + val solution: List[Uci.Move] = Line solution puzzle.lines map { uci => + Uci.Move(uci) err s"Invalid puzzle solution UCI $uci" + } + val init = chess.Game(none, puzzle.fenAfterInitialMove).withTurns(puzzle.initialPly) + val (_, branchList) = solution.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) { + case ((prev, branches), uci) => + val (game, _) = prev(uci.orig, uci.dest, uci.promotion).prefixFailuresWith(s"puzzle ${puzzle.id}").err + val branch = tree.Branch( + id = UciCharPair(uci), + ply = game.turns, + move = Uci.WithSan(uci, game.pgnMoves.last), + fen = chess.format.Forsyth >> game, + check = game.situation.check, + crazyData = none) + (game, branch :: branches) + } + branchList.foldLeft[Option[tree.Branch]](None) { + case (None, branch) => branch.some + case (Some(child), branch) => Some(branch addChild child) + } + } } diff --git a/modules/puzzle/src/main/Line.scala b/modules/puzzle/src/main/Line.scala index 61a45874be..1146c7240d 100644 --- a/modules/puzzle/src/main/Line.scala +++ b/modules/puzzle/src/main/Line.scala @@ -46,9 +46,12 @@ object Line { } } - loop(lines collect { + lines.collectFirst { + case Win(move) => List(move) + } | loop(lines collect { case Node(move, _) => List(move) }) + } def toString(lines: Lines, level: Int = 0): String = { diff --git a/modules/puzzle/src/main/PngExport.scala b/modules/puzzle/src/main/PngExport.scala deleted file mode 100644 index 1933fbf1a1..0000000000 --- a/modules/puzzle/src/main/PngExport.scala +++ /dev/null @@ -1,18 +0,0 @@ -package lila.puzzle - -import chess.format.Forsyth -import java.io.{ File, OutputStream } -import scala.sys.process._ - -object PngExport { - - private val logger = ProcessLogger(_ => (), _ => ()) - - def apply(execPath: String)(puzzle: Puzzle)(out: OutputStream) { - val color = puzzle.color.letter.toString - val lastMove = puzzle.initialMove - val fen = (puzzle.fenAfterInitialMove | puzzle.fen).takeWhile(' ' !=) - val exec = Process(Seq("php", "board-creator.php", fen, color, lastMove), new File(execPath)) - exec #> out ! logger - } -} diff --git a/modules/puzzle/src/main/Puzzle.scala b/modules/puzzle/src/main/Puzzle.scala index 5cd3dab007..000c16dc59 100644 --- a/modules/puzzle/src/main/Puzzle.scala +++ b/modules/puzzle/src/main/Puzzle.scala @@ -1,6 +1,7 @@ package lila.puzzle import chess.Color +import chess.format.Uci import org.joda.time.DateTime import lila.rating.Perf @@ -23,7 +24,7 @@ case class Puzzle( def withVote(f: AggregateVote => AggregateVote) = copy(vote = f(vote)) - def initialMove = history.last + def initialMove: Uci.Move = history.lastOption flatMap Uci.Move.apply err s"Bad initial move $this" def enabled = vote.sum > -9000 @@ -31,8 +32,7 @@ case class Puzzle( import chess.format.{ Uci, Forsyth } for { sit1 <- Forsyth << fen - uci <- Uci.Move(initialMove) - sit2 <- sit1.move(uci.orig, uci.dest, uci.promotion).toOption map (_.situationAfter) + sit2 <- sit1.move(initialMove).toOption.map(_.situationAfter) } yield Forsyth >> sit2 } } diff --git a/public/stylesheets/puzzle.css b/public/stylesheets/puzzle.css index 2931fce32b..38532442ed 100644 --- a/public/stylesheets/puzzle.css +++ b/public/stylesheets/puzzle.css @@ -8,7 +8,8 @@ background: #fff; height: 38%; display: flex; - align-items: center; + flex-flow: column; + justify-content: center; } #puzzle .feedback .player { display: flex; @@ -34,6 +35,20 @@ #puzzle .feedback .instruction strong { font-size: 1.5em; } +@keyframes reveal { + 0% { opacity: 0; } + 100% { opacity: 0.8; } +} +#puzzle .feedback .view_solution { + margin-top: 20px; + text-align: center; + opacity: 0.8; + animation: reveal 1s; + transition: opacity 0.13s; +} +#puzzle .feedback .view_solution:hover { + opacity: 1; +} #puzzle .timeline { display: flex; diff --git a/ui/puzzle/src/ctrl.js b/ui/puzzle/src/ctrl.js index 1f0e9b9832..0987b8055e 100644 --- a/ui/puzzle/src/ctrl.js +++ b/ui/puzzle/src/ctrl.js @@ -11,6 +11,7 @@ var opposite = chessground.util.opposite; var groundBuild = require('./ground'); var socketBuild = require('./socket'); var moveTestBuild = require('./moveTest'); +var mergeSolution = require('./solution'); var throttle = require('common').throttle; var xhr = require('./xhr'); var sound = require('./sound'); @@ -18,9 +19,11 @@ var sound = require('./sound'); module.exports = function(opts, i18n) { var vm = { + mode: 'play', // play | try | view loading: false, justPlayed: null, - initialPath: null + initialPath: null, + canViewSolution: false }; var data = opts.data; @@ -42,6 +45,12 @@ module.exports = function(opts, i18n) { m.redraw(); }, 500); + setTimeout(function() { + vm.canViewSolution = true; + m.redraw(); + }, 500); + // }, 5000); + var showGround = function() { var node = vm.node; var color = node.ply % 2 === 0 ? 'white' : 'black'; @@ -188,6 +197,12 @@ module.exports = function(opts, i18n) { jump(path); }; + var viewSolution = function() { + vm.mode = 'view'; + mergeSolution(tree, vm.initialPath, data.puzzle.branch); + m.redraw(); + }; + var socket = socketBuild({ send: opts.socketSend, addNode: addNode, @@ -215,6 +230,7 @@ module.exports = function(opts, i18n) { tree: tree, ground: ground, userJump: userJump, + viewSolution: viewSolution, trans: lichess.trans(opts.i18n), socketReceive: socket.receive }; diff --git a/ui/puzzle/src/feedbackView.js b/ui/puzzle/src/feedbackView.js index 63ae99cf23..d0064ef790 100644 --- a/ui/puzzle/src/feedbackView.js +++ b/ui/puzzle/src/feedbackView.js @@ -1,18 +1,33 @@ var m = require('mithril'); +var bindOnce = require('common').bindOnce; -function yourTurn(ctrl) { +function play(ctrl) { var turnColor = ctrl.ground.data.turnColor; var puzzleColor = ctrl.data.puzzle.color; - return m('div.your_turn.player.' + turnColor, [ - m('div.no-square', m('piece.king.' + turnColor)), - m('div.instruction', [ - m('strong', ctrl.trans.noarg(turnColor == puzzleColor ? 'yourTurn' : 'waiting')), - m('em', ctrl.trans.noarg(puzzleColor == 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack')) - ]) + return m('div.feedback.play', [ + m('div.player.' + turnColor, [ + m('div.no-square', m('piece.king.' + turnColor)), + m('div.instruction', [ + m('strong', ctrl.trans.noarg(turnColor == puzzleColor ? 'yourTurn' : 'waiting')), + m('em', ctrl.trans.noarg(puzzleColor == 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack')) + ]) + ]), + ctrl.vm.canViewSolution ? m('div.view_solution', + m('a.button', { + config: bindOnce('click', ctrl.viewSolution) + }, 'View the solution') + ) : null + ]); +} + +function view(ctrl) { + return m('div.feedback.view', [ + 'view' ]); } module.exports = function(ctrl) { - return m('div.feedback', yourTurn(ctrl)); + if (ctrl.vm.mode === 'play') return play(ctrl); + if (ctrl.vm.mode === 'view') return view(ctrl); }; diff --git a/ui/puzzle/src/ground.js b/ui/puzzle/src/ground.js index fa6c5bd1f2..2315cf512c 100644 --- a/ui/puzzle/src/ground.js +++ b/ui/puzzle/src/ground.js @@ -5,7 +5,7 @@ function makeConfig(data, config, onMove) { fen: config.fen, check: config.check, lastMove: config.lastMove, - orientation: data.orientation, + orientation: data.puzzle.color, coordinates: data.pref.coords !== 0, movable: { free: false, diff --git a/ui/puzzle/src/solution.js b/ui/puzzle/src/solution.js new file mode 100644 index 0000000000..f08319fe99 --- /dev/null +++ b/ui/puzzle/src/solution.js @@ -0,0 +1,10 @@ +var treeOps = require('tree').ops; + +module.exports = function(tree, initialPath, solution) { + + var initialNode = tree.nodeAtPath(initialPath); + var solutionNode = treeOps.childById(initialNode, solution.id); + + if (solutionNode) treeOps.merge(solutionNode, solution); + else initialNode.children.push(solution); +};