280 lines
8.8 KiB
TypeScript
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
|
|
};
|
|
}
|