UNPKG

wgo

Version:

JavaScript library for game of Go

1,304 lines (1,288 loc) 195 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WGo = {})); }(this, (function (exports) { 'use strict'; /** * Enumeration representing stone color, can be used for representing board position. */ (function (Color) { Color[Color["BLACK"] = 1] = "BLACK"; Color[Color["B"] = 1] = "B"; Color[Color["WHITE"] = -1] = "WHITE"; Color[Color["W"] = -1] = "W"; Color[Color["EMPTY"] = 0] = "EMPTY"; Color[Color["E"] = 0] = "E"; })(exports.Color || (exports.Color = {})); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } function __spread() { for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); return ar; } /** * Class for syntax errors in SGF string. * @ extends Error */ var SGFSyntaxError = /** @class */ (function (_super) { __extends(SGFSyntaxError, _super); function SGFSyntaxError(message, parser) { var _newTarget = this.constructor; var _this = _super.call(this, message) || this; _this.__proto__ = _newTarget.prototype; // var tempError = Error.apply(this); _this.name = _this.name = 'SGFSyntaxError'; _this.message = message || 'There was an unspecified syntax error in the SGF'; if (parser) { _this.message += " on line " + parser.lineNo + ", char " + parser.charNo + ":\n"; _this.message += "\t" + parser.sgfString.split('\n')[parser.lineNo - 1] + "\n"; _this.message += "\t" + Array(parser.charNo + 1).join(' ') + "^"; } return _this; } return SGFSyntaxError; }(Error)); /** * Contains methods for parsing sgf string. * @module SGFParser */ var CODE_A = 'A'.charCodeAt(0); var CODE_Z = 'Z'.charCodeAt(0); var CODE_WHITE_CHAR = ' '.charCodeAt(0); function isCharUCLetter(char) { if (!char) { return false; } var charCode = char.charCodeAt(0); return charCode >= CODE_A && charCode <= CODE_Z; } /** * Class for parsing of sgf files. Can be used for parsing of SGF fragments as well. */ var SGFParser = /** @class */ (function () { /** * Creates new instance of SGF parser with SGF loaded ready to be parsed. * @param sgf string to parse. */ function SGFParser(sgf) { /** Current character position */ this.position = 0; /** Current line number */ this.lineNo = 1; /** Current char number (on the line) */ this.charNo = 0; this.sgfString = sgf; } /** * Returns current significant character (ignoring whitespace characters). * If there is end of string, return undefined. */ SGFParser.prototype.currentChar = function () { while (this.sgfString.charCodeAt(this.position) <= CODE_WHITE_CHAR) { // While the character is a whitespace, increase position pointer and line and column numbers. this.nextChar(); } return this.sgfString[this.position]; }; /** * Move pointer to next character and return it (including whitespace). */ SGFParser.prototype.nextChar = function () { if (this.sgfString[this.position] === '\n') { this.charNo = 0; this.lineNo++; } else { this.charNo++; } this.position++; return this.sgfString[this.position]; }; /** * Reads current significant character and if it isn't equal with the argument, throws an error. * Then move pointer to next character. */ SGFParser.prototype.processChar = function (char) { if (this.currentChar() !== char) { throw new SGFSyntaxError("Unexpected character " + this.currentChar() + ". Character " + char + " was expected.", this); } return this.nextChar(); }; /** * Parse SGF property value - `"[" CValueType "]"`. * @param optional */ SGFParser.prototype.parsePropertyValue = function (optional) { if (optional && this.currentChar() !== '[') { return; } var value = ''; // process "[" and read first char var char = this.processChar('['); while (char !== ']') { if (!char) { // char mustn't be undefined throw new SGFSyntaxError('End of SGF inside of property', this); } else if (char === '\\') { // if there is character '\' save next character char = this.nextChar(); if (!char) { // char have to exist of course throw new SGFSyntaxError('End of SGF inside of property', this); } else if (char === '\n') { // ignore new line, otherwise save continue; } } // save the character value += char; // and move to next one char = this.nextChar(); } this.processChar(']'); return value; }; /** * Reads the property identifiers (One or more UC letters) - `UcLetter { UcLetter }`. */ SGFParser.prototype.parsePropertyIdent = function () { var ident = ''; // Read current significant character var char = this.currentChar(); if (!isCharUCLetter(char)) { throw new SGFSyntaxError('Property identifier must consists from upper case letters.', this); } ident += char; while (char = this.nextChar()) { if (!isCharUCLetter(char)) { break; } ident += char; } return ident; }; /** * Parses sequence of property values - `PropValue { PropValue }`. */ SGFParser.prototype.parsePropertyValues = function () { var values = []; var value = this.parsePropertyValue(); if (value) { values.push(value); } while (value = this.parsePropertyValue(true)) { values.push(value); } return values; }; /** * Parses a SGF property - `PropIdent PropValue { PropValue }`. */ SGFParser.prototype.parseProperty = function () { if (!isCharUCLetter(this.currentChar())) { return; } return [this.parsePropertyIdent(), this.parsePropertyValues()]; }; /** * Parses a SGF node - `";" { Property }`. */ SGFParser.prototype.parseNode = function () { this.processChar(';'); var properties = {}; var property; while (property = this.parseProperty()) { properties[property[0]] = property[1]; } return properties; }; /** * Parses a SGF Sequence - `Node { Node }`. */ SGFParser.prototype.parseSequence = function () { var sequence = []; sequence.push(this.parseNode()); while (this.currentChar() === ';') { sequence.push(this.parseNode()); } return sequence; }; /** * Parses a SGF *GameTree* - `"(" Sequence { GameTree } ")"`. */ SGFParser.prototype.parseGameTree = function () { this.processChar('('); var sequence = this.parseSequence(); var children = []; if (this.currentChar() === '(') { children = this.parseCollection(); } this.processChar(')'); return { sequence: sequence, children: children }; }; /** * Parses a SGF *Collection* - `Collection = GameTree { GameTree }`. This is the main method for parsing SGF file. */ SGFParser.prototype.parseCollection = function () { var gameTrees = []; gameTrees.push(this.parseGameTree()); while (this.currentChar() === '(') { gameTrees.push(this.parseGameTree()); } return gameTrees; }; return SGFParser; }()); /** * From SGF specification, there are these types of property values: * * CValueType = (ValueType | *Compose*) * ValueType = (*None* | *Number* | *Real* | *Double* | *Color* | *SimpleText* | *Text* | *Point* | *Move* | *Stone*) * * WGo's kifu node (KNode object) implements similar types with few exceptions: * * - Types `Number`, `Real` and `Double` are implemented by javascript's `number`. * - Types `SimpleText` and `Text` are considered as the same. * - Types `Point`, `Move` and `Stone` are all the same, implemented as simple object with `x` and `y` coordinates. * - Type `None` is implemented as `true` * * Each `Compose` type, which is used in SGF, has its own type. * * - `Point ':' Point` (used in AR property) has special type `Line` - object with two sets of coordinates. * - `Point ':' Simpletext` (used in LB property) has special type `Label` - object with coordinates and text property * - `Simpletext ":" Simpletext` (used in AP property) - not implemented * - `Number ":" SimpleText` (used in FG property) - not implemented * * Moreover each property value has these settings: * * - *Single value* / *Array* (more values) * - *Not empty* / *Empty* (value or array can be empty) * * {@link http://www.red-bean.com/sgf/sgf4.html} */ var NONE = { read: function (str) { return true; }, write: function (value) { return ''; }, }; var NUMBER = { read: function (str) { return parseFloat(str); }, write: function (value) { return value.toString(10); }, }; var TEXT = { read: function (str) { return str; }, write: function (value) { return value; }, }; var COLOR = { read: function (str) { return (str === 'w' || str === 'W' ? exports.Color.WHITE : exports.Color.BLACK); }, write: function (value) { return (value === exports.Color.WHITE ? 'W' : 'B'); }, }; var POINT = { read: function (str) { return str ? { x: str.charCodeAt(0) - 97, y: str.charCodeAt(1) - 97, } : null; }, write: function (value) { return value ? String.fromCharCode(value.x + 97) + String.fromCharCode(value.y + 97) : ''; }, }; var LABEL = { read: function (str) { return ({ x: str.charCodeAt(0) - 97, y: str.charCodeAt(1) - 97, text: str.substr(3), }); }, write: function (value) { return (String.fromCharCode(value.x + 97) + String.fromCharCode(value.y + 97) + ":" + value.text); }, }; var VECTOR = { read: function (str) { return str ? [ { x: str.charCodeAt(0) - 97, y: str.charCodeAt(1) - 97, }, { x: str.charCodeAt(3) - 97, y: str.charCodeAt(4) - 97, }, ] : null; }, write: function (value) { return ( // tslint:disable-next-line:max-line-length value ? String.fromCharCode(value[0].x + 97) + String.fromCharCode(value[0].y + 97) + ":" + (String.fromCharCode(value[1].x + 97) + String.fromCharCode(value[1].y + 97)) : ''); }, }; var propertyValueTypes = { _default: { transformer: TEXT, multiple: false, notEmpty: true, }, }; /// Move properties ------------------------------------------------------------------------------- propertyValueTypes.B = propertyValueTypes.W = { transformer: POINT, multiple: false, notEmpty: false, }; propertyValueTypes.KO = { transformer: NONE, multiple: false, notEmpty: false, }; propertyValueTypes.MN = { transformer: NUMBER, multiple: false, notEmpty: true, }; /// Setup properties ------------------------------------------------------------------------------ propertyValueTypes.AB = propertyValueTypes.AW = propertyValueTypes.AE = { transformer: POINT, multiple: true, notEmpty: true, }; propertyValueTypes.PL = { transformer: COLOR, multiple: false, notEmpty: true, }; /// Node annotation properties -------------------------------------------------------------------- propertyValueTypes.C = propertyValueTypes.N = { transformer: TEXT, multiple: false, notEmpty: true, }; // tslint:disable-next-line:max-line-length propertyValueTypes.DM = propertyValueTypes.GB = propertyValueTypes.GW = propertyValueTypes.HO = propertyValueTypes.UC = propertyValueTypes.V = { transformer: NUMBER, multiple: false, notEmpty: true, }; /// Move annotation properties -------------------------------------------------------------------- propertyValueTypes.BM = propertyValueTypes.TE = { transformer: NUMBER, multiple: false, notEmpty: true, }; propertyValueTypes.DO = propertyValueTypes.IT = { transformer: NONE, multiple: false, notEmpty: false, }; /// Markup properties ----------------------------------------------------------------------------- // tslint:disable-next-line:max-line-length propertyValueTypes.CR = propertyValueTypes.MA = propertyValueTypes.SL = propertyValueTypes.SQ = propertyValueTypes.TR = { transformer: POINT, multiple: true, notEmpty: true, }; propertyValueTypes.LB = { transformer: LABEL, multiple: true, notEmpty: true, }; propertyValueTypes.AR = propertyValueTypes.LN = { transformer: VECTOR, multiple: true, notEmpty: true, }; propertyValueTypes.DD = propertyValueTypes.TB = propertyValueTypes.TW = { transformer: POINT, multiple: true, notEmpty: false, }; /// Root properties ------------------------------------------------------------------------------- propertyValueTypes.AP = propertyValueTypes.CA = { transformer: TEXT, multiple: false, notEmpty: true, }; // note: rectangular board is not implemented (in SZ property) propertyValueTypes.FF = propertyValueTypes.GM = propertyValueTypes.ST = propertyValueTypes.SZ = { transformer: NUMBER, multiple: false, notEmpty: true, }; /// Game info properties -------------------------------------------------------------------------- propertyValueTypes.AN = propertyValueTypes.BR = propertyValueTypes.BT = propertyValueTypes.CP = propertyValueTypes.DT = propertyValueTypes.EV = propertyValueTypes.GN = propertyValueTypes.GC = propertyValueTypes.GN = propertyValueTypes.ON = propertyValueTypes.OT = propertyValueTypes.PB = propertyValueTypes.PC = propertyValueTypes.PW = propertyValueTypes.RE = propertyValueTypes.RO = propertyValueTypes.RU = propertyValueTypes.SO = propertyValueTypes.US = propertyValueTypes.WR = propertyValueTypes.WT = { transformer: TEXT, multiple: false, notEmpty: true, }; propertyValueTypes.TM = propertyValueTypes.HA = propertyValueTypes.KM = { transformer: NUMBER, multiple: false, notEmpty: true, }; /// Timing properties ----------------------------------------------------------------------------- propertyValueTypes.BL = propertyValueTypes.WL = propertyValueTypes.OB = propertyValueTypes.OW = { transformer: NUMBER, multiple: false, notEmpty: true, }; /// Miscellaneous properties ---------------------------------------------------------------------- propertyValueTypes.PM = { transformer: NUMBER, multiple: false, notEmpty: true, }; // VW property must be specified as compressed list (ab:cd) and only one value is allowed // empty value [] will reset the viewport. Other options are not supported. propertyValueTypes.VW = { transformer: VECTOR, multiple: false, notEmpty: true, }; var processJSGF = function (gameTree, rootNode) { rootNode.setSGFProperties(gameTree.sequence[0] || {}); var lastNode = rootNode; for (var i = 1; i < gameTree.sequence.length; i++) { var node = new KifuNode(); node.setSGFProperties(gameTree.sequence[i]); lastNode.appendChild(node); lastNode = node; } for (var i = 0; i < gameTree.children.length; i++) { lastNode.appendChild(processJSGF(gameTree.children[i], new KifuNode())); } return rootNode; }; // Characters, which has to be escaped when transforming to SGF var escapeCharacters = ['\\\\', '\\]']; var escapeSGFValue = function (value) { return escapeCharacters.reduce(function (prev, current) { return prev.replace(new RegExp(current, 'g'), current); }, value); }; /** * Class representing one kifu node. */ var KifuNode = /** @class */ (function () { function KifuNode() { this.parent = null; this.children = []; this.properties = {}; } Object.defineProperty(KifuNode.prototype, "root", { get: function () { // tslint:disable-next-line:no-this-assignment var node = this; while (node.parent != null) { node = node.parent; } return node; }, enumerable: false, configurable: true }); Object.defineProperty(KifuNode.prototype, "innerSGF", { /** * Kifu node representation as sgf-like string - will contain `;`, all properties and all children. */ get: function () { var output = ';'; for (var propIdent in this.properties) { if (this.properties.hasOwnProperty(propIdent)) { output += propIdent + "[" + this.getSGFProperty(propIdent).map(escapeSGFValue).join('][') + "]"; } } if (this.children.length === 1) { return "" + output + this.children[0].innerSGF; } if (this.children.length > 1) { return this.children.reduce(function (prev, current) { return prev + "(" + current.innerSGF + ")"; }, output); } return output; }, set: function (sgf) { // clean up this.clean(); var transformedSgf = sgf; // create regular SGF from sgf-like string if (transformedSgf[0] !== '(') { if (transformedSgf[0] !== ';') { transformedSgf = ";" + transformedSgf; } transformedSgf = "(" + transformedSgf + ")"; } KifuNode.fromSGF(transformedSgf, 0, this); }, enumerable: false, configurable: true }); KifuNode.prototype.getPath = function () { var path = { depth: 0, forks: [] }; // tslint:disable-next-line:no-this-assignment var node = this; while (node.parent) { path.depth++; if (node.parent.children.length > 1) { path.forks.unshift(node.parent.children.indexOf(node)); } node = node.parent; } return path; }; /// GENERAL TREE NODE MANIPULATION METHODS (subset of DOM API's Node) /** * Insert a KNode as the last child node of this node. * * @throws {Error} when argument is invalid. * @param {KifuNode} node to append. * @returns {number} position(index) of appended node. */ KifuNode.prototype.appendChild = function (node) { if (node == null || !(node instanceof KifuNode) || node === this) { throw new Error('Invalid argument passed to `appendChild` method, KNode was expected.'); } if (node.parent) { node.parent.removeChild(node); } node.parent = this; return this.children.push(node) - 1; }; /** * Returns a Boolean value indicating whether a node is a descendant of a given node or not. * * @param {KifuNode} node to be tested * @returns {boolean} true, if this node contains given node. */ KifuNode.prototype.contains = function (node) { if (this.children.indexOf(node) >= 0) { return true; } return this.children.some(function (child) { return child.contains(node); }); }; /** * Inserts the first KNode given in a parameter immediately before the second, child of this KNode. * * @throws {Error} when argument is invalid. * @param {KifuNode} newNode node to be inserted * @param {(KifuNode)} referenceNode reference node, if omitted, new node will be inserted at the end. * @returns {KifuNode} this node */ KifuNode.prototype.insertBefore = function (newNode, referenceNode) { if (newNode == null || !(newNode instanceof KifuNode) || newNode === this) { throw new Error('Invalid argument passed to `insertBefore` method, KNode was expected.'); } if (referenceNode == null) { this.appendChild(newNode); return this; } if (newNode.parent) { newNode.parent.removeChild(newNode); } newNode.parent = this; this.children.splice(this.children.indexOf(referenceNode), 0, newNode); return this; }; /** * Removes a child node from the current element, which must be a child of the current node. * * @param {KifuNode} child node to be removed * @returns {KifuNode} this node */ KifuNode.prototype.removeChild = function (child) { this.children.splice(this.children.indexOf(child), 1); child.parent = null; return this; }; /** * Replaces one child Node of the current one with the second one given in parameter. * * @throws {Error} when argument is invalid * @param {KifuNode} newChild node to be inserted * @param {KifuNode} oldChild node to be replaced * @returns {KifuNode} this node */ KifuNode.prototype.replaceChild = function (newChild, oldChild) { if (newChild == null || !(newChild instanceof KifuNode) || newChild === this) { throw new Error('Invalid argument passed to `replaceChild` method, KNode was expected.'); } this.insertBefore(newChild, oldChild); this.removeChild(oldChild); return this; }; /** * Remove all properties and children. Parent will remain. */ KifuNode.prototype.clean = function () { for (var i = this.children.length - 1; i >= 0; i--) { this.removeChild(this.children[i]); } this.properties = {}; }; /// BASIC PROPERTY GETTER and SETTER /** * Gets property by SGF property identificator. Returns property value (type depends on property type) * * @param {string} propIdent - SGF property idetificator * @returns {any} property value or values or undefined, if property is missing. */ KifuNode.prototype.getProperty = function (propIdent) { return this.properties[propIdent]; }; /** * Sets property by SGF property identificator. * * @param {string} propIdent - SGF property idetificator * @param {any} value - property value or values */ KifuNode.prototype.setProperty = function (propIdent, value) { if (value === undefined) { delete this.properties[propIdent]; } else { this.properties[propIdent] = value; } return this; }; /** * Alias for `setProperty` without second parameter. * @param propIdent */ KifuNode.prototype.removeProperty = function (propIdent) { this.setProperty(propIdent); }; /** * Iterates through all properties. */ KifuNode.prototype.forEachProperty = function (callback) { var _this = this; Object.keys(this.properties).forEach(function (propIdent) { return callback(propIdent, _this.properties[propIdent]); }); }; /// SGF RAW METHODS /** * Gets one SGF property value as string (with brackets `[` and `]`). * * @param {string} propIdent SGF property identificator. * @returns {string[]} Array of SGF property values or null if there is not such property. */ KifuNode.prototype.getSGFProperty = function (propIdent) { if (this.properties[propIdent] !== undefined) { var propertyValueType_1 = propertyValueTypes[propIdent] || propertyValueTypes._default; if (propertyValueType_1.multiple) { return this.properties[propIdent].map(function (propValue) { return propertyValueType_1.transformer.write(propValue); }); } return [propertyValueType_1.transformer.write(this.properties[propIdent])]; } return null; }; /** * Sets one SGF property. * * @param {string} propIdent SGF property identificator * @param {string[]} propValues SGF property values * @returns {KifuNode} this KNode for chaining */ KifuNode.prototype.setSGFProperty = function (propIdent, propValues) { var propertyValueType = propertyValueTypes[propIdent] || propertyValueTypes._default; if (propValues === undefined) { delete this.properties[propIdent]; return this; } if (propertyValueType.multiple) { this.properties[propIdent] = propValues.map(function (val) { return propertyValueType.transformer.read(val); }); } else { this.properties[propIdent] = propertyValueType.transformer.read(propValues[0]); } return this; }; /** * Sets multiple SGF properties. * * @param {Object} properties - map with signature propIdent -> propValues. * @returns {KifuNode} this KNode for chaining */ KifuNode.prototype.setSGFProperties = function (properties) { for (var ident in properties) { if (properties.hasOwnProperty(ident)) { this.setSGFProperty(ident, properties[ident]); } } return this; }; /** * Transforms KNode object to standard SGF string. */ KifuNode.prototype.toSGF = function () { return "(" + this.innerSGF + ")"; }; /** * Deeply clones the node. If node isn't root, its predecessors won't be cloned, and the node becomes root. */ KifuNode.prototype.cloneNode = function (appendToParent) { var node = new KifuNode(); var properties = JSON.parse(JSON.stringify(this.properties)); node.properties = properties; this.children.forEach(function (child) { node.appendChild(child.cloneNode()); }); if (appendToParent && this.parent) { this.parent.appendChild(node); } return node; }; /** * Creates KNode object from SGF transformed to JavaScript object. * * @param gameTree */ KifuNode.fromJS = function (gameTree, kifuNode) { if (kifuNode === void 0) { kifuNode = new KifuNode(); } return processJSGF(gameTree, kifuNode); }; /** * Creates KNode object from SGF string. * * @param sgf * @param gameNo */ KifuNode.fromSGF = function (sgf, gameNo, kifuNode) { if (gameNo === void 0) { gameNo = 0; } if (kifuNode === void 0) { kifuNode = new KifuNode(); } var parser = new SGFParser(sgf); return KifuNode.fromJS(parser.parseCollection()[gameNo], kifuNode); }; return KifuNode; }()); /** * WGo's game engine offers to set 3 rules: * * - *checkRepeat* - one of `repeat.KO`, `repeat.ALL`, `repeat.NONE` - defines if or when a move can be repeated. * - *allowRewrite* - if set true a move can rewrite existing move (for uncommon applications) * - *allowSuicide* - if set true a suicide will be allowed (and stone will be immediately captured) * * In this module there are some common preset rule sets (Japanese, Chinese etc...). * Extend object `gameRules` if you wish to add some rule set. Names of the rules should correspond with * SGF `RU` property. */ (function (Repeating) { Repeating["KO"] = "KO"; Repeating["ALL"] = "ALL"; Repeating["NONE"] = "NONE"; })(exports.Repeating || (exports.Repeating = {})); var JAPANESE_RULES = { repeating: exports.Repeating.KO, allowRewrite: false, allowSuicide: false, komi: 6.5, }; var CHINESE_RULES = { repeating: exports.Repeating.NONE, allowRewrite: false, allowSuicide: false, komi: 7.5, }; var ING_RULES = { repeating: exports.Repeating.NONE, allowRewrite: false, allowSuicide: true, komi: 7.5, }; var NO_RULES = { repeating: exports.Repeating.ALL, allowRewrite: true, allowSuicide: true, komi: 0, }; var goRules = { Japanese: JAPANESE_RULES, GOE: ING_RULES, NZ: ING_RULES, AGA: CHINESE_RULES, Chinese: CHINESE_RULES, }; /** * Contains implementation of go position class. * @module Position */ // creates 2-dim array function createGrid(size) { var grid = []; for (var i = 0; i < size; i++) { grid.push([]); } return grid; } /** * Position class represents a state of the go game in one moment in time. It is composed from a grid containing black * and white stones, capture counts, and actual turn. It is designed to be mutable. */ var Position = /** @class */ (function () { /** * Creates instance of position object. * * @alias WGo.Position * @class * * @param {number} [size = 19] - Size of the board. */ function Position(size) { if (size === void 0) { size = 19; } /** * One dimensional array containing stones of the position. */ this.grid = []; /** * Contains numbers of stones that both players captured. * * @property {number} black - Count of white stones captured by **black**. * @property {number} white - Count of black stones captured by **white**. */ this.capCount = { black: 0, white: 0, }; /** * Who plays next move. */ this.turn = exports.Color.BLACK; this.size = size; // init grid this.clear(); } Position.prototype.isOnPosition = function (x, y) { return x >= 0 && y >= 0 && x < this.size && y < this.size; }; /** * Returns stone on the given field. * * @param {number} x - X coordinate * @param {number} y - Y coordinate * @return {Color} Color */ Position.prototype.get = function (x, y) { if (!this.isOnPosition(x, y)) { return undefined; } return this.grid[x * this.size + y]; }; /** * Sets stone on the given field. * * @param {number} x - X coordinate * @param {number} y - Y coordinate * @param {Color} c - Color */ Position.prototype.set = function (x, y, c) { if (!this.isOnPosition(x, y)) { throw new TypeError('Attempt to set field outside of position.'); } this.grid[x * this.size + y] = c; return this; }; /** * Clears the whole position (every value is set to EMPTY). */ Position.prototype.clear = function () { for (var i = 0; i < this.size * this.size; i++) { this.grid[i] = exports.Color.EMPTY; } return this; }; /** * Clones the whole position. * * @return {WGo.Position} Copy of the position. * @todo Clone turn as well. */ Position.prototype.clone = function () { var clone = new Position(this.size); clone.grid = this.grid.slice(0); clone.capCount.black = this.capCount.black; clone.capCount.white = this.capCount.white; clone.turn = this.turn; return clone; }; /** * Compares this position with another position and return object with changes * * @param {WGo.Position} position - Position to compare to. * @return {Field[]} Array of different fields */ Position.prototype.compare = function (position) { if (position.size !== this.size) { throw new TypeError('Positions of different sizes cannot be compared.'); } var diff = []; for (var i = 0; i < this.size * this.size; i++) { if (this.grid[i] !== position.grid[i]) { diff.push({ x: Math.floor(i / this.size), y: i % this.size, c: position.grid[i], }); } } return diff; }; /** * Sets stone on given coordinates and capture adjacent stones without liberties if there are any. * If move is invalid, false is returned. */ Position.prototype.applyMove = function (x, y, c, allowSuicide, allowRewrite) { if (c === void 0) { c = this.turn; } if (allowSuicide === void 0) { allowSuicide = false; } if (allowRewrite === void 0) { allowRewrite = false; } // check if move is on empty field of the board if (!(allowRewrite || this.get(x, y) === exports.Color.EMPTY)) { return false; } // clone position and add a stone var prevColor = this.get(x, y); this.set(x, y, c); // check capturing of all surrounding stones var capturesAbove = this.get(x, y - 1) === -c && this.captureIfNoLiberties(x, y - 1); var capturesRight = this.get(x + 1, y) === -c && this.captureIfNoLiberties(x + 1, y); var capturesBelow = this.get(x, y + 1) === -c && this.captureIfNoLiberties(x, y + 1); var capturesLeft = this.get(x - 1, y) === -c && this.captureIfNoLiberties(x - 1, y); var hasCaptured = capturesAbove || capturesRight || capturesBelow || capturesLeft; // check suicide if (!hasCaptured) { if (!this.hasLiberties(x, y)) { if (allowSuicide) { this.capture(x, y, c); } else { // revert position this.set(x, y, prevColor); return false; } } } this.turn = -c; return true; }; /** * Validate position. Position is tested from 0:0 to size:size, if there are some moves, * that should be captured, they will be removed. Returns a new Position object. * This position isn't modified. */ Position.prototype.validatePosition = function () { for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { this.captureIfNoLiberties(x - 1, y); } } return this; }; /** * Returns true if stone or group on the given coordinates has at least one liberty. */ Position.prototype.hasLiberties = function (x, y, alreadyTested, c) { if (alreadyTested === void 0) { alreadyTested = createGrid(this.size); } if (c === void 0) { c = this.get(x, y); } // out of the board there aren't liberties if (!this.isOnPosition(x, y)) { return false; } // however empty field means liberty if (this.get(x, y) === exports.Color.EMPTY) { return true; } // already tested field or stone of enemy isn't a liberty. if (alreadyTested[x][y] || this.get(x, y) === -c) { return false; } // set this field as tested alreadyTested[x][y] = true; // in this case we are checking our stone, if we get 4 false, it has no liberty return (this.hasLiberties(x, y - 1, alreadyTested, c) || this.hasLiberties(x, y + 1, alreadyTested, c) || this.hasLiberties(x - 1, y, alreadyTested, c) || this.hasLiberties(x + 1, y, alreadyTested, c)); }; /** * Checks if specified stone/group has zero liberties and if so it captures/removes stones from the position. */ Position.prototype.captureIfNoLiberties = function (x, y) { // if it has zero liberties capture it if (!this.hasLiberties(x, y)) { // capture stones from game this.capture(x, y); return true; } return false; }; /** * Captures/removes stone on specified position and all adjacent and connected stones. This method ignores liberties. */ Position.prototype.capture = function (x, y, c) { if (c === void 0) { c = this.get(x, y); } if (this.isOnPosition(x, y) && c !== exports.Color.EMPTY && this.get(x, y) === c) { this.set(x, y, exports.Color.EMPTY); if (c === exports.Color.BLACK) { this.capCount.white = this.capCount.white + 1; } else { this.capCount.black = this.capCount.black + 1; } this.capture(x, y - 1, c); this.capture(x, y + 1, c); this.capture(x - 1, y, c); this.capture(x + 1, y, c); } }; /** * For debug purposes. */ Position.prototype.toString = function () { var TL = '┌'; var TM = '┬'; var TR = '┐'; var ML = '├'; var MM = '┼'; var MR = '┤'; var BL = '└'; var BM = '┴'; var BR = '┘'; var BS = '●'; var WS = '○'; var HF = '─'; // horizontal fill var output = ' '; for (var i = 0; i < this.size; i++) { output += i < 9 ? i + " " : i; } output += '\n'; for (var y = 0; y < this.size; y++) { for (var x = 0; x < this.size; x++) { var color = this.grid[x * this.size + y]; if (x === 0) { output += (y < 10 ? " " + y : y) + " "; } if (color !== exports.Color.EMPTY) { output += color === exports.Color.BLACK ? BS : WS; } else { var char = void 0; if (y === 0) { // top line if (x === 0) { char = TL; } else if (x < this.size - 1) { char = TM; } else { char = TR; } } else if (y < this.size - 1) { // middle line if (x === 0) { char = ML; } else if (x < this.size - 1) { char = MM; } else { char = MR; } } else { // bottom line if (x === 0) { char = BL; } else if (x < this.size - 1) { char = BM; } else { char = BR; } } output += char; } if (x === this.size - 1) { if (y !== this.size - 1) { output += '\n'; } } else { output += HF; } } } return output; }; /** * Returns position grid as two dimensional array. */ Position.prototype.toTwoDimensionalArray = function () { var arr = []; for (var x = 0; x < this.size; x++) { arr[x] = []; for (var y = 0; y < this.size; y++) { arr[x][y] = this.grid[x * this.size + y]; } } return arr; }; return Position; }()); // import { Color, Field, Move } from '../types'; // /** // * Position of the board (grid) is represented as 2 dimensional array of colors. // */ // export type Position = Color[][]; // /** // * Creates empty position (filled with Color.EMPTY) of specified size. // * @param size // */ // export function createPosition(size: number) { // const position: Color[][] = []; // for (let i = 0; i < size; i++) { // const row: Color[] = []; // for (let j = 0; j < size; j++) { // row.push(Color.EMPTY); // } // position.push(row); // } // return position; // } // /** // * Deep clones a position. // * @param position // */ // export function clonePosition(position: Position) { // return position.map(row => row.slice(0)); // } // /** // * Compares position `pos1` with position `pos2` and returns all differences on `pos2`. // * @param pos1 // * @param pos2 // */ // export function comparePositions(pos1: Position, pos2: Position): Field[] { // if (pos1.length !== pos2.length) { // throw new TypeError('Positions of different sizes cannot be compared.'); // } // const diff: Field[] = []; // for (let x = 0; x < pos1.length; x++) { // for (let y = 0; y < pos2.length; y++) { // if (pos1[x][y] !== pos2[x][y]) { // diff.push({ x, y, c: pos2[x][y] }); // } // } // } // return diff; // } // function isOnBoard(position: Position, x: number, y: number) { // return x >= 0 && x < position.length && y >= 0 && y < position.length; // } // /** // * Creates new position with specified move (with rules applied - position won't contain captured stones). // * If move is invalid, null is returned. // */ // export function applyMove(position: Position, x: number, y: number, c: Color.B | Color.W, allowSuicide = false) { // // check if move is on empty field of the board // if (!isOnBoard(position, x, y) || position[x][y] !== Color.EMPTY) { // return null; // } // // clone position and add a stone // const newPosition = clonePosition(position); // newPosition[x][y] = c; // // check capturing of all surrounding stones // const capturesAbove = captureIfNoLiberties(newPosition, x, y - 1, -c); // const capturesRight = captureIfNoLiberties(newPosition, x + 1, y, -c); // const capturesBelow = captureIfNoLiberties(newPosition, x, y + 1, -c); // const capturesLeft = captureIfNoLiberties(newPosition, x - 1, y, -c); // const hasCaptured = capturesAbove || capturesRight || capturesBelow || capturesLeft; // // check suicide // if (!hasCaptured) { // if (!hasLiberties(newPosition, x, y)) { // if (allowSuicide) { // capture(newPosition, x, y, c); // } else { // return null; // } // } // } // return newPosition; // } // /** // * Validate position. Position is tested from 0:0 to size:size, if there are some moves, // * that should be captured, they will be removed. Returns a new Position object. // */ // export function getValidatedPosition(position: Position) { // const newPosition = clonePosition(position); // for (let x = 0; x < position.length; x++) { // for (let y = 0; y < position.length; y++) { // captureIfNoLiberties(newPosition, x, y); // } // } // return newPosition; // } // /** // * Capture stone or group of stones if they are zero liberties. Mutates the given position. // * // * @param position // * @param x // * @param y // * @param c // */ // function captureIfNoLiberties(position: Position, x: number, y: number, c: Color = position[x][y]) { // let hasCaptured = false; // // is there a stone possible to capture? // if (isOnBoard(position, x, y) && c !== Color.EMPTY && posi