UNPKG

besogo

Version:

Embeddable SGF player for the game of Go (aka Weiqi, Baduk)

522 lines (472 loc) 18.8 kB
besogo.makeEditor = function(sizeX, sizeY) { 'use strict'; // Creates an associated game state tree var root = besogo.makeGameRoot(sizeX, sizeY), current = root, // Navigation cursor listeners = [], // Listeners of general game/editor state changes // Enumeration of editor tools/modes TOOLS = ['navOnly', // read-only navigate mode 'auto', // auto-mode: navigate or auto-play color 'playB', // play black stone 'playW', // play white stone 'addB', // setup black stone 'addW', // setup white stone 'addE', // setup empty stone 'clrMark', // remove markup 'circle', // circle markup 'square', // square markup 'triangle', // triangle markup 'cross', // "X" cross markup 'block', // filled square markup 'label'], // label markup tool = 'auto', // Currently active tool (default: auto-mode) label = "1", // Next label that will be applied navHistory = [], // Navigation history gameInfo = {}, // Game info properties // Order of coordinate systems COORDS = 'none numeric western eastern pierre corner eastcor'.split(' '), coord = 'none', // Selected coordinate system // Variant style: even/odd - children/siblings, <2 - show auto markup for variants variantStyle = 0; // 0-3, 0 is default return { addListener: addListener, click: click, nextNode: nextNode, prevNode: prevNode, nextSibling: nextSibling, toggleCoordStyle: toggleCoordStyle, getCoordStyle: getCoordStyle, setCoordStyle: setCoordStyle, toggleVariantStyle: toggleVariantStyle, getVariantStyle: getVariantStyle, setVariantStyle: setVariantStyle, getGameInfo: getGameInfo, setGameInfo: setGameInfo, setComment: setComment, getTool: getTool, setTool: setTool, getLabel: getLabel, setLabel: setLabel, getVariants: getVariants, // Returns variants of current node getCurrent: getCurrent, setCurrent: setCurrent, cutCurrent: cutCurrent, promote: promote, demote: demote, getRoot: getRoot, loadRoot: loadRoot // Loads new game state }; // Returns the active tool function getTool() { return tool; } // Sets the active tool, returns false if failed function setTool(set) { // Toggle label mode if already label tool already selected if (set === 'label' && set === tool) { if ( /^-?\d+$/.test(label) ) { // If current label is integer setLabel('A'); // Toggle to characters } else { setLabel('1'); // Toggle back to numbers } return true; // Notification already handled by setLabel } // Set the tool only if in list and actually changed if (TOOLS.indexOf(set) !== -1 && tool !== set) { tool = set; notifyListeners({ tool: tool, label: label }); // Notify tool change return true; } return false; } // Gets the next label to apply function getLabel() { return label; } // Sets the next label to apply and sets active tool to label function setLabel(set) { if (typeof set === 'string') { set = set.replace(/\s/g, ' ').trim(); // Convert all whitespace to space and trim label = set || "1"; // Default to "1" if empty string tool = 'label'; // Also change current tool to label notifyListeners({ tool: tool, label: label }); // Notify tool/label change } } // Toggle the coordinate style function toggleCoordStyle() { coord = COORDS[(COORDS.indexOf(coord) + 1) % COORDS.length]; notifyListeners({ coord: coord }); } // Gets the current coordinate style function getCoordStyle() { return coord; } // Sets the coordinate system style function setCoordStyle(setCoord) { if (besogo.coord[setCoord]) { coord = setCoord; notifyListeners({ coord: setCoord }); } } // Toggles the style for showing variants function toggleVariantStyle(toggleShow) { var childStyle = variantStyle % 2, // 0: children, 1: siblings showStyle = variantStyle - childStyle; // 0: show auto-markup, 2: hide if (toggleShow) { // Truthy input toggles showing of auto-markup showStyle = (showStyle + 2) % 4; // 0 => 2 or 2 => 0 } else { // Falsy input toggles child vs sibling style childStyle = (childStyle + 1) % 2; // 0 => 1 or 1 => 0 } variantStyle = childStyle + showStyle; notifyListeners({ variantStyle: variantStyle, markupChange: true }); } // Returns the variant style function getVariantStyle() { return variantStyle; } // Directly sets the variant style function setVariantStyle(style) { if (style === 0 || style === 1 || style === 2 || style === 3) { variantStyle = style; notifyListeners({ variantStyle: variantStyle, markupChange: true }); } } function getGameInfo() { return gameInfo; } function setGameInfo(info, id) { if (id) { gameInfo[id] = info; } else { gameInfo = info; } notifyListeners({ gameInfo: gameInfo }); } function setComment(text) { text = text.trim(); // Trim whitespace and standardize line breaks text = text.replace(/\r\n/g,'\n').replace(/\n\r/g,'\n').replace(/\r/g,'\n'); text.replace(/\f\t\v\u0085\u00a0/g,' '); // Convert other whitespace to space current.comment = text; notifyListeners({ comment: text }); } // Returns variants of the current node according to the set style function getVariants() { if (variantStyle >= 2) { // Do not show variants if style >= 2 return []; } if (variantStyle === 1) { // Display sibling variants // Root node does not have parent nor siblings return current.parent ? current.parent.children : []; } return current.children; // Otherwise, style must be 0, display child variants } // Returns the currently active node in the game state tree function getCurrent() { return current; } // Returns the root of the game state tree function getRoot() { return root; } function loadRoot(load) { root = load; current = load; notifyListeners({ treeChange: true, navChange: true, stoneChange: true }); } // Navigates forward num nodes (to the end if num === -1) function nextNode(num) { if (current.children.length === 0) { // Check if no children return false; // Do nothing if no children (avoid notification) } while (current.children.length > 0 && num !== 0) { if (navHistory.length) { // Non-empty navigation history current = navHistory.pop(); } else { // Empty navigation history current = current.children[0]; // Go to first child } num--; } // Notify listeners of navigation (with no tree edits) notifyListeners({ navChange: true }, true); // Preserve history } // Navigates backward num nodes (to the root if num === -1) function prevNode(num) { if (current.parent === null) { // Check if root return false; // Do nothing if already at root (avoid notification) } while (current.parent && num !== 0) { navHistory.push(current); // Save current into navigation history current = current.parent; num--; } // Notify listeners of navigation (with no tree edits) notifyListeners({ navChange: true }, true); // Preserve history } // Cyclically switches through siblings function nextSibling(change) { var siblings, i = 0; if (current.parent) { siblings = current.parent.children; // Exit early if only child if (siblings.length === 1) { return; } // Find index of current amongst siblings i = siblings.indexOf(current); // Apply change cyclically i = (i + change) % siblings.length; if (i < 0) { i += siblings.length; } current = siblings[i]; // Notify listeners of navigation (with no tree edits) notifyListeners({ navChange: true }); } } // Sets the current node function setCurrent(node) { if (current !== node) { current = node; // Notify listeners of navigation (with no tree edits) notifyListeners({ navChange: true }); } } // Removes current branch from the tree function cutCurrent() { var parent = current.parent; if (tool === 'navOnly') { return; // Tree editing disabled in navOnly mode } if (parent) { if (confirm("Delete this branch?") === true) { parent.removeChild(current); current = parent; // Notify navigation and tree edited notifyListeners({ treeChange: true, navChange: true }); } } } // Raises current variation to a higher precedence function promote() { if (tool === 'navOnly') { return; // Tree editing disabled in navOnly mode } if (current.parent && current.parent.promote(current)) { notifyListeners({ treeChange: true }); // Notify tree edited } } // Drops current variation to a lower precedence function demote() { if (tool === 'navOnly') { return; // Tree editing disabled in navOnly mode } if (current.parent && current.parent.demote(current)) { notifyListeners({ treeChange: true }); // Notify tree edited } } // Handle click with application of selected tool function click(i, j, ctrlKey, shiftKey) { switch(tool) { case 'navOnly': navigate(i, j, shiftKey); break; case 'auto': if (!navigate(i, j, shiftKey) && !shiftKey) { // Try to navigate to (i, j) playMove(i, j, 0, ctrlKey); // Play auto-color move if navigate fails } break; case 'playB': playMove(i, j, -1, ctrlKey); // Black move break; case 'playW': playMove(i, j, 1, ctrlKey); // White move break; case 'addB': if (ctrlKey) { playMove(i, j, -1, true); // Play black } else { placeSetup(i, j, -1); // Set black } break; case 'addW': if (ctrlKey) { playMove(i, j, 1, true); // Play white } else { placeSetup(i, j, 1); // Set white } break; case 'addE': placeSetup(i, j, 0); break; case 'clrMark': setMarkup(i, j, 0); break; case 'circle': setMarkup(i, j, 1); break; case 'square': setMarkup(i, j, 2); break; case 'triangle': setMarkup(i, j, 3); break; case 'cross': setMarkup(i, j, 4); break; case 'block': setMarkup(i, j, 5); break; case 'label': setMarkup(i, j, label); break; } } // Navigates to child with move at (x, y), searching tree if shift key pressed // Returns true is successful, false if not function navigate(x, y, shiftKey) { var i, move, children = current.children; // Look for move across children for (i = 0; i < children.length; i++) { move = children[i].move; if (shiftKey) { // Search for move in branch if (jumpToMove(x, y, children[i])) { return true; } } else if (move && move.x === x && move.y === y) { current = children[i]; // Navigate to child if found notifyListeners({ navChange: true }); // Notify navigation (with no tree edits) return true; } } if (shiftKey && jumpToMove(x, y, root, current)) { return true; } return false; } // Recursive function for jumping to move with depth-first search function jumpToMove(x, y, start, end) { var i, move, children = start.children; if (end && end === start) { return false; } move = start.move; if (move && move.x === x && move.y === y) { current = start; notifyListeners({ navChange: true }); // Notify navigation (with no tree edits) return true; } for (i = 0; i < children.length; i++) { if (jumpToMove(x, y, children[i], end)) { return true; } } return false; } // Plays a move at the given color and location // Set allowAll to truthy to allow illegal moves function playMove(i, j, color, allowAll) { var next; // Check if current node is immutable or root if ( !current.isMutable('move') || !current.parent ) { next = current.makeChild(); // Create a new child node if (next.playMove(i, j, color, allowAll)) { // Play in new node // Keep (add to game state tree) only if move succeeds current.addChild(next); current = next; // Notify tree change, navigation, and stone change notifyListeners({ treeChange: true, navChange: true, stoneChange: true }); } // Current node is mutable and not root } else if(current.playMove(i, j, color, allowAll)) { // Play in current // Only need to update if move succeeds notifyListeners({ stoneChange: true }); // Stones changed } } // Places a setup stone at the given color and location function placeSetup(i, j, color) { var next; if (color === current.getStone(i, j)) { // Compare setup to current if (color !== 0) { color = 0; // Same as current indicates removal desired } else { // Color and current are both empty return; // No change if attempting to set empty to empty } } // Check if current node can accept setup stones if (!current.isMutable('setup')) { next = current.makeChild(); // Create a new child node if (next.placeSetup(i, j, color)) { // Place setup stone in new node // Keep (add to game state tree) only if change occurs current.addChild(next); current = next; // Notify tree change, navigation, and stone change notifyListeners({ treeChange: true, navChange: true, stoneChange: true }); } } else if(current.placeSetup(i, j, color)) { // Try setup in current // Only need to update if change occurs notifyListeners({ stoneChange: true }); // Stones changed } } // Sets the markup at the given location and place function setMarkup(i, j, mark) { var temp; // For label incrementing if (mark === current.getMarkup(i, j)) { // Compare mark to current if (mark !== 0) { mark = 0; // Same as current indicates removal desired } else { // Mark and current are both empty return; // No change if attempting to set empty to empty } } if (current.addMarkup(i, j, mark)) { // Try to add the markup if (typeof mark === 'string') { // If markup is a label, increment the label if (/^-?\d+$/.test(mark)) { // Integer number label temp = +mark; // Convert to number // Increment and convert back to string setLabel( "" + (temp + 1) ); } else if ( /[A-Za-z]$/.test(mark) ) { // Ends with [A-Za-z] // Get the last character in the label temp = mark.charAt(mark.length - 1); if (temp === 'z') { // Cyclical increment temp = 'A'; // Move onto uppercase letters } else if (temp === 'Z') { temp = 'a'; // Move onto lowercase letters } else { temp = String.fromCharCode(temp.charCodeAt() + 1); } // Replace last character of label with incremented char setLabel( mark.slice(0, mark.length - 1) + temp ); } } notifyListeners({ markupChange: true }); // Notify markup change } } // Adds a listener (by call back func) that will be notified on game/editor state changes function addListener(listener) { listeners.push(listener); } // Notify listeners with the given message object // Data sent to listeners: // tool: changed tool selection // label: changed next label // coord: changed coordinate system // variantStyle: changed variant style // gameInfo: changed game info // comment: changed comment in current node // Flags sent to listeners: // treeChange: nodes added or removed from tree // navChange: current switched to different node // stoneChange: stones modified in current node // markupChange: markup modified in current node function notifyListeners(msg, keepHistory) { var i; if (!keepHistory && msg.navChange) { navHistory = []; // Clear navigation history } for (i = 0; i < listeners.length; i++) { listeners[i](msg); } } };