puzzle WIP

pull/7680/head
Thibault Duplessis 2020-11-11 23:04:31 +01:00
parent 44cdccb302
commit 778621b457
7 changed files with 69 additions and 67 deletions

View File

@ -58,18 +58,18 @@ final class Puzzle(
}
}
def show(id: Puz.Id) =
def show(id: String) =
Open { implicit ctx =>
NoBot {
OptionFuResult(env.puzzle.api.puzzle find id)(renderShow)
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id))(renderShow)
}
}
def load(id: Puz.Id) =
def load(id: String) =
Open { implicit ctx =>
NoBot {
XhrOnly {
OptionFuOk(env.puzzle.api.puzzle find id)(puzzleJson) map (_ as JSON)
OptionFuOk(env.puzzle.api.puzzle find Puz.Id(id))(puzzleJson _).dmap(_ as JSON)
}
}
}
@ -90,8 +90,7 @@ final class Puzzle(
}
}
// new API
def round2(id: Puz.Id) =
def round2(id: String) =
OpenBody { implicit ctx =>
NoBot {
???
@ -133,7 +132,7 @@ final class Puzzle(
}
}
def vote(id: Puz.Id) =
def vote(id: String) =
AuthBody { implicit ctx => me =>
NoBot {
???

View File

@ -87,11 +87,7 @@ POST /training/batch controllers.Puzzle.batchSolve
GET /training/:id controllers.Puzzle.show(id: String)
GET /training/:id/load controllers.Puzzle.load(id: String)
POST /training/:id/vote controllers.Puzzle.vote(id: String)
POST /training/:id/round controllers.Puzzle.round(id: String)
# new UI
POST /training/:id/round2 controllers.Puzzle.round2(id: Int)
# mobile app BC
POST /training/:id/attempt controllers.Puzzle.round(id: Int)
POST /training/:id/round2 controllers.Puzzle.round2(id: String)
# User Analysis
GET /analysis/help controllers.UserAnalysis.help

View File

@ -59,13 +59,33 @@ final class JsonView(
"id" -> puzzle.id,
"rating" -> puzzle.glicko.intRating,
"plays" -> puzzle.plays,
"fen" -> puzzle.fen,
"color" -> puzzle.color.name,
"initialPly" -> puzzle.initialPly,
"gameId" -> puzzle.gameId,
"line" -> puzzle.line.toList.map(_.uci).mkString(" "),
"solution" -> makeSolution(puzzle).map(defaultNodeJsonWriter.writes),
"vote" -> puzzle.vote
)
private def makeSolution(puzzle: Puzzle): Option[tree.Branch] = {
import chess.format._
val init = chess.Game(none, puzzle.fenAfterInitialMove.some).withTurns(puzzle.initialPly)
val (_, branchList) = puzzle.line.tail.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) {
case ((prev, branches), uci) =>
val (game, move) =
prev(uci.orig, uci.dest, uci.promotion).fold(err => sys error s"puzzle ${puzzle.id} $err", identity)
val branch = tree.Branch(
id = UciCharPair(move.toUci),
ply = game.turns,
move = Uci.WithSan(move.toUci, 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)
}
}
}
object JsonView {

View File

@ -21,7 +21,7 @@
"@fnando/sparkline": "^0.3.10",
"ceval": "2.0.0",
"chessground": "^7.9.2",
"chessops": "^0.7.2",
"chessops": "^0.7.3",
"common": "2.0.0",
"snabbdom": "^0.7.4",
"tree": "2.0.0"

View File

@ -1,24 +1,24 @@
import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree';
import { ctrl as cevalCtrl, CevalCtrl } from 'ceval';
import keyboard from './keyboard';
import { moveTestBuild, MoveTestFn } from './moveTest';
import mergeSolution from './solution';
import makePromotion from './promotion';
import computeAutoShapes from './autoShape';
import { defined, prop, Prop } from 'common';
import { storedProp } from 'common/storage';
import throttle from 'common/throttle';
import * as xhr from './xhr';
import * as speech from './speech';
import { Role, Move, Outcome } from 'chessops/types';
import { parseSquare, parseUci, makeSquare, makeUci } from 'chessops/util';
import { parseFen, makeFen } from 'chessops/fen';
import { makeSanAndPlay } from 'chessops/san';
import * as xhr from './xhr';
import computeAutoShapes from './autoShape';
import keyboard from './keyboard';
import makePromotion from './promotion';
import mergeSolution from './solution';
import throttle from 'common/throttle';
import { Api as CgApi } from 'chessground/api';
import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree';
import { Chess } from 'chessops/chess';
import { chessgroundDests, scalachessCharPair } from 'chessops/compat';
import { Config as CgConfig } from 'chessground/config';
import { Api as CgApi } from 'chessground/api';
import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleRound, PuzzleVote, MoveTest } from './interfaces';
import { ctrl as cevalCtrl, CevalCtrl } from 'ceval';
import { defined, prop, Prop } from 'common';
import { makeSanAndPlay } from 'chessops/san';
import { moveTestBuild, MoveTestFn } from './moveTest';
import { parseFen, makeFen } from 'chessops/fen';
import { parseSquare, parseUci, makeSquare, makeUci } from 'chessops/util';
import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleRound, MoveTest } from './interfaces';
import { Role, Move, Outcome } from 'chessops/types';
import { storedProp } from 'common/storage';
export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
@ -53,7 +53,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
function initiate(fromData: PuzzleData): void {
data = fromData;
tree = treeBuild(treeOps.reconstruct(data.game.treeParts));
var initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root));
const initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root));
// play | try | view
vm.mode = 'play';
vm.loading = false;
@ -64,6 +64,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
vm.lastFeedback = 'init';
vm.initialPath = initialPath;
vm.initialNode = tree.nodeAtPath(initialPath);
vm.pov = vm.initialNode.ply % 2 == 0 ? 'black' : 'white';
setPath(treePath.init(initialPath));
setTimeout(function() {
@ -100,16 +101,16 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
const node = vm.node;
const color: Color = node.ply % 2 === 0 ? 'white' : 'black';
const dests = chessgroundDests(position());
const movable = (vm.mode === 'view' || color === data.puzzle.color) ? {
const movable = (vm.mode === 'view' || color === vm.pov) ? {
color: dests.size > 0 ? color : undefined,
dests
} : {
color: undefined,
dests: new Map(),
};
color: undefined,
dests: new Map(),
};
const config = {
fen: node.fen,
orientation: data.puzzle.color,
orientation: vm.pov,
turnColor: color,
movable: movable,
premovable: {
@ -119,8 +120,8 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
lastMove: uciToLastMove(node.uci)
};
if (node.ply >= vm.initialNode.ply) {
if (vm.mode !== 'view' && color !== data.puzzle.color) {
config.movable.color = data.puzzle.color;
if (vm.mode !== 'view' && color !== vm.pov) {
config.movable.color = vm.pov;
config.premovable.enabled = true;
}
}
@ -216,9 +217,6 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
vm.mode = 'try';
sendResult(false);
}
} else if (progress === 'retry') {
vm.lastFeedback = 'retry';
revertUserMove();
} else if (progress === 'win') {
if (vm.mode !== 'view') {
if (vm.mode === 'play') sendResult(true);
@ -382,7 +380,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
if (!vm.canViewSolution) return;
sendResult(false);
vm.mode = 'view';
mergeSolution(vm.initialNode, data.puzzle.branch, data.puzzle.color);
mergeSolution(vm.initialNode, data.puzzle.solution, vm.pov);
reorderChildren(vm.initialPath, true);
// try and play the solution next move
@ -415,10 +413,8 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
if (callToVote()) thanksUntil = Date.now() + 2000;
nbToVoteCall(5);
vm.voted = v;
xhr.vote(data.puzzle.id, v).then((res: PuzzleVote) => {
data.puzzle.vote = res[1];
redraw();
});
xhr.vote(data.puzzle.id, v);
redraw();
});
initiate(opts.data);

View File

@ -59,13 +59,14 @@ export interface Vm {
nodeList: Tree.Node[];
node: Tree.Node;
mainline: Tree.Node[];
pov: Color;
mode: 'play' | 'view' | 'try';
loading: boolean;
round: any;
voted?: boolean | null;
justPlayed?: Key;
resultSent: boolean;
lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry';
lastFeedback: 'init' | 'fail' | 'win' | 'good';
initialPath: Tree.Path;
initialNode: Tree.Node;
canViewSolution: boolean;
@ -112,7 +113,6 @@ export interface PuzzleGame {
rated: boolean;
players: Array<{userId: string, name: string, color: Color}>;
treeParts: Tree.Node[];
clock: string;
}
export interface PuzzleUser {
@ -121,14 +121,10 @@ export interface PuzzleUser {
}
export interface Puzzle {
id: number;
enabled: boolean;
vote: number;
color: Color;
lines: Lines;
branch: any;
id: string;
solution: Tree.Node;
rating: number;
attempts: number;
plays: number;
initialPly: number;
}
@ -141,11 +137,6 @@ export interface PuzzleRound {
voted?: null | true | false;
}
export interface PuzzleVote {
0: true | false; // up/down
1: number; // new score
}
export interface Promotion {
start(orig: Key, dest: Key, callback: (orig: Key, dest: Key, prom: Role) => void): boolean;
cancel(): void;

View File

@ -1,7 +1,7 @@
import { PuzzleRound, PuzzleVote, PuzzleData } from './interfaces';
import { PuzzleRound, PuzzleData } from './interfaces';
import * as xhr from 'common/xhr';
export function round(puzzleId: number, win: boolean): Promise<PuzzleRound> {
export function round(puzzleId: string, win: boolean): Promise<PuzzleRound> {
return xhr.json(`/training/${puzzleId}/round2`, {
method: 'POST',
body: xhr.form({ win: win ? 1 : 0 }),
@ -9,7 +9,7 @@ export function round(puzzleId: number, win: boolean): Promise<PuzzleRound> {
});
}
export function vote(puzzleId: number, v: boolean): Promise<PuzzleVote> {
export function vote(puzzleId: string, v: boolean): Promise<void> {
return xhr.json(`/training/${puzzleId}/vote`, {
method: 'POST',
body: xhr.form({ vote: v ? 1 : 0 })