more work on gamebook
This commit is contained in:
parent
cc803359dd
commit
72789d3c28
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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!;
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -94,7 +94,7 @@ export function merge(n1: Tree.Node, n2: Tree.Node): void {
|
|||
}).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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue