more work on gamebook

pull/3486/merge
Thibault Duplessis 2017-08-19 16:08:09 -05:00
parent cc803359dd
commit 72789d3c28
11 changed files with 179 additions and 59 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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));
}

View File

@ -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<any>(null);
const feedback = prop('find'); // find | eval | win | fail | view
const feedback = prop<Feedback>('find');
const contains = window.lichess.fp.contains;
const redraw = root.redraw;

View File

@ -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;

View File

@ -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<State> = {
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!;
}

View File

@ -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')
);
}

View File

@ -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`);
}

View File

@ -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;

View File

@ -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);
};

View File

@ -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
};
}