/**
* 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);