more work on gamebook

This commit is contained in:
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; font-size: 1.3em;
} }
.gb_play .comment { .gb_play .comment {
flex: 1 1 auto;
background: #fff; background: #fff;
border-radius: 10px; border-radius: 10px;
position: relative; position: relative;
border: 1px solid #aaa; border: 1px solid #aaa;
display: flex; display: flex;
} }
.gb_play .comment:after { .gb_play .comment::after {
position: absolute; position: absolute;
content: ''; content: '';
bottom: -9px; bottom: -9px;
@ -34,24 +33,51 @@
overflow-y: auto; overflow-y: auto;
padding: 10px 12px; padding: 10px 12px;
} }
@keyframes mascot {
50% { transform: scale(1.03); }
100% { transform: scale(1); }
}
.gb_play .mascot { .gb_play .mascot {
margin: 8px 0 -7px 0; margin: 10px 0 -7px 0;
cursor: pointer; 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; margin-top: 20px;
height: 120px; height: 120px;
background: #e0e0e0;
border: 1px solid #ccc;
}
.gb_play .turn {
text-transform: uppercase;
line-height: 120px;
text-align: center; text-align: center;
font-size: 1.5em; 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 { .gb_buttons .button {
margin-left: 10px; margin-left: 10px;
} }

View file

@ -250,7 +250,7 @@ export default class AnalyseCtrl {
color = this.turnColor(), color = this.turnColor(),
dests = chessUtil.readDests(this.node.dests), dests = chessUtil.readDests(this.node.dests),
drops = chessUtil.readDrops(this.node.drops), drops = chessUtil.readDrops(this.node.drops),
movableColor = this.practice ? this.bottomColor() : ( movableColor = (this.practice || this.gamebookPlay()) ? this.bottomColor() : (
!this.embed && ( !this.embed && (
(dests && Object.keys(dests).length > 0) || (dests && Object.keys(dests).length > 0) ||
drops === null || drops.length drops === null || drops.length
@ -275,7 +275,7 @@ export default class AnalyseCtrl {
config.movable!.color = color; config.movable!.color = color;
} }
config.premovable = { 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; this.cgConfig = config;
return config; return config;

View file

@ -108,7 +108,7 @@ export function make(root: AnalyseCtrl, playableDepth: () => number): PracticeCt
} else { } else {
comment(null); comment(null);
if (node.san && commentable(node)) { 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)) if (commentable(parentNode, +1))
comment(makeComment(parentNode, node, root.path)); comment(makeComment(parentNode, node, root.path));
} }

View file

@ -9,6 +9,8 @@ export interface RetroCtrl {
[key: string]: any; [key: string]: any;
} }
type Feedback = 'find' | 'eval' | 'win' | 'fail' | 'view';
export function make(root: AnalyseCtrl): RetroCtrl { export function make(root: AnalyseCtrl): RetroCtrl {
const game = root.data.game; const game = root.data.game;
@ -17,7 +19,7 @@ export function make(root: AnalyseCtrl): RetroCtrl {
const explorerCancelPlies: number[] = []; const explorerCancelPlies: number[] = [];
let solvedPlies: number[] = []; let solvedPlies: number[] = [];
const current = prop<any>(null); 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 contains = window.lichess.fp.contains;
const redraw = root.redraw; const redraw = root.redraw;

View file

@ -16,7 +16,7 @@ export function render(ctrl: AnalyseCtrl): VNode {
const study = ctrl.study!, const study = ctrl.study!,
isMyMove = ctrl.turnColor() === ctrl.data.orientation, isMyMove = ctrl.turnColor() === ctrl.data.orientation,
isCommented = !!(ctrl.node.comments || []).find(c => c.text.length > 2), 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; let content: MaybeVNodes;

View file

@ -1,26 +1,74 @@
import AnalyseCtrl from '../../ctrl'; import AnalyseCtrl from '../../ctrl';
import { StudyCtrl } from '../interfaces'; import { StudyCtrl } from '../interfaces';
import { readOnlyProp } from '../../util'; import { readOnlyProp } from '../../util';
import { path as treePath, ops as treeOps } from 'tree';
import Mascot from './mascot'; import Mascot from './mascot';
type Feedback = 'play' | 'good' | 'bad' | 'end';
export interface State {
feedback: Feedback;
comment?: string;
hint?: string;
}
export default class GamebookPlayCtrl { export default class GamebookPlayCtrl {
ply: Ply;
mascot = new Mascot(); mascot = new Mascot();
state: State;
constructor(readonly root: AnalyseCtrl, readonly chapterId: string, readonly redraw: () => void) { constructor(readonly root: AnalyseCtrl, readonly chapterId: string, readonly redraw: () => void) {
this.ply = this.root.node.ply;
// root.showAutoShapes = readOnlyProp(true); this.makeState();
// root.showGauge = readOnlyProp(true);
// root.showComputer = readOnlyProp(true); // ensure all original nodes have a gamebook entry,
// goal(root.data.practiceGoal!); // so we can differentiate original nodes from user-made ones
// nbMoves(0); treeOps.updateAll(root.tree.root, n => n.gamebook = n.gamebook || {});
// 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());
} }
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!; private study = (): StudyCtrl => this.root.study!;
} }

View file

@ -4,34 +4,64 @@ import GamebookPlayCtrl from './gamebookPlayCtrl';
// import AnalyseCtrl from '../../ctrl'; // import AnalyseCtrl from '../../ctrl';
import { bind, dataIcon, innerHTML } from '../../util'; import { bind, dataIcon, innerHTML } from '../../util';
import { enrichText } from '../studyComments'; import { enrichText } from '../studyComments';
import { State } from './gamebookPlayCtrl';
// import { MaybeVNodes } from '../../interfaces'; // import { MaybeVNodes } from '../../interfaces';
// import { throttle } from 'common'; // 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 { export function render(ctrl: GamebookPlayCtrl): VNode {
const root = ctrl.root, const root = ctrl.root,
node: Tree.Node = root.node, state = ctrl.state;
gb: Tree.Gamebook = node.gamebook || {}, // state.feedback = 'bad';
isMyMove = root.turnColor() === root.data.orientation,
comment = (root.node.comments || [])[0]; const comment = state.comment || defaultComments[state.feedback],
isMyMove = ['good', 'bad'].indexOf(state.feedback) > -1;
return h('div.gamebook', { return h('div.gamebook', {
hook: { insert: _ => window.lichess.loadCss('/assets/stylesheets/gamebook.play.css') } hook: { insert: _ => window.lichess.loadCss('/assets/stylesheets/gamebook.play.css') }
}, [ }, [
h('div.comment', h('div.content', { 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', { h('img.mascot', {
attrs: { attrs: {
width: 120, width: 120,
height: 120, height: 120,
src: window.lichess.assetUrl(`/assets/images/mascot/${ctrl.mascot.current}.svg`), src: ctrl.mascot.url(),
title: 'Click to choose your teacher' title: 'Click to choose your teacher'
}, },
hook: bind('click', ctrl.mascot.switch, ctrl.redraw) hook: bind('click', ctrl.mascot.switch, ctrl.redraw)
}), }),
h('div.soapbox', [ renderFeedback(ctrl, state)
h('div.turn', isMyMove ? 'Your turn' : 'Opponent turn')
])
]); ]);
} }
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.current = this.list[newIndex % this.list.length];
this.storage.set(this.current); 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; req.ch = vm.chapterId;
return req; 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() { function configureAnalysis() {
if (ctrl.embed) return; if (ctrl.embed) return;
@ -109,7 +113,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
vm.mode.write = vm.mode.write && canContribute; vm.mode.write = vm.mode.write && canContribute;
li.pubsub.emit('chat.writeable')(data.features.chat); li.pubsub.emit('chat.writeable')(data.features.chat);
li.pubsub.emit('chat.permissions')({local: canContribute}); 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); if (!computer) ctrl.getCeval().enabled(false);
ctrl.getCeval().allowed(computer); ctrl.getCeval().allowed(computer);
if (!data.chapter.features.explorer) ctrl.explorer.disable(); if (!data.chapter.features.explorer) ctrl.explorer.disable();
@ -144,6 +148,8 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
configureAnalysis(); configureAnalysis();
vm.loading = false; vm.loading = false;
instanciateGamebookPlay();
let nextPath: Tree.Path; let nextPath: Tree.Path;
if (vm.mode.sticky) { if (vm.mode.sticky) {
@ -194,7 +200,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
let gamebookPlay: GamebookPlayCtrl | undefined; let gamebookPlay: GamebookPlayCtrl | undefined;
function instanciateGamebookPlay() { function instanciateGamebookPlay() {
if (!data.chapter.gamebook || members.canContribute()) return gamebookPlay = undefined; if (!isGamebookPlay()) return gamebookPlay = undefined;
if (gamebookPlay && gamebookPlay.chapterId === vm.chapterId) return; if (gamebookPlay && gamebookPlay.chapterId === vm.chapterId) return;
gamebookPlay = new GamebookPlayCtrl(ctrl, vm.chapterId, redraw); gamebookPlay = new GamebookPlayCtrl(ctrl, vm.chapterId, redraw);
vm.mode.sticky = false; vm.mode.sticky = false;
@ -243,6 +249,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
currentChapter, currentChapter,
isChapterOwner, isChapterOwner,
canJumpTo(path: Tree.Path) { canJumpTo(path: Tree.Path) {
if (gamebookPlay) return gamebookPlay.canJumpTo(path);
return data.chapter.conceal === undefined || return data.chapter.conceal === undefined ||
isChapterOwner() || isChapterOwner() ||
treePath.contains(ctrl.path, path) || // can always go back treePath.contains(ctrl.path, path) || // can always go back
@ -251,6 +258,7 @@ export default function(data: StudyData, ctrl: AnalyseCtrl, tagTypes: TagTypes,
onJump() { onJump() {
chapters.localPaths[vm.chapterId] = ctrl.path; chapters.localPaths[vm.chapterId] = ctrl.path;
if (practice) practice.onJump(); if (practice) practice.onJump();
if (gamebookPlay) gamebookPlay.onJump();
}, },
withPosition(obj) { withPosition(obj) {
obj.ch = vm.chapterId; 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) { export function countChildrenAndComments(node: Tree.Node) {
var count = { const count = {
nodes: 1, nodes: 1,
comments: (node.comments || []).length comments: (node.comments || []).length
}; };
node.children.forEach(function(child) { node.children.forEach(function(child) {
var c = countChildrenAndComments(child); const c = countChildrenAndComments(child);
count.nodes += c.nodes; count.nodes += c.nodes;
count.comments += c.comments; count.comments += c.comments;
}); });
@ -70,11 +70,11 @@ export function countChildrenAndComments(node: Tree.Node) {
} }
export function reconstruct(parts: any): Tree.Node { export function reconstruct(parts: any): Tree.Node {
var root = parts[0], const root = parts[0], nb = parts.length;
node = root; let node = root, i: number;
root.id = ''; root.id = '';
for (var i = 1, nb = parts.length; i < nb; i++) { for (i = 1; i < nb; i++) {
var n = parts[i]; const n = parts[i];
if (node.children) node.children.unshift(n); if (node.children) node.children.unshift(n);
else node.children = [n]; else node.children = [n];
node = n; node = n;
@ -90,11 +90,11 @@ export function merge(n1: Tree.Node, n2: Tree.Node): void {
n2.comments && n2.comments.forEach(function(c) { n2.comments && n2.comments.forEach(function(c) {
if (!n1.comments) n1.comments = [c]; if (!n1.comments) n1.comments = [c];
else if (!n1.comments.filter(function(d) { else if (!n1.comments.filter(function(d) {
return d.text === c.text; return d.text === c.text;
}).length) n1.comments.push(c); }).length) n1.comments.push(c);
}); });
n2.children.forEach(function(c) { n2.children.forEach(function(c) {
var existing = childById(n1, c.id); const existing = childById(n1, c.id);
if (existing) merge(existing, c); if (existing) merge(existing, c);
else n1.children.push(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 { export function updateAll(root: Tree.Node, f: (node: Tree.Node) => void): void {
// applies f recursively to all nodes // applies f recursively to all nodes
var update = function(node: Tree.Node) { function update(node: Tree.Node) {
f(node); f(node);
node.children.forEach(update); node.children.forEach(update);
}; };

View file

@ -29,6 +29,7 @@ export interface TreeWrapper {
merge(tree: Tree.Node): void; merge(tree: Tree.Node): void;
removeCeval(): void; removeCeval(): void;
removeComputerVariations(): void; removeComputerVariations(): void;
parentNode(path: Tree.Path): Tree.Node;
getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined; 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 { function deleteNodeAt(path: Tree.Path): void {
var parent = nodeAtPath(treePath.init(path)); ops.removeChild(parentNode(path), treePath.last(path));
var id = treePath.last(path);
ops.removeChild(parent, id);
} }
function promoteAt(path: Tree.Path, toMainline: boolean): void { 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 { function getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined {
if (!('parentClock' in node)) { if (!('parentClock' in node)) {
var parent = path && nodeAtPath(treePath.init(path)); const par = path && parentNode(path);
if (!parent) node.parentClock = node.clock; if (!par) node.parentClock = node.clock;
else if (!('clock' in parent)) node.parentClock = undefined; else if (!('clock' in par)) node.parentClock = undefined;
else node.parentClock = parent.clock; else node.parentClock = par.clock;
} }
return node.parentClock; return node.parentClock;
} }
@ -259,6 +262,7 @@ export function build(root: Tree.Node): TreeWrapper {
}); });
}); });
}, },
parentNode,
getParentClock getParentClock
}; };
} }