complete (?) implementation of study forced variations. whew.

studyForceVariation
Thibault Duplessis 2018-09-28 20:55:17 +02:00
parent 583d260e5e
commit f0e31a48cf
16 changed files with 141 additions and 48 deletions

View File

@ -57,6 +57,9 @@ case class Chapter(
def setClock(clock: Option[Centis], path: Path): Option[Chapter] =
updateRoot(_.setClockAt(clock, path))
def forceVariation(force: Boolean, path: Path): Option[Chapter] =
updateRoot(_.forceVariationAt(force, path))
def opening: Option[FullOpening] =
if (!Variant.openingSensibleVariants(setup.variant)) none
else FullOpeningDB searchInFens root.mainline.map(_.fen)

View File

@ -94,6 +94,9 @@ final class ChapterRepo(coll: Coll) {
def setClock(chapter: Chapter, path: Path, clock: Option[chess.Centis]): Funit =
setNodeValue(chapter, path, "l", clock)
def forceVariation(chapter: Chapter, path: Path, force: Boolean): Funit =
setNodeValue(chapter, path, "fv", force option true)
def setScore(chapter: Chapter, path: Path, score: Option[lila.tree.Eval.Score]): Funit =
setNodeValue(chapter, path, "e", score)

View File

@ -55,6 +55,7 @@ case class Node(
def addChild(child: Node) = copy(children = children addNode child)
def withClock(centis: Option[Centis]) = copy(clock = centis)
def withForceVariation(force: Boolean) = copy(forceVariation = force)
def isCommented = comments.value.nonEmpty
@ -113,6 +114,14 @@ object Node {
case (head, tail) => get(head) flatMap (_.children nodeAt tail)
}
def nodesOn(path: Path): List[(Node, Path)] = path.split ?? {
case (head, tail) => get(head) ?? { first =>
(first, Path(List(head))) :: first.children.nodesOn(tail).map {
case (n, p) => (n, p prepend head)
}
}
}
def addNodeAt(node: Node, path: Path): Option[Children] = path.split match {
case None => addNode(node).some
case Some((head, tail)) => updateChildren(head, _.addNodeAt(node, tail))
@ -256,6 +265,10 @@ object Node {
if (path.isEmpty) copy(clock = clock).some
else updateChildrenAt(path, _ withClock clock)
def forceVariationAt(force: Boolean, path: Path): Option[Root] =
if (path.isEmpty) copy(clock = clock).some
else updateChildrenAt(path, _ withForceVariation force)
private def updateChildrenAt(path: Path, f: Node => Node): Option[Root] =
withChildren(_.updateAt(path, f))

View File

@ -17,6 +17,14 @@ case class Path(ids: List[UciCharPair]) extends AnyVal {
def +(node: Node): Path = Path(ids :+ node.id)
def +(more: Path): Path = Path(ids ::: more.ids)
def prepend(id: UciCharPair) = Path(id :: ids)
def intersect(other: Path): Path = Path {
ids zip other.ids takeWhile {
case (a, b) => a == b
} map (_._1)
}
override def toString = ids.mkString
}

View File

@ -5,6 +5,8 @@ case class Position(chapter: Chapter, path: Path) {
def ref = Position.Ref(chapter.id, path)
def node: Option[RootOrNode] = chapter.root nodeAt path
override def toString = ref.toString
}
case object Position {

View File

@ -165,6 +165,12 @@ private final class Socket(
"w" -> who(uid)
), noMessadata)
case ForceVariation(pos, force, uid) => notifyVersion("forceVariation", Json.obj(
"p" -> pos,
"force" -> force,
"w" -> who(uid)
), noMessadata)
case SetConceal(pos, ply) => notifyVersion("conceal", Json.obj(
"p" -> pos,
"ply" -> ply.map(_.value)
@ -322,6 +328,7 @@ object Socket {
case class DeleteComment(position: Position.Ref, commentId: Comment.Id, uid: Uid)
case class SetGlyphs(position: Position.Ref, glyphs: Glyphs, uid: Uid)
case class SetClock(position: Position.Ref, clock: Option[Centis], uid: Uid)
case class ForceVariation(position: Position.Ref, force: Boolean, uid: Uid)
case class ReloadChapters(chapters: List[Chapter.Metadata])
case object ReloadAll
case class ChangeChapter(uid: Uid, position: Position.Ref)

View File

@ -104,6 +104,14 @@ final class SocketHandler(
} api.promote(userId, studyId, position.ref, toMainline, uid)
}
}
case ("forceVariation", o) => AnaRateLimit(uid, member) {
reading[AtPosition](o) { position =>
for {
force <- (o \ "d" \ "force").asOpt[Boolean]
userId <- member.userId
} api.forceVariation(userId, studyId, position.ref, force, uid)
}
}
case ("setRole", o) => AnaRateLimit(uid, member) {
reading[SetRole](o) { d =>
member.userId foreach { userId =>

View File

@ -269,7 +269,13 @@ final class StudyApi(
} match {
case Some(newChapter) =>
chapterRepo.update(newChapter) >>-
sendTo(study, Socket.Promote(position, toMainline, uid))
sendTo(study, Socket.Promote(position, toMainline, uid)) >>
newChapter.root.children.nodesOn {
newChapter.root.mainlinePath.intersect(position.path)
}.collect {
case (node, path) if node.forceVariation =>
doForceVariation(Study.WithChapter(study, newChapter), path, false, Uid(""))
}.sequenceFu.void
case None =>
fufail(s"Invalid promoteToMainline $studyId $position") >>-
reloadUidBecauseOf(study, uid, chapter.id)
@ -277,6 +283,23 @@ final class StudyApi(
}
}
def forceVariation(userId: User.ID, studyId: Study.Id, position: Position.Ref, force: Boolean, uid: Uid): Funit =
sequenceStudyWithChapter(studyId, position.chapterId) { sc =>
Contribute(userId, sc.study) {
doForceVariation(sc, position.path, force, uid)
}
}
private def doForceVariation(sc: Study.WithChapter, path: Path, force: Boolean, uid: Uid): Funit =
sc.chapter.forceVariation(force, path) match {
case Some(newChapter) =>
chapterRepo.forceVariation(newChapter, path, force) >>-
sendTo(sc.study, Socket.ForceVariation(Position(newChapter, path).ref, force, uid))
case None =>
fufail(s"Invalid forceVariation ${Position(sc.chapter, path)} $force") >>-
reloadUidBecauseOf(sc.study, uid, sc.chapter.id)
}
def setRole(byUserId: User.ID, studyId: Study.Id, userId: User.ID, roleStr: String) = sequenceStudy(studyId) { study =>
(study isOwner byUserId) ?? {
val role = StudyMember.Role.byId.getOrElse(roleStr, StudyMember.Role.Read)
@ -453,19 +476,6 @@ final class StudyApi(
}
}
def forceVariation(studyId: Study.Id, position: Position.Ref, force: Boolean, uid: Uid): Funit =
sequenceStudyWithChapter(studyId, position.chapterId) { sc =>
sc.chapter.forceVariation(force, position.path) match {
case Some(newChapter) =>
studyRepo.updateNow(sc.study)
chapterRepo.forceVariation(newChapter, position.path, force) >>-
sendTo(sc.study, Socket.SetClock(position, clock, uid))
case None =>
fufail(s"Invalid setClock $position $clock") >>-
reloadUidBecauseOf(sc.study, uid, position.chapterId)
}
}
def explorerGame(userId: User.ID, studyId: Study.Id, data: actorApi.ExplorerGame, uid: Uid) = sequenceStudyWithChapter(studyId, data.position.chapterId) {
case Study.WithChapter(study, chapter) => Contribute(userId, study) {
if (data.insert) explorerGameHandler.insert(userId, study, Position(chapter, data.position.path), data.gameId) flatMap {

View File

@ -36,7 +36,8 @@ object TreeBuilder {
crazyData = node.crazyData,
eval = node.score.map(_.eval),
children = toBranches(node.children),
opening = FullOpeningDB findByFen node.fen.value
opening = FullOpeningDB findByFen node.fen.value,
forceVariation = node.forceVariation
)
def makeRoot(root: Node.Root) =

View File

@ -217,6 +217,7 @@ declare namespace Tree {
glyphs?: Glyph[];
clock?: Clock;
parentClock?: Clock;
forceVariation: boolean;
shapes?: Shape[];
comp?: boolean;
san?: string;

View File

@ -520,6 +520,12 @@ export default class AnalyseCtrl {
if (this.study) this.study.promote(path, toMainline);
}
forceVariation(path: Tree.Path, force: boolean): void {
this.tree.forceVariationAt(path, force);
this.jump(path);
if (this.study) this.study.forceVariation(path, force);
}
reset(): void {
this.showGround();
this.redraw();

View File

@ -34,6 +34,7 @@ export interface StudyCtrl {
setPath(path: Tree.Path, node: Tree.Node, playedMyself: boolean): void;
deleteNode(path: Tree.Path): void;
promote(path: Tree.Path, toMainline: boolean): void;
forceVariation(path: Tree.Path, force: boolean): void;
setChapter(id: string, force?: boolean): void;
toggleSticky(): void;
toggleWrite(): void;

View File

@ -429,6 +429,14 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
ctrl.tree.setClockAt(d.c, position.path);
redraw();
},
forceVariation(d) {
const position = d.p,
who = d.w;
setMemberActive(who);
if (wrongChapter(d)) return;
ctrl.tree.forceVariationAt(position.path, d.force);
redraw();
},
conceal(d) {
if (wrongChapter(d)) return;
data.chapter.conceal = d.ply;
@ -506,6 +514,12 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
path
}));
},
forceVariation(path, force) {
makeChange("forceVariation", addChapterId({
force,
path
}));
},
setChapter(id, force) {
if (id === vm.chapterId && !force) return;
if (!vm.mode.sticky || !makeChange("setChapter", id)) {

View File

@ -36,39 +36,41 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes |
if (opts.isMainline) {
const isWhite = main.ply % 2 === 1,
commentTags = renderMainlineCommentsOf(ctx, main, conceal, true).filter(nonEmpty);
if (!cs[1] && empty(commentTags)) return ((isWhite ? [moveView.renderIndex(main.ply, false)] : []) as MaybeVNodes).concat(
if (!cs[1] && empty(commentTags) && !main.forceVariation) return ((isWhite ? [moveView.renderIndex(main.ply, false)] : []) as MaybeVNodes).concat(
renderMoveAndChildrenOf(ctx, main, {
parentPath: opts.parentPath,
isMainline: true,
conceal
}) || []
);
const mainChildren = renderChildrenOf(ctx, main, {
const mainChildren = main.forceVariation ? undefined : renderChildrenOf(ctx, main, {
parentPath: opts.parentPath + main.id,
isMainline: true,
conceal
});
const passOpts = {
parentPath: opts.parentPath,
isMainline: true,
isMainline: !main.forceVariation,
conceal
};
return (isWhite ? [moveView.renderIndex(main.ply, false)] : [] as MaybeVNodes).concat([
renderMoveOf(ctx, main, passOpts),
isWhite ? emptyMove(passOpts.conceal) : null,
h('interrupt', commentTags.concat(
renderLines(ctx, cs.slice(1), {
parentPath: opts.parentPath,
isMainline: true,
conceal,
noConceal: !conceal
})
))
] as MaybeVNodes).concat(
isWhite && mainChildren ? [
moveView.renderIndex(main.ply, false),
emptyMove(passOpts.conceal)
] : []).concat(mainChildren || []);
return (isWhite ? [moveView.renderIndex(main.ply, false)] : [] as MaybeVNodes).concat(
main.forceVariation ? [] : [
renderMoveOf(ctx, main, passOpts),
isWhite ? emptyMove(passOpts.conceal) : null
]).concat([
h('interrupt', commentTags.concat(
renderLines(ctx, main.forceVariation ? cs : cs.slice(1), {
parentPath: opts.parentPath,
isMainline: passOpts.isMainline,
conceal,
noConceal: !conceal
})
))
] as MaybeVNodes).concat(
isWhite && mainChildren ? [
moveView.renderIndex(main.ply, false),
emptyMove(passOpts.conceal)
] : []).concat(mainChildren || []);
}
if (!cs[1]) return renderMoveAndChildrenOf(ctx, main, opts);
return renderInlined(ctx, cs, opts) || [renderLines(ctx, cs, opts)];

View File

@ -18,8 +18,7 @@ interface Coords {
const elementId = 'analyse-cm';
function getPosition(e: MouseEvent): Coords {
let posx = 0;
let posy = 0;
let posx = 0, posy = 0;
if (e.pageX || e.pageY) {
posx = e.pageX;
posy = e.pageY;
@ -59,7 +58,8 @@ function action(icon: string, text: string, handler: () => void): VNode {
function view(opts: Opts, coords: Coords): VNode {
const ctrl = opts.root,
node = ctrl.tree.nodeAtPath(opts.path),
onMainline = ctrl.tree.pathIsMainline(opts.path);
onMainline = ctrl.tree.pathIsMainline(opts.path) && !ctrl.tree.pathIsForcedVariation(opts.path),
trans = ctrl.trans.noarg;
return h('div#' + elementId + '.visible', {
hook: {
insert: vnode => positionMenu(vnode.elm as HTMLElement, coords),
@ -67,12 +67,16 @@ function view(opts: Opts, coords: Coords): VNode {
}
}, [
h('p.title', nodeFullName(node)),
onMainline ? null : action('S', ctrl.trans.noarg('promoteVariation'), () => ctrl.promote(opts.path, false)),
onMainline ? null : action('E', ctrl.trans.noarg('makeMainLine'), () => ctrl.promote(opts.path, true)),
action('q', ctrl.trans.noarg('deleteFromHere'), () => ctrl.deleteNode(opts.path))
onMainline ? null : action('S', trans('promoteVariation'), () => ctrl.promote(opts.path, false)),
onMainline ? null : action('E', trans('makeMainLine'), () => ctrl.promote(opts.path, true)),
action('q', trans('deleteFromHere'), () => ctrl.deleteNode(opts.path))
].concat(
ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : []
));
).concat([
onMainline ?
action('F', 'Force variation', () => ctrl.forceVariation(opts.path, true)) :
null
]));
}
export default function(e: MouseEvent, opts: Opts): void {

View File

@ -21,10 +21,12 @@ export interface TreeWrapper {
setGlyphsAt(glyphs: Tree.Glyph[], path: Tree.Path): MaybeNode;
setClockAt(clock: Tree.Clock | undefined, path: Tree.Path): MaybeNode;
pathIsMainline(path: Tree.Path): boolean;
pathIsForcedVariation(path: Tree.Path): boolean;
lastMainlineNode(path: Tree.Path): Tree.Node;
pathExists(path: Tree.Path): boolean;
deleteNodeAt(path: Tree.Path): void;
promoteAt(path: Tree.Path, toMainline: boolean): void;
forceVariationAt(path: Tree.Path, force: boolean): MaybeNode;
getCurrentNodesAfterPly(nodeList: Tree.Node[], mainline: Tree.Node[], ply: number): Tree.Node[];
merge(tree: Tree.Node): void;
removeCeval(): void;
@ -93,6 +95,10 @@ export function build(root: Tree.Node): TreeWrapper {
return pathIsMainlineFrom(child, treePath.tail(path));
}
function pathIsForcedVariation(path: Tree.Path): boolean {
return !!getNodeList(path).find(n => n.forceVariation);
}
function lastMainlineNodeFrom(node: Tree.Node, path: Tree.Path): Tree.Node {
if (path === '') return node;
const pathId = treePath.head(path);
@ -192,12 +198,6 @@ export function build(root: Tree.Node): TreeWrapper {
});
}
function setClockAt(clock: Tree.Clock | undefined, path: Tree.Path) {
return updateAt(path, function(node) {
node.clock = clock;
});
}
function parentNode(path: Tree.Path): Tree.Node {
return nodeAtPath(treePath.init(path));
}
@ -238,14 +238,24 @@ export function build(root: Tree.Node): TreeWrapper {
setCommentAt,
deleteCommentAt,
setGlyphsAt,
setClockAt,
setClockAt(clock: Tree.Clock | undefined, path: Tree.Path) {
return updateAt(path, function(node) {
node.clock = clock;
});
},
pathIsMainline,
pathIsForcedVariation,
lastMainlineNode(path: Tree.Path): Tree.Node {
return lastMainlineNodeFrom(root, path);
},
pathExists,
deleteNodeAt,
promoteAt,
forceVariationAt(path: Tree.Path, force: boolean) {
return updateAt(path, function(node) {
node.forceVariation = force;
});
},
getCurrentNodesAfterPly,
merge(tree: Tree.Node) {
ops.merge(root, tree);