UNPKG

@reis/seki

Version:

Seki – A modern javascript based Go board renderer and player, that is simple to use, extensible, compact and intuitive.

959 lines (818 loc) 18.2 kB
/** * This class represents a single node in the game moves tree. It contains * properties like the x and y grid coordinates, the move played, board setup * instructions, markup, player turn and comments. The moves tree in the game * record is represented by a string of GameNodes, each with pointers to their * parent and children. Each node can have multiple children (move variations), * but only one parent. */ export default class GameNode { //Parent and children root parent children = [] variationRoot //The selected path index (for navigating variations) index = 0 isPath = false /** * Constructor */ constructor(data) { if (data) { for (const key in data) { this[key] = data[key] } } //Root node is ourselves unless we get attached to a parent this.root = this } /** * Get root node */ getRoot() { return this.root } /** * Check if we're the root node */ isRoot() { return this === this.root } /************************************************************************** * Child and parent controls ***/ /** * Check if the node has any chilren */ hasChildren() { return (this.children.length > 0) } /** * Check if the node has more than one child */ hasMultipleChildren() { return (this.children.length > 1) } /** * Get all the children */ getChildren() { return this.children } /** * Set all the children */ setChildren(children) { this.children = children this.children.forEach(child => child.setParent(this)) } /** * Get node's child specified by index */ getChild(i = 0) { return this.children[i] } /** * Find the index of a child node */ indexOf(child) { return this.children.indexOf(child) } /** * Add a child (returns the index of the newly added child) */ addChild(child) { child.detachFromParent() this.children.push(child) child.setParent(this) return this.children.length - 1 } /** * Remove a child node */ removeChild(child) { const i = this.indexOf(child) this.removeChildAtIndex(i) } /** * Remove a child node at given index */ removeChildAtIndex(i) { //Invalid index if (i === -1 || !this.children[i]) { return } //Get the child, remove its parent const child = this.children[i] child.removeParent() //Remove it from our children array, and if we removed the main variation //node, update the variation root of the new main variation node. THe rest //of the child nodes remain unaffected. this.children.splice(i, 1) if (i === 0 && this.children.length > 0) { this.children[0].updateVariationRoot() } } /** * Move a child to a different index */ moveChild(child, newIndex) { //Validate index const {children} = this if (newIndex < 0 || newIndex >= children.length) { return } //Not found const currentIndex = this.indexOf(child) if (currentIndex === -1) { return } //Get const existing = children[newIndex] //Swap children[newIndex] = child children[currentIndex] = existing //Reparent children to ensure variation roots are correct children.forEach(child => child.setParent(this)) } /** * Move a child one position up */ moveChildUp(child) { //Move child if index valid const currentIndex = this.indexOf(child) if (currentIndex > 0) { this.moveChild(child, currentIndex - 1) } } /** * Move a child one position down */ moveChildDown(child) { //Move child if index valid const {children} = this const currentIndex = this.indexOf(child) if (currentIndex > -1 && currentIndex < children.length - 1) { this.moveChild(child, currentIndex + 1) } } /** * Update the variation root */ updateVariationRoot() { //Get parent and current variation root const {parent} = this //Update as needed if (parent && parent.variationRoot) { this.variationRoot = parent.variationRoot } else if (parent && parent.indexOf(this) > 0) { this.variationRoot = this } else { this.variationRoot = null } //Update for all our children this.children.forEach(child => child.updateVariationRoot()) } /** * Check if the node has a parent */ hasParent() { return Boolean(this.parent) } /** * Get parent node */ getParent() { return this.parent } /** * Set parent node */ setParent(parent) { this.parent = parent this.root = parent.root this.updateVariationRoot() } /** * Remove parent */ removeParent() { this.parent = null this.root = this this.updateVariationRoot() } /** * Siblings */ hasSiblings() { const {parent} = this return (parent && parent.hasMultipleChildren()) } /** * Get next sibling */ getNextSibling() { const {parent} = this if (parent) { const i = parent.indexOf(this) return parent.getChild(i + 1) } } /** * Get previous sibling */ getPreviousSibling() { const {parent} = this if (parent) { const i = parent.indexOf(this) return parent.getChild(i - 1) } } /***************************************************************************** * Node manipulation ***/ /** * Detach this node from its parent */ detachFromParent() { const {parent} = this if (parent) { parent.removeChild(this) return parent } } /** * Append this node to a parent node */ appendToParent(node) { node.addChild(this) return node.indexOf(this) } /** * Append child node to this node */ appendChild(node) { this.addChild(node) return this.indexOf(node) } /** * Move to node to a specific index in the parent's child tree */ moveToIndex(index) { if (this.parent) { this.parent.moveChild(this, index) } } /** * Move the node up in the parent's child tree */ moveUp() { if (this.parent) { this.parent.moveChildUp(this) } } /** * Move the node down in the parent's child tree */ moveDown() { if (this.parent) { this.parent.moveChildDown(this) } } /************************************************************************** * Move handling ***/ /** * Get move number */ getMoveNumber() { const {parent, move} = this const n = move ? 1 : 0 const p = parent ? parent.getMoveNumber() : 0 return n + p } /** * Check if we're the variation root */ isVariationRoot() { return this === this.variationRoot } /** * Check if we're on a variation branch */ isVariationBranch() { return Boolean(this.variationRoot) } /** * Check if we're the main variation */ isMainVariation() { return !this.isVariationBranch() } /** * Get array of move nodes up to the variation node */ getVariationMoveNodes(arr = []) { const {parent, variationRoot} = this if (variationRoot && this.isMove()) { arr.unshift(this) } if (parent) { return parent.getVariationMoveNodes(arr) } return arr } /** * Get array of move nodes up to the root node */ getAllMoveNodes(arr = []) { const {parent} = this if (this.isMove()) { arr.unshift(this) } if (parent) { return parent.getAllMoveNodes(arr) } return arr } /** * Check if this node has move instructions */ hasMoveInstructions() { const {move} = this return Boolean(move) } /** * Check if we have turn instructions */ hasTurnInstructions() { return (typeof this.turn !== 'undefined') } /** * Check if this is a move node (alias) */ isMove() { return this.hasMoveInstructions() } /** * Check if this is a non-pass move */ isPlayMove() { const {move} = this return Boolean(move && !move.pass) } /** * Check if this is a pass move */ isPassMove() { const {move} = this return Boolean(move && move.pass) } /** * Get the color of the current move */ getMoveColor() { const {move} = this if (move) { return move.color } } /** * Check if we have a move at given coordinates */ hasMove(x, y) { const {move} = this return Boolean(move && move.x === x && move.y === y) } /** * Get previous move node */ getPreviousMove() { const {parent} = this if (parent) { if (parent.isMove()) { return parent } return parent.getPreviousMove() } } /** * Check if there is a move variation at the given coordinates */ hasMoveVariation(x, y) { const {children} = this return children .some(child => child.hasMove(x, y)) } /** * Check if given coordinates are from one of the next child move nodes */ isMoveVariation(x, y) { const {children} = this return children .some(child => child.hasMove(x, y)) } /** * Get the move child node index for the given coordinates */ getMoveVariationIndex(x, y) { const {children} = this return children .findIndex(child => child.hasMove(x, y)) } /** * Check if the node has any move variation */ hasMoveVariations() { const {children} = this return children .filter(child => child.isMove()) .length > 0 } /** * Check if the node has more than one move variation */ hasMultipleMoveVariations() { const {children} = this return children .filter(child => child.isMove()) .length > 1 } /** * Get all the child nodes that are moves */ getMoveVariations() { const {children} = this return children .filter(child => child.isMove()) } /** * Find a named node in ourselves or our children */ findNamedNode(name, path) { //That's us! if (this.name === name) { return this } //Get children const {children} = this //Check all child variations for (let i = 0; i < children.length; i++) { //Advance path if (path) { path.advance(i) } //Find child const child = children[i].findNamedNode(name, path) if (child) { return child } //Otherwise retreat path if (path) { path.retreat() } } } /** * Find a given node in ourselves or our children */ findNode(target, path) { //That's us! if (this === target) { return this } //Get children const {children} = this //Check all child variations for (let i = 0; i < children.length; i++) { //Advance path if (path) { path.advance(i) } //Find child const child = children[i].findNode(target, path) if (child) { return child } //Otherwise retreat path if (path) { path.retreat() } } } /** * Check if we have comments in this node */ hasComments() { const {comments} = this return (comments && comments.length > 0) } /** * Get comments */ getComments() { return this.comments || [] } /** * Set comments */ setComments(comments) { if (!Array.isArray(comments)) { comments = comments ? [comments] : [] } this.comments = comments } /************************************************************************** * Path helpers ***/ /** * Set the path index if valid */ setPathIndex(i) { if (this.isValidPathIndex(i)) { this.index = i } } /** * Get active path index */ getPathIndex() { return this.index } /** * Increment path index */ incrementPathIndex() { const {children, index} = this const next = index + 1 if (next < children.length && children[next]) { this.index = next } } /** * Decrement path index */ decrementPathIndex() { const {children, index} = this const prev = index - 1 if (prev >= 0 && children[prev]) { this.index = prev } } /** * Check if an index is a valid path index */ isValidPathIndex(i) { const {children} = this if (typeof i === 'undefined' || i < 0) { return false } return (typeof children[i] !== 'undefined') } /** * Is main path */ isMainPath() { return (this.index === 0) } /** * Set the path to point to the given node */ setPathNode(child) { const i = this.indexOf(child) this.setPathIndex(i) } /** * Get node on path index */ getPathNode() { const {children, index} = this return children[index] } /** * Get array of all path nodes from this node onwards */ getPathNodes() { const nodes = [] let node = this while (node) { nodes.push(node) node = node.getPathNode() } return nodes } /** * Make this node the path node on its parent */ setAsParentPathNode() { const {parent} = this if (parent) { parent.setPathNode(this) } } /** * Check if a node is the selected path */ isSelectedPath(child) { const {children, index} = this return (child === children[index]) } /** * Mark path along whole nodes tree */ markPath(isPath = true) { const {children, index} = this this.isPath = isPath children.forEach((child, i) => child.markPath(isPath && i === index)) } /************************************************************************** * Markup helpers ***/ /** * Add markup */ addMarkup(x, y, data) { //Remove existing markup on same coords first this.removeMarkup(x, y) //No markup instructions container in this node? if (typeof this.markup === 'undefined') { this.markup = [] } //Find entry for this type const {markup} = this const {type, text} = data const entry = markup.find(entry => entry.type === type) //No entry yet? if (!entry) { markup.push({ type, coords: [{x, y, text}], }) return } //Add to existing entry entry.coords.push({x, y, text}) } /** * Remove markup */ removeMarkup(x, y) { //No markup instructions container in this node? const {markup} = this if (typeof markup === 'undefined') { return } //Go over markup and remove markup.forEach(entry => { const i = entry.coords.findIndex(coord => coord.x === x && coord.y === y) if (i !== -1) { entry.coords.splice(i, 1) } }) //Remove empty entries this.markup = markup.filter(entry => entry.coords.length > 0) } /** * Check if we have markup for certain coords in this node */ hasMarkup(x, y) { //No markup instructions container in this node? const {markup} = this if (typeof markup === 'undefined') { return } //Go over markup and check return markup .some(entry => entry.coords .some(coord => coord.x === x && coord.y === y)) } /** * Check if we have markup instructions in this node */ hasMarkupInstructions() { return Array.isArray(this.markup) } /** * Remove all markup instructions */ removeAllMarkupInstructions() { delete this.markup } /************************************************************************** * Setup helpers ***/ /** * Add setup instruction */ addSetup(x, y, data) { //Remove existing setup on same coords first this.removeSetup(x, y) //No setup instructions container in this node? if (typeof this.setup === 'undefined') { this.setup = [] } //Is this a move node? if (this.isMove()) { //Create a new node const node = new GameNode() const i = node.appendToParent(this) //Add the setup instruction to this node node.addSetup(x, y, data) //Return the newly created node index return i } //Find entry for this type const {setup} = this const {type} = data const entry = setup.find(entry => entry.type === type) //No entry yet? if (!entry) { setup.push({ type, coords: [{x, y}], }) return } //Add to existing entry entry.coords.push({x, y}) } /** * Remove setup */ removeSetup(x, y) { //No setup instructions container in this node? const {setup} = this if (typeof setup === 'undefined') { return } //Go over setup and remove setup.forEach(entry => { const i = entry.coords.findIndex(coord => coord.x === x && coord.y === y) if (i !== -1) { entry.coords.splice(i, 1) } }) //Remove empty entries this.setup = setup.filter(entry => entry.coords.length > 0) } /** * Check if we have setup for certain coords in this node */ hasSetup(x, y) { //No setup instructions container in this node? const {setup} = this if (typeof setup === 'undefined') { return } //Go over setup and check return setup .some(entry => entry.coords .some(coord => coord.x === x && coord.y === y)) } /** * Check if we have setup instructions in this node */ hasSetupInstructions() { return Array.isArray(this.setup) } /************************************************************************** * Line helpers ***/ /** * Add line */ addLine(...args) { this.lines ??= [] this.lines.push(args) } /** * Remove lines */ removeLines() { delete this.lines } /** * Has lines */ hasLines() { return this.lines?.length > 0 } /************************************************************************** * Instruction checks ***/ /** * Check if there are any instructions in this node */ hasInstructions() { return ( this.hasMoveInstructions() || this.hasTurnInstructions() || this.hasSetupInstructions() || this.hasMarkupInstructions() || this.hasComments() ) } }