/** * This extension handles go game records(kifu). In WGo kifu is stored in JSON. Kifu structure example: * * JGO proposal = { * size: 19, * info: { * black: {name:"Lee Chang-Ho", rank:"9p"}, * white: {name:"Lee Sedol", rank:"9p"}, * komi: 6.5, * }, * game: [ * {B:"mm"}, * {W:"nn"}, * {B:"cd"}, * {}, * ] * } * */ (function (WGo, undefined) { "use strict"; var recursive_clone = function (node) { var n = new KNode(JSON.parse(JSON.stringify(node.getProperties()))); for (var ch in node.children) { n.appendChild(recursive_clone(node.children[ch])); } return n; }; var find_property = function (prop, node) { var res; if (node[prop] !== undefined) return node[prop]; for (var ch in node.children) { res = find_property(prop, node.children[ch]); if (res) return res; } return false; }; var recursive_save = function (gameTree, node) { gameTree.push(JSON.parse(JSON.stringify(node.getProperties()))); if (node.children.length > 1) { var nt = []; for (var i = 0; i < node.children.length; i++) { var t = []; recursive_save(t, node.children[i]); nt.push(t); } gameTree.push(nt); } else if (node.children.length) { recursive_save(gameTree, node.children[0]); } }; var recursive_save2 = function (gameTree, node) { var anode = node; var tnode; for (var i = 1; i < gameTree.length; i++) { if (gameTree[i].constructor == Array) { for (var j = 0; j < gameTree[i].length; j++) { tnode = new KNode(gameTree[i][j][0]); anode.appendChild(tnode); recursive_save2(gameTree[i][j], tnode); } } else { tnode = new KNode(gameTree[i]); anode.insertAfter(tnode); anode = tnode; } } }; var sgf_escape = function (text) { if (typeof text == "string") return text.replace(/\\/g, "\\\\").replace(/]/g, "\\]"); else return text; }; var a_char = "a".charCodeAt(0); var sgf_coordinates = function (x, y) { return String.fromCharCode(a_char + x) + String.fromCharCode(a_char + y); }; var sgf_write_group = function (prop, values, output) { if (!values.length) return; output.sgf += prop; for (var i in values) { output.sgf += "[" + values[i] + "]"; } }; var sgf_write_node = function (node, output) { // move if (node.move) { var move = ""; if (!node.move.pass) move = sgf_coordinates(node.move.x, node.move.y); if (node.move.c == WGo.B) output.sgf += "B[" + move + "]"; else output.sgf += "W[" + move + "]"; } // setup if (node.setup) { var AB = []; var AW = []; var AE = []; for (var i in node.setup) { if (node.setup[i].c == WGo.B) AB.push(sgf_coordinates(node.setup[i].x, node.setup[i].y)); else if (node.setup[i].c == WGo.W) AW.push(sgf_coordinates(node.setup[i].x, node.setup[i].y)); else AE.push(sgf_coordinates(node.setup[i].x, node.setup[i].y)); } sgf_write_group("AB", AB, output); sgf_write_group("AW", AW, output); sgf_write_group("AE", AE, output); } // markup if (node.markup) { var markup = {}; for (var i in node.markup) { markup[node.markup[i].type] = markup[node.markup[i].type] || []; if (node.markup[i].type == "LB") markup["LB"].push( sgf_coordinates(node.markup[i].x, node.markup[i].y) + ":" + sgf_escape(node.markup[i].text) ); else markup[node.markup[i].type].push( sgf_coordinates(node.markup[i].x, node.markup[i].y) ); } for (var key in markup) { sgf_write_group(key, markup[key], output); } } // other var props = node.getProperties(); for (var key in props) { if (typeof props[key] == "object") continue; if (key == "turn") output.sgf += "PL[" + (props[key] == WGo.B ? "B" : "W") + "]"; else if (key == "comment") output.sgf += "C[" + sgf_escape(props[key]) + "]"; else output.sgf += key + "[" + sgf_escape(props[key]) + "]"; } if (node.children.length == 1) { output.sgf += "\n;"; sgf_write_node(node.children[0], output); } else if (node.children.length > 1) { for (var key in node.children) { sgf_write_variantion(node.children[key], output); } } }; var sgf_write_variantion = function (node, output) { output.sgf += "(\n;"; sgf_write_node(node, output); output.sgf += "\n)"; }; /** * Kifu class - for storing go game record and easy manipulation with it */ var Kifu = function () { this.size = 19; this.info = {}; this.root = new KNode(); this.nodeCount = 0; this.propertyCount = 0; }; Kifu.prototype = { constructor: Kifu, clone: function () { var clone = new Kifu(); clone.size = this.size; clone.info = JSON.parse(JSON.stringify(this.info)); clone.root = recursive_clone(this.root); clone.nodeCount = this.nodeCount; clone.propertyCount = this.propertyCount; return clone; }, hasComments: function () { return !!find_property("comment", this.root); }, }; /** * Create kifu object from SGF string */ Kifu.fromSgf = function (sgf) { return WGo.SGF.parse(sgf); }; /** * Create kifu object from JGO */ Kifu.fromJGO = function (arg) { var jgo = typeof arg == "string" ? JSON.parse(arg) : arg; var kifu = new Kifu(); kifu.info = JSON.parse(JSON.stringify(jgo.info)); kifu.size = jgo.size; kifu.nodeCount = jgo.nodeCount; kifu.propertyCount = jgo.propertyCount; kifu.root = new KNode(jgo.game[0]); recursive_save2(jgo.game, kifu.root); return kifu; }; /** * Return SGF string from kifu object */ Kifu.prototype.toSgf = function () { var output = { sgf: "(\n;" }; var root_props = {}; // other info for (var key in this.info) { if (key == "black") { if (this.info.black.name) root_props.PB = sgf_escape(this.info.black.name); if (this.info.black.rank) root_props.BR = sgf_escape(this.info.black.rank); if (this.info.black.team) root_props.BT = sgf_escape(this.info.black.team); } else if (key == "white") { if (this.info.white.name) root_props.PW = sgf_escape(this.info.white.name); if (this.info.white.rank) root_props.WR = sgf_escape(this.info.white.rank); if (this.info.white.team) root_props.WT = sgf_escape(this.info.white.team); } else root_props[key] = sgf_escape(this.info[key]); } // board size if (this.size) root_props.SZ = this.size; // add missing info if (!root_props.AP) root_props.AP = "WGo.js:2"; if (!root_props.FF) root_props.FF = "4"; if (!root_props.GM) root_props.GM = "1"; if (!root_props.CA) root_props.CA = "UTF-8"; // write root for (var key in root_props) { if (root_props[key]) output.sgf += key + "[" + root_props[key] + "]"; } sgf_write_node(this.root, output); output.sgf += ")"; return output.sgf; }; /** * Return JGO from kifu object */ Kifu.prototype.toJGO = function (stringify) { var jgo = {}; jgo.size = this.size; jgo.info = JSON.parse(JSON.stringify(this.info)); jgo.nodeCount = this.nodeCount; jgo.propertyCount = this.propertyCount; jgo.game = []; recursive_save(jgo.game, this.root); if (stringify) return JSON.stringify(jgo); else return jgo; }; var player_formatter = function (value) { var str; if (value.name) { str = WGo.filterHTML(value.name); if (value.rank) str += " (" + WGo.filterHTML(value.rank) + ")"; if (value.team) str += ", " + WGo.filterHTML(value.team); } else { if (value.team) str = WGo.filterHTML(value.team); if (value.rank) str += " (" + WGo.filterHTML(value.rank) + ")"; } return str; }; /** * Game information formatters. Each formatter is a function which somehow formats input text. */ Kifu.infoFormatters = { black: player_formatter, white: player_formatter, TM: function (time) { if (time == 0) return WGo.t("none"); var res, t = Math.floor(time / 60); if (t == 1) res = "1 " + WGo.t("minute"); else if (t > 1) res = t + " " + WGo.t("minutes"); t = time % 60; if (t == 1) res += " 1 " + WGo.t("second"); else if (t > 1) res += " " + t + " " + WGo.t("seconds"); return res; }, RE: function (res) { return ( '' + WGo.t("show") + "" ); }, }; /** * List of game information properties */ Kifu.infoList = [ "black", "white", "AN", "CP", "DT", "EV", "GN", "GC", "HA", "ON", "OT", "RE", "RO", "RU", "SO", "TM", "US", "PC", "KM", ]; WGo.Kifu = Kifu; var no_add = function (arr, obj, key) { for (var i = 0; i < arr.length; i++) { if (arr[i].x == obj.x && arr[i].y == obj.y) { arr[i][key] = obj[key]; return; } } arr.push(obj); }; var no_remove = function (arr, obj) { if (!arr) return; for (var i = 0; i < arr.length; i++) { if (arr[i].x == obj.x && arr[i].y == obj.y) { arr.splice(i, 1); return; } } }; /** * Node class of kifu game tree. It can contain move, setup or markup properties. * * @param {object} properties * @param {KNode} parent (null for root node) */ var KNode = function (properties, parent) { this.parent = parent || null; this.children = []; // save all properties if (properties) for (var key in properties) this[key] = properties[key]; }; KNode.prototype = { constructor: KNode, /** * Get node's children specified by index. If it doesn't exist, method returns null. */ getChild: function (ch) { var i = ch || 0; if (this.children[i]) return this.children[i]; else return null; }, /** * Add setup property. * * @param {object} setup object with structure: {x:, y:, c:} */ addSetup: function (setup) { this.setup = this.setup || []; no_add(this.setup, setup, "c"); return this; }, /** * Remove setup property. * * @param {object} setup object with structure: {x:, y:} */ removeSetup: function (setup) { no_remove(this.setup, setup); return this; }, /** * Add markup property. * * @param {object} markup object with structure: {x:, y:, type:} */ addMarkup: function (markup) { this.markup = this.markup || []; no_add(this.markup, markup, "type"); return this; }, /** * Remove markup property. * * @param {object} markup object with structure: {x:, y:} */ removeMarkup: function (markup) { no_remove(this.markup, markup); return this; }, /** * Remove this node. * Node is removed from its parent and children are passed to parent. */ remove: function () { var p = this.parent; if (!p) throw new Exception("Root node cannot be removed"); for (var i in p.children) { if (p.children[i] == this) { p.children.splice(i, 1); break; } } p.children = p.children.concat(this.children); this.parent = null; return p; }, /** * Insert node after this node. All children are passed to new node. */ insertAfter: function (node) { for (var child in this.children) { this.children[child].parent = node; } node.children = node.children.concat(this.children); node.parent = this; this.children = [node]; return node; }, /** * Append child node to this node. */ appendChild: function (node) { node.parent = this; this.children.push(node); return node; }, /** * Get properties as object. */ getProperties: function () { var props = {}; for (var key in this) { if ( this.hasOwnProperty(key) && key != "children" && key != "parent" && key[0] != "_" ) props[key] = this[key]; } return props; }, }; WGo.KNode = KNode; var pos_diff = function (old_p, new_p) { var size = old_p.size, add = [], remove = []; for (var i = 0; i < size * size; i++) { if (old_p.schema[i] && !new_p.schema[i]) remove.push({ x: Math.floor(i / size), y: i % size }); else if (old_p.schema[i] != new_p.schema[i]) add.push({ x: Math.floor(i / size), y: i % size, c: new_p.schema[i] }); } return { add: add, remove: remove, }; }; /** * KifuReader object is capable of reading a kifu nodes and executing them. It contains Game object with actual position. * Variable change contains last changes of position. * If parameter rememberPath is set, KifuReader will remember last selected child of all nodes. * If parameter allowIllegalMoves is set, illegal moves will be played instead of throwing an exception */ var KifuReader = function (kifu, rememberPath, allowIllegalMoves) { this.kifu = kifu; this.node = this.kifu.root; this.allow_illegal = allowIllegalMoves || false; this.game = new WGo.Game( this.kifu.size, this.allow_illegal ? "NONE" : "KO", this.allow_illegal, this.allow_illegal ); this.path = { m: 0 }; if (this.kifu.info["HA"] && this.kifu.info["HA"] > 1) this.game.turn = WGo.W; this.change = exec_node(this.game, this.node, true); if (rememberPath) this.rememberPath = true; else this.rememberPath = false; }; var set_subtract = function (a, b) { var n = [], q; for (var i in a) { q = true; for (var j in b) { if (a[i].x == b[j].x && a[i].y == b[j].y) { q = false; break; } } if (q) n.push(a[i]); } return n; }; var concat_changes = function (ch_orig, ch_new) { ch_orig.add = set_subtract(ch_orig.add, ch_new.remove).concat(ch_new.add); ch_orig.remove = set_subtract(ch_orig.remove, ch_new.add).concat( ch_new.remove ); }; // change game object according to node, return changes var exec_node = function (game, node, first) { if (node.parent) node.parent._last_selected = node.parent.children.indexOf(node); // handle moves nodes if (node.move != undefined) { if (node.move.pass) { game.pass(node.move.c); return { add: [], remove: [] }; } else { var res = game.play(node.move.x, node.move.y, node.move.c); if (typeof res == "number") throw new InvalidMoveError(res, node); // we must check whether to add move (it can be suicide) for (var i in res) { if (res[i].x == node.move.x && res[i].y == node.move.y) { return { add: [], remove: res, }; } } return { add: [node.move], remove: res, }; } } // handle other(setup) nodes else { if (!first) game.pushPosition(); var add = [], remove = []; if (node.setup != undefined) { for (var i in node.setup) { if (node.setup[i].c) { game.setStone(node.setup[i].x, node.setup[i].y, node.setup[i].c); add.push(node.setup[i]); } else { game.removeStone(node.setup[i].x, node.setup[i].y); remove.push(node.setup[i]); } } } if (node.turn) game.turn = node.turn; return { add: add, remove: remove, }; } }; var exec_next = function (i) { if (i === undefined && this.rememberPath) i = this.node._last_selected; i = i || 0; var node = this.node.children[i]; if (!node) return false; var ch = exec_node(this.game, node); this.path.m++; if (this.node.children.length > 1) this.path[this.path.m] = i; this.node = node; return ch; }; var exec_previous = function () { if (!this.node.parent) return false; this.node = this.node.parent; this.game.popPosition(); if (this.node.turn) this.game.turn = this.node.turn; if (this.path[this.path.m] !== undefined) delete this.path[this.path.m]; this.path.m--; return true; }; var exec_first = function () { //if(!this.node.parent) return; this.game.firstPosition(); this.node = this.kifu.root; this.path = { m: 0 }; if (this.kifu.info["HA"] && this.kifu.info["HA"] > 1) this.game.turn = WGo.W; this.change = exec_node(this.game, this.node, true); }; KifuReader.prototype = { constructor: KifuReader, /** * Go to next node and if there is a move play it. */ next: function (i) { this.change = exec_next.call(this, i); return this; }, /** * Execute all nodes till the end. */ last: function () { var ch; this.change = { add: [], remove: [], }; while ((ch = exec_next.call(this))) concat_changes(this.change, ch); return this; }, /** * Return to the previous position (redo actual node) */ previous: function () { var old_pos = this.game.getPosition(); exec_previous.call(this); this.change = pos_diff(old_pos, this.game.getPosition()); return this; }, /** * Go to the initial position */ first: function () { var old_pos = this.game.getPosition(); exec_first.call(this); this.change = pos_diff(old_pos, this.game.getPosition()); return this; }, /** * Go to position specified by path object */ goTo: function (path) { if (path === undefined) return this; var old_pos = this.game.getPosition(); exec_first.call(this); var r; for (var i = 0; i < path.m; i++) { if (!exec_next.call(this, path[i + 1])) { break; } } this.change = pos_diff(old_pos, this.game.getPosition()); return this; }, /** * Go to previous fork (a node with more than one child) */ previousFork: function () { var old_pos = this.game.getPosition(); while (exec_previous.call(this) && this.node.children.length == 1) {} this.change = pos_diff(old_pos, this.game.getPosition()); return this; }, /** * Shortcut. Get actual position object. */ getPosition: function () { return this.game.getPosition(); }, /** * Allow or disallow illegal moves to be played */ allowIllegalMoves: function (b) { if (b) { this.game.allow_rewrite = true; this.game.allow_suicide = true; this.repeating = "NONE"; } else { this.game.allow_rewrite = false; this.game.allow_suicide = false; this.repeating = "KO"; } }, }; WGo.KifuReader = KifuReader; // Class handling invalid moves in kifu var InvalidMoveError = function (code, node) { this.name = "InvalidMoveError"; this.message = "Invalid move in kifu detected. "; if ( node.move && node.move.c !== undefined && node.move.x !== undefined && node.move.y !== undefined ) { var letter = node.move.x; if (node.move.x > 7) letter++; letter = String.fromCharCode(letter + 65); this.message += "Trying to play " + (node.move.c == WGo.WHITE ? "white" : "black") + " move on " + String.fromCharCode(node.move.x + 65) + "" + (19 - node.move.y); } else this.message += "Move object doesn't contain arbitrary attributes."; if (code) { switch (code) { case 1: this.message += ", but these coordinates are not on board."; break; case 2: this.message += ", but there already is a stone."; break; case 3: this.message += ", but this move is a suicide."; break; case 4: this.message += ", but this position already occured."; break; } } else this.message += "."; }; InvalidMoveError.prototype = new Error(); InvalidMoveError.prototype.constructor = InvalidMoveError; WGo.InvalidMoveError = InvalidMoveError; WGo.i18n.en["show"] = "show"; WGo.i18n.en["res-show-tip"] = "Click to show result."; })(WGo);