lila/ui/tree/src/tree.ts

280 lines
8.8 KiB
TypeScript

import * as treePath from './path';
import * as ops from './ops';
import { defined } from 'common';
export type MaybeNode = Tree.Node | undefined;
export interface TreeWrapper {
root: Tree.Node;
lastPly(): number;
nodeAtPath(path: Tree.Path): Tree.Node;
getNodeList(path: Tree.Path): Tree.Node[];
longestValidPath(path: string): Tree.Path;
getOpening(nodeList: Tree.Node[]): Tree.Opening | undefined;
updateAt(path: Tree.Path, update: (node: Tree.Node) => void): MaybeNode;
addNode(node: Tree.Node, path: Tree.Path): Tree.Path | undefined;
addNodes(nodes: Tree.Node[], path: Tree.Path): Tree.Path | undefined;
addDests(dests: string, path: Tree.Path, opening?: Tree.Opening): MaybeNode;
setShapes(shapes: Tree.Shape[], path: Tree.Path): MaybeNode;
setCommentAt(comment: Tree.Comment, path: Tree.Path): MaybeNode;
deleteCommentAt(id: string, path: Tree.Path): MaybeNode;
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;
removeComputerVariations(): void;
parentNode(path: Tree.Path): Tree.Node;
getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined;
}
export function build(root: Tree.Node): TreeWrapper {
function lastNode(): Tree.Node {
return ops.findInMainline(root, function(node: Tree.Node) {
return !node.children.length;
})!;
}
function nodeAtPath(path: Tree.Path): Tree.Node {
return nodeAtPathFrom(root, path);
}
function nodeAtPathFrom(node: Tree.Node, path: Tree.Path): Tree.Node {
if (path === '') return node;
const child = ops.childById(node, treePath.head(path));
return child ? nodeAtPathFrom(child, treePath.tail(path)) : node;
}
function nodeAtPathOrNull(path: Tree.Path): Tree.Node | undefined {
return nodeAtPathOrNullFrom(root, path);
}
function nodeAtPathOrNullFrom(node: Tree.Node, path: Tree.Path): Tree.Node | undefined {
if (path === '') return node;
const child = ops.childById(node, treePath.head(path));
return child ? nodeAtPathOrNullFrom(child, treePath.tail(path)) : undefined;
}
function longestValidPathFrom(node: Tree.Node, path: Tree.Path): Tree.Path {
var id = treePath.head(path);
const child = ops.childById(node, id);
return child ? id + longestValidPathFrom(child, treePath.tail(path)) : '';
}
function getCurrentNodesAfterPly(nodeList: Tree.Node[], mainline: Tree.Node[], ply: number): Tree.Node[] {
var node, nodes = [];
for (var i in nodeList) {
node = nodeList[i];
if (node.ply <= ply && mainline[i].id !== node.id) break;
if (node.ply > ply) nodes.push(node);
}
return nodes;
};
function pathIsMainline(path: Tree.Path): boolean {
return pathIsMainlineFrom(root, path);
}
function pathExists(path: Tree.Path): boolean {
return !!nodeAtPathOrNull(path);
}
function pathIsMainlineFrom(node: Tree.Node, path: Tree.Path): boolean {
if (path === '') return true;
const pathId = treePath.head(path),
child = node.children[0];
if (!child || child.id !== pathId) return false;
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);
const child = node.children[0];
if (!child || child.id !== pathId) return node;
return lastMainlineNodeFrom(child, treePath.tail(path));
}
function getNodeList(path: Tree.Path): Tree.Node[] {
return ops.collect(root, function(node: Tree.Node) {
const id = treePath.head(path);
if (id === '') return;
path = treePath.tail(path);
return ops.childById(node, id);
});
}
function getOpening(nodeList: Tree.Node[]): Tree.Opening | undefined {
var opening: Tree.Opening | undefined;
nodeList.forEach(function(node: Tree.Node) {
opening = node.opening || opening;
});
return opening;
}
function updateAt(path: Tree.Path, update: (node: Tree.Node) => void): Tree.Node | undefined {
const node = nodeAtPathOrNull(path);
if (node) {
update(node);
return node;
}
return;
}
// returns new path
function addNode(node: Tree.Node, path: Tree.Path): Tree.Path | undefined {
const newPath = path + node.id,
existing = nodeAtPathOrNull(newPath);
if (existing) {
(['dests', 'drops', 'clock'] as Array<keyof Tree.Node>).forEach(key => {
if (defined(node[key]) && !defined(existing[key])) existing[key] = node[key] as never;
});
return newPath;
}
return updateAt(path, function(parent: Tree.Node) {
parent.children.push(node);
}) ? newPath : undefined;
}
function addNodes(nodes: Tree.Node[], path: Tree.Path): Tree.Path | undefined {
var node = nodes[0];
if (!node) return path;
const newPath = addNode(node, path);
return newPath ? addNodes(nodes.slice(1), newPath) : undefined;
}
function deleteNodeAt(path: Tree.Path): void {
ops.removeChild(parentNode(path), treePath.last(path));
}
function promoteAt(path: Tree.Path, toMainline: boolean): void {
var nodes = getNodeList(path);
for (var i = nodes.length - 2; i >= 0; i--) {
var node = nodes[i + 1];
var parent = nodes[i];
if (parent.children[0].id !== node.id) {
ops.removeChild(parent, node.id);
parent.children.unshift(node);
if (!toMainline) break;
}
}
}
function setCommentAt(comment: Tree.Comment, path: Tree.Path) {
return !comment.text ? deleteCommentAt(comment.id, path) : updateAt(path, function(node) {
node.comments = node.comments || [];
const existing = node.comments.find(function(c) {
return c.id === comment.id;
});
if (existing) existing.text = comment.text;
else node.comments.push(comment);
});
}
function deleteCommentAt(id: string, path: Tree.Path) {
return updateAt(path, function(node) {
var comments = (node.comments || []).filter(function(c) {
return c.id !== id
});
node.comments = comments.length ? comments : undefined;
});
}
function setGlyphsAt(glyphs: Tree.Glyph[], path: Tree.Path) {
return updateAt(path, function(node) {
node.glyphs = glyphs;
});
}
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)) {
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;
}
return {
root,
lastPly(): number {
return lastNode().ply;
},
nodeAtPath,
getNodeList,
longestValidPath: (path: string) => longestValidPathFrom(root, path),
getOpening,
updateAt,
addNode,
addNodes,
addDests(dests: string, path: Tree.Path, opening?: Tree.Opening) {
return updateAt(path, function(node: Tree.Node) {
node.dests = dests;
if (opening) node.opening = opening;
});
},
setShapes(shapes: Tree.Shape[], path: Tree.Path) {
return updateAt(path, function(node: Tree.Node) {
node.shapes = shapes;
});
},
setCommentAt,
deleteCommentAt,
setGlyphsAt,
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);
},
removeCeval() {
ops.updateAll(root, function(n) {
delete n.ceval;
delete n.threat;
});
},
removeComputerVariations() {
ops.mainlineNodeList(root).forEach(function(n) {
n.children = n.children.filter(function(c) {
return !c.comp;
});
});
},
parentNode,
getParentClock
};
}