From 72789d3c2824943ea3e36f3c8a254dbab3ddb9bc Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 19 Aug 2017 16:08:09 -0500 Subject: [PATCH] more work on gamebook --- public/stylesheets/gamebook.play.css | 50 +++++++++---- ui/analyse/src/ctrl.ts | 4 +- ui/analyse/src/practice/practiceCtrl.ts | 2 +- ui/analyse/src/retrospect/retroCtrl.ts | 4 +- ui/analyse/src/study/gamebook/gamebookEdit.ts | 2 +- .../src/study/gamebook/gamebookPlayCtrl.ts | 72 +++++++++++++++---- .../src/study/gamebook/gamebookPlayView.ts | 48 ++++++++++--- ui/analyse/src/study/gamebook/mascot.ts | 2 + ui/analyse/src/study/studyCtrl.ts | 16 +++-- ui/tree/src/ops.ts | 20 +++--- ui/tree/src/tree.ts | 18 +++-- 11 files changed, 179 insertions(+), 59 deletions(-) diff --git a/public/stylesheets/gamebook.play.css b/public/stylesheets/gamebook.play.css index 54a8dc0ea6..8bb07aa0cb 100644 --- a/public/stylesheets/gamebook.play.css +++ b/public/stylesheets/gamebook.play.css @@ -9,14 +9,13 @@ font-size: 1.3em; } .gb_play .comment { - flex: 1 1 auto; background: #fff; border-radius: 10px; position: relative; border: 1px solid #aaa; display: flex; } -.gb_play .comment:after { +.gb_play .comment::after { position: absolute; content: ''; bottom: -9px; @@ -34,24 +33,51 @@ overflow-y: auto; padding: 10px 12px; } +@keyframes mascot { + 50% { transform: scale(1.03); } + 100% { transform: scale(1); } +} .gb_play .mascot { - margin: 8px 0 -7px 0; + margin: 10px 0 -7px 0; cursor: pointer; } -.gb_play .soapbox { +.gb_play .mascot:hover { + animation: mascot 0.5s ease-in-out; +} +.gb_play .feedback { + flex: 0 0 120px; margin-top: 20px; height: 120px; - background: #e0e0e0; - border: 1px solid #ccc; -} -.gb_play .turn { - text-transform: uppercase; - line-height: 120px; text-align: center; font-size: 1.5em; - color: #3893E8; + background: #e0e0e0; + display: flex; + flex-flow: column; + justify-content: center; +} +.gb_play .feedback.act { + font-size: 2em; + color: #fff; + cursor: pointer; + opacity: 0.8; + transition: 0.13s; + border-radius: 5px; +} +.gb_play .feedback.act:hover { + opacity: 1; + transform: scale(1.02, 1.02); +} +.gb_play .feedback.act span { + animation: rubberBand 8s infinite; +} +.gb_play .feedback.bad { + background: #dc322f; +} +.gb_play .feedback.good, +.gb_play .feedback.end { + background: #639B24; + color: #fff; } - .gb_buttons .button { margin-left: 10px; } diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 1fa173afd1..234fbdcef6 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -250,7 +250,7 @@ export default class AnalyseCtrl { color = this.turnColor(), dests = chessUtil.readDests(this.node.dests), drops = chessUtil.readDrops(this.node.drops), - movableColor = this.practice ? this.bottomColor() : ( + movableColor = (this.practice || this.gamebookPlay()) ? this.bottomColor() : ( !this.embed && ( (dests && Object.keys(dests).length > 0) || drops === null || drops.length @@ -275,7 +275,7 @@ export default class AnalyseCtrl { config.movable!.color = color; } config.premovable = { - enabled: config.movable!.color && config.turnColor !== config.movable!.color + enabled: config.movable!.color && config.turnColor !== config.movable!.color && !this.gamebookPlay() }; this.cgConfig = config; return config; diff --git a/ui/analyse/src/practice/practiceCtrl.ts b/ui/analyse/src/practice/practiceCtrl.ts index c08ef9fedf..01533444fd 100644 --- a/ui/analyse/src/practice/practiceCtrl.ts +++ b/ui/analyse/src/practice/practiceCtrl.ts @@ -108,7 +108,7 @@ export function make(root: AnalyseCtrl, playableDepth: () => number): PracticeCt } else { comment(null); if (node.san && commentable(node)) { - const parentNode = root.tree.nodeAtPath(treePath.init(root.path)); + const parentNode = root.tree.parentNode(root.path); if (commentable(parentNode, +1)) comment(makeComment(parentNode, node, root.path)); } diff --git a/ui/analyse/src/retrospect/retroCtrl.ts b/ui/analyse/src/retrospect/retroCtrl.ts index 1539b0ab39..61a8305564 100644 --- a/ui/analyse/src/retrospect/retroCtrl.ts +++ b/ui/analyse/src/retrospect/retroCtrl.ts @@ -9,6 +9,8 @@ export interface RetroCtrl { [key: string]: any; } +type Feedback = 'find' | 'eval' | 'win' | 'fail' | 'view'; + export function make(root: AnalyseCtrl): RetroCtrl { const game = root.data.game; @@ -17,7 +19,7 @@ export function make(root: AnalyseCtrl): RetroCtrl { const explorerCancelPlies: number[] = []; let solvedPlies: number[] = []; const current = prop(null); - const feedback = prop('find'); // find | eval | win | fail | view + const feedback = prop('find'); const contains = window.lichess.fp.contains; const redraw = root.redraw; diff --git a/ui/analyse/src/study/gamebook/gamebookEdit.ts b/ui/analyse/src/study/gamebook/gamebookEdit.ts index 5e39acb666..1f13723f38 100644 --- a/ui/analyse/src/study/gamebook/gamebookEdit.ts +++ b/ui/analyse/src/study/gamebook/gamebookEdit.ts @@ -16,7 +16,7 @@ export function render(ctrl: AnalyseCtrl): VNode { const study = ctrl.study!, isMyMove = ctrl.turnColor() === ctrl.data.orientation, isCommented = !!(ctrl.node.comments || []).find(c => c.text.length > 2), - hasVariation = ctrl.tree.nodeAtPath(treePath.init(ctrl.path)).children.length > 1; + hasVariation = ctrl.tree.parentNode(ctrl.path).children.length > 1; let content: MaybeVNodes; diff --git a/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts b/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts index 12bf157e75..e8ee6e8b13 100644 --- a/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts +++ b/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts @@ -1,26 +1,74 @@ import AnalyseCtrl from '../../ctrl'; import { StudyCtrl } from '../interfaces'; import { readOnlyProp } from '../../util'; +import { path as treePath, ops as treeOps } from 'tree'; import Mascot from './mascot'; +type Feedback = 'play' | 'good' | 'bad' | 'end'; + +export interface State { + feedback: Feedback; + comment?: string; + hint?: string; +} + export default class GamebookPlayCtrl { - ply: Ply; mascot = new Mascot(); + state: State; constructor(readonly root: AnalyseCtrl, readonly chapterId: string, readonly redraw: () => void) { - this.ply = this.root.node.ply; - // root.showAutoShapes = readOnlyProp(true); - // root.showGauge = readOnlyProp(true); - // root.showComputer = readOnlyProp(true); - // goal(root.data.practiceGoal!); - // nbMoves(0); - // success(null); - // comment(makeComment(root.tree.root)); - // const chapter = studyData.chapter; - // history.replaceState(null, chapter.name, data.url + '/' + chapter.id); - // analysisUrl('/analysis/standard/' + root.node.fen.replace(/ /g, '_') + '?color=' + root.bottomColor()); + + this.makeState(); + + // ensure all original nodes have a gamebook entry, + // so we can differentiate original nodes from user-made ones + treeOps.updateAll(root.tree.root, n => n.gamebook = n.gamebook || {}); } + private makeState(): void { + const node = this.root.node, + nodeComment = (node.comments || [])[0], + state: Partial = { + comment: nodeComment ? nodeComment.text : undefined + }, + parPath = treePath.init(this.root.path), + parNode = this.root.tree.nodeAtPath(parPath); + if (!this.root.onMainline && !this.root.tree.pathIsMainline(parPath)) return; + if (this.root.turnColor() === this.root.data.orientation) { + state.feedback = 'play'; + state.hint = (node.gamebook || {}).hint; + } else if (this.root.onMainline) { + if (node.children[0]) { + state.feedback = 'good'; + } else { + state.feedback = 'end'; + } + } else { + state.feedback = 'bad'; + if (!state.comment) { + state.comment = parNode.gamebook!.deviation; + } + } + this.state = state as State; + } + + retry = () => { + let path = this.root.path; + while (path && !this.root.tree.pathIsMainline(path)) path = treePath.init(path); + this.root.userJump(path); + } + + next = () => { + const child = this.root.node.children[0]; + if (child) this.root.userJump(this.root.path + child.id); + } + + canJumpTo = (path: Tree.Path) => treePath.contains(this.root.path, path); + + onJump = () => { + this.makeState(); + }; + private study = (): StudyCtrl => this.root.study!; } diff --git a/ui/analyse/src/study/gamebook/gamebookPlayView.ts b/ui/analyse/src/study/gamebook/gamebookPlayView.ts index 7bad39836c..cafddf0f20 100644 --- a/ui/analyse/src/study/gamebook/gamebookPlayView.ts +++ b/ui/analyse/src/study/gamebook/gamebookPlayView.ts @@ -4,34 +4,64 @@ import GamebookPlayCtrl from './gamebookPlayCtrl'; // import AnalyseCtrl from '../../ctrl'; import { bind, dataIcon, innerHTML } from '../../util'; import { enrichText } from '../studyComments'; +import { State } from './gamebookPlayCtrl'; // import { MaybeVNodes } from '../../interfaces'; // import { throttle } from 'common'; +const defaultComments = { + play: 'What would you play in this position?', + good: 'Yes indeed, good move!', + bad: 'This is not the move you are looking for.', + end: 'And that is all she wrote.' +}; + export function render(ctrl: GamebookPlayCtrl): VNode { const root = ctrl.root, - node: Tree.Node = root.node, - gb: Tree.Gamebook = node.gamebook || {}, - isMyMove = root.turnColor() === root.data.orientation, - comment = (root.node.comments || [])[0]; + state = ctrl.state; + // state.feedback = 'bad'; + + const comment = state.comment || defaultComments[state.feedback], + isMyMove = ['good', 'bad'].indexOf(state.feedback) > -1; return h('div.gamebook', { hook: { insert: _ => window.lichess.loadCss('/assets/stylesheets/gamebook.play.css') } }, [ h('div.comment', h('div.content', { - hook: comment && innerHTML(comment.text, text => enrichText(text, true)) + hook: innerHTML(comment, text => enrichText(text, true)) })), h('img.mascot', { attrs: { width: 120, height: 120, - src: window.lichess.assetUrl(`/assets/images/mascot/${ctrl.mascot.current}.svg`), + src: ctrl.mascot.url(), title: 'Click to choose your teacher' }, hook: bind('click', ctrl.mascot.switch, ctrl.redraw) }), - h('div.soapbox', [ - h('div.turn', isMyMove ? 'Your turn' : 'Opponent turn') - ]) + renderFeedback(ctrl, state) ]); } + +function renderFeedback(ctrl: GamebookPlayCtrl, state: State) { + const fb = state.feedback; + if (fb === 'bad') return h('div.feedback.act.bad', { + hook: bind('click', ctrl.retry, ctrl.redraw) + }, [ + h('i', { attrs: dataIcon('P') }), + h('span', 'Retry') + ]); + if (fb === 'good') return h('div.feedback.act.good', { + hook: bind('click', ctrl.next, ctrl.redraw) + }, [ + h('i', { attrs: dataIcon('G') }), + h('span', 'Next') + ]); + if (fb === 'end') return h('div.feedback.end', [ + h('i', { attrs: dataIcon('E') }), + h('span', 'Gamebook complete') + ]); + return h('div.feedback.' + fb, + h('span', fb === 'play' ? 'Your turn' : 'Opponent turn') + ); +} diff --git a/ui/analyse/src/study/gamebook/mascot.ts b/ui/analyse/src/study/gamebook/mascot.ts index 4c64ff6b37..dc4ddee090 100644 --- a/ui/analyse/src/study/gamebook/mascot.ts +++ b/ui/analyse/src/study/gamebook/mascot.ts @@ -16,4 +16,6 @@ export default class Mascot { this.current = this.list[newIndex % this.list.length]; this.storage.set(this.current); } + + url = () => window.lichess.assetUrl(`/assets/images/mascot/${this.current}.svg`); } diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index 8aae55bdee..c6043c64fd 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -99,8 +99,12 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, req.ch = vm.chapterId; return req; } - // remain on initial position if gamebook player - if (vm.mode.sticky && !(data.chapter.gamebook && !members.canContribute())) ctrl.userJump(data.position.path); + + function isGamebookPlay() { + return data.chapter.gamebook && !members.canContribute(); + } + + if (vm.mode.sticky && !isGamebookPlay()) ctrl.userJump(data.position.path); function configureAnalysis() { if (ctrl.embed) return; @@ -109,7 +113,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, vm.mode.write = vm.mode.write && canContribute; li.pubsub.emit('chat.writeable')(data.features.chat); li.pubsub.emit('chat.permissions')({local: canContribute}); - const computer: boolean = !!(data.chapter.features.computer || data.chapter.practice); + const computer: boolean = !isGamebookPlay() && !!(data.chapter.features.computer || data.chapter.practice); if (!computer) ctrl.getCeval().enabled(false); ctrl.getCeval().allowed(computer); if (!data.chapter.features.explorer) ctrl.explorer.disable(); @@ -144,6 +148,8 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, configureAnalysis(); vm.loading = false; + instanciateGamebookPlay(); + let nextPath: Tree.Path; if (vm.mode.sticky) { @@ -194,7 +200,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, let gamebookPlay: GamebookPlayCtrl | undefined; function instanciateGamebookPlay() { - if (!data.chapter.gamebook || members.canContribute()) return gamebookPlay = undefined; + if (!isGamebookPlay()) return gamebookPlay = undefined; if (gamebookPlay && gamebookPlay.chapterId === vm.chapterId) return; gamebookPlay = new GamebookPlayCtrl(ctrl, vm.chapterId, redraw); vm.mode.sticky = false; @@ -243,6 +249,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, currentChapter, isChapterOwner, canJumpTo(path: Tree.Path) { + if (gamebookPlay) return gamebookPlay.canJumpTo(path); return data.chapter.conceal === undefined || isChapterOwner() || treePath.contains(ctrl.path, path) || // can always go back @@ -251,6 +258,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes, onJump() { chapters.localPaths[vm.chapterId] = ctrl.path; if (practice) practice.onJump(); + if (gamebookPlay) gamebookPlay.onJump(); }, withPosition(obj) { obj.ch = vm.chapterId; diff --git a/ui/tree/src/ops.ts b/ui/tree/src/ops.ts index 4a9fcdd46c..16ee0492e9 100644 --- a/ui/tree/src/ops.ts +++ b/ui/tree/src/ops.ts @@ -57,12 +57,12 @@ export function removeChild(parent: Tree.Node, id: string): void { } export function countChildrenAndComments(node: Tree.Node) { - var count = { + const count = { nodes: 1, comments: (node.comments || []).length }; node.children.forEach(function(child) { - var c = countChildrenAndComments(child); + const c = countChildrenAndComments(child); count.nodes += c.nodes; count.comments += c.comments; }); @@ -70,11 +70,11 @@ export function countChildrenAndComments(node: Tree.Node) { } export function reconstruct(parts: any): Tree.Node { - var root = parts[0], - node = root; + const root = parts[0], nb = parts.length; + let node = root, i: number; root.id = ''; - for (var i = 1, nb = parts.length; i < nb; i++) { - var n = parts[i]; + for (i = 1; i < nb; i++) { + const n = parts[i]; if (node.children) node.children.unshift(n); else node.children = [n]; node = n; @@ -90,11 +90,11 @@ export function merge(n1: Tree.Node, n2: Tree.Node): void { n2.comments && n2.comments.forEach(function(c) { if (!n1.comments) n1.comments = [c]; else if (!n1.comments.filter(function(d) { - return d.text === c.text; - }).length) n1.comments.push(c); + return d.text === c.text; + }).length) n1.comments.push(c); }); n2.children.forEach(function(c) { - var existing = childById(n1, c.id); + const existing = childById(n1, c.id); if (existing) merge(existing, c); else n1.children.push(c); }); @@ -112,7 +112,7 @@ export function mainlineNodeList(from: Tree.Node): Tree.Node[] { export function updateAll(root: Tree.Node, f: (node: Tree.Node) => void): void { // applies f recursively to all nodes - var update = function(node: Tree.Node) { + function update(node: Tree.Node) { f(node); node.children.forEach(update); }; diff --git a/ui/tree/src/tree.ts b/ui/tree/src/tree.ts index 3373c2b086..1a5b443bf4 100644 --- a/ui/tree/src/tree.ts +++ b/ui/tree/src/tree.ts @@ -29,6 +29,7 @@ export interface TreeWrapper { merge(tree: Tree.Node): void; removeCeval(): void; removeComputerVariations(): void; + parentNode(path: Tree.Path): Tree.Node; getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined; } @@ -148,9 +149,7 @@ export function build(root: Tree.Node): TreeWrapper { } function deleteNodeAt(path: Tree.Path): void { - var parent = nodeAtPath(treePath.init(path)); - var id = treePath.last(path); - ops.removeChild(parent, id); + ops.removeChild(parentNode(path), treePath.last(path)); } function promoteAt(path: Tree.Path, toMainline: boolean): void { @@ -198,12 +197,16 @@ export function build(root: Tree.Node): TreeWrapper { }); } + function parentNode(path: Tree.Path): Tree.Node { + return nodeAtPath(treePath.init(path)); + } + function getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined { if (!('parentClock' in node)) { - var parent = path && nodeAtPath(treePath.init(path)); - if (!parent) node.parentClock = node.clock; - else if (!('clock' in parent)) node.parentClock = undefined; - else node.parentClock = parent.clock; + const par = path && parentNode(path); + if (!par) node.parentClock = node.clock; + else if (!('clock' in par)) node.parentClock = undefined; + else node.parentClock = par.clock; } return node.parentClock; } @@ -259,6 +262,7 @@ export function build(root: Tree.Node): TreeWrapper { }); }); }, + parentNode, getParentClock }; }