smartgamer
Version:
Use smartgame objects created from SGF (Smart Game Format) files.
340 lines (284 loc) • 7.6 kB
JavaScript
/**
* Interact with smartgame objects.
* @param {object} smartgame A JS Object representing a smartgame
* @see http://www.red-bean.com/sgf/sgf4.html
* @return {object} An object with methods for navigating and manipulating a
* smartgame
*/
module.exports = function (smartgame) {
'use strict';
var sequence;
var node;
var Smartgamer = function () {
this.init();
};
Smartgamer.prototype = {
init: function () {
if (smartgame) {
this.game = smartgame.gameTrees[0];
this.reset();
}
},
// Load a smartgame to make it possible to load new games or add one after
// initialization, if desired
load: function (newSmartgame) {
smartgame = newSmartgame;
this.init();
},
// Having multiple games in a collection is not common, but it's part of
// the spec
games: function () {
return smartgame.gameTrees;
},
selectGame: function (i) {
if (i < smartgame.gameTrees.length) {
this.game = smartgame.gameTrees[i];
this.reset();
} else {
throw new Error('the collection doesn\'t contain that many games');
}
return this;
},
reset: function () {
sequence = this.game;
node = sequence.nodes[0];
this.path = { m: 0 };
return this;
},
getSmartgame: function () {
return smartgame;
},
/**
* Return any variations available at the current move
**/
variations: function () {
if (sequence) {
var localNodes = sequence.nodes;
var localIndex = (localNodes) ? localNodes.indexOf(node) : null;
if (localNodes) {
if (localIndex === (localNodes.length - 1)) {
return sequence.sequences || [];
} else {
return [];
}
}
}
},
/**
* Go to the next move
**/
next: function (variation) {
variation = variation || 0;
var localNodes = sequence.nodes;
var localIndex = (localNodes) ? localNodes.indexOf(node) : null;
// If there are no additional nodes in this sequence,
// advance to the next one
if (localIndex === null || localIndex >= (localNodes.length - 1)) {
if (sequence.sequences) {
if (sequence.sequences[variation]) {
sequence = sequence.sequences[variation];
} else {
sequence = sequence.sequences[0];
}
node = sequence.nodes[0];
// Note the fork chosen for this variation in the path
this.path[this.path.m] = variation;
this.path.m += 1;
} else {
// End of sequence / game
return this;
}
} else {
node = localNodes[localIndex + 1];
this.path.m += 1;
}
return this;
},
/**
* Go to the previous move
**/
previous: function () {
var localNodes = sequence.nodes;
var localIndex = (localNodes) ? localNodes.indexOf(node) : null;
// Delete any variation forks at this point
// TODO: Make this configurable... we should keep this if we're
// remembering chosen paths
delete this.path[this.path.m];
if (!localIndex || localIndex === 0) {
if (sequence.parent && !sequence.parent.gameTrees) {
sequence = sequence.parent;
if (sequence.nodes) {
node = sequence.nodes[sequence.nodes.length - 1];
this.path.m -= 1;
} else {
node = null;
}
} else {
// Already at the beginning
return this;
}
} else {
node = localNodes[localIndex - 1];
this.path.m -= 1;
}
return this;
},
// Go to the last move of the game
last: function () {
var totalMoves = this.totalMoves();
while(this.path.m < totalMoves) {
this.next();
}
return this;
},
// Go to the first move of the game
first: function () {
this.reset();
return this;
},
/**
* Go to a particular move, specified as a
* a) number
* b) path string
* c) path object
**/
goTo: function (path) {
if (typeof path === 'string') {
path = this.pathTransform(path, 'object');
} else if (typeof path === 'number') {
path = { m: path };
}
this.reset();
var n = node;
for (var i = 0; i < path.m && n; i += 1) {
// Check for a variation in the path for the upcoming move
var variation = path[i + 1] || 0;
n = this.next(variation);
}
return this;
},
getGameInfo: function () {
return this.game.nodes[0];
},
// Provide the current node
node: function () {
return node;
},
// Get the total number of moves in a game
totalMoves: function () {
var localSequence = this.game;
var moves = 0;
while(localSequence) {
moves += localSequence.nodes.length;
if (localSequence.sequences) {
localSequence = localSequence.sequences[0];
} else {
localSequence = null;
}
}
// TODO: Right now we're *assuming* that the root node doesn't have a
// move in it, which is *recommended* but not required practice.
// @see http://www.red-bean.com/sgf/sgf4.html
// "Note: it's bad style to have move properties in root nodes.
// (it isn't forbidden though)"
return moves - 1;
},
// Get or set a comment on the current node
// @see http://www.red-bean.com/sgf/sgf4.html#text
comment: function (text) {
if (typeof text === 'undefined') {
// Unescape characters
if (node.C) {
return node.C.replace(/\\([\\:\]])/g, '$1');
} else {
return '';
}
} else {
// Escape characters
node.C = text.replace(/[\\:\]]/g, '\\$&');
}
},
/**
* Translate alpha coordinates into an array
* @param string alphaCoordinates
* @return array [x, y]
**/
translateCoordinates: function (alphaCoordinates) {
var coordinateLabels = 'abcdefghijklmnopqrst';
var intersection = [];
intersection[0] = coordinateLabels.indexOf(alphaCoordinates.substring(0, 1));
intersection[1] = coordinateLabels.indexOf(alphaCoordinates.substring(1, 2));
return intersection;
},
/**
* Convert path objects to strings and path strings to objects
**/
pathTransform: function (input, outputType, verbose) {
var output;
// If no output type has been specified, try to set it to the
// opposite of the input
if (typeof outputType === 'undefined') {
outputType = (typeof input === 'string') ? 'object' : 'string';
}
/**
* Turn a path object into a string.
*/
function stringify(input) {
if (typeof input === 'string') {
return input;
}
if (!input) {
return '';
}
output = input.m;
var variations = [];
for (var key in input) {
if (input.hasOwnProperty(key) && key !== 'm') {
// Only show variations that are not the primary one, since
// primary variations are chosen by default
if (input[key] > 0) {
if (verbose) {
variations.push(', variation ' + input[key] + ' at move ' + key);
} else {
variations.push('-' + key + ':' + input[key]);
}
}
}
}
output += variations.join('');
return output;
}
/**
* Turn a path string into an object.
*/
function parse(input) {
if (typeof input === 'object') {
input = stringify(input);
}
if (!input) {
return { m: 0 };
}
var path = input.split('-');
output = {
m: Number(path.shift())
};
if (path.length) {
path.forEach(function (variation, i) {
variation = variation.split(':');
output[Number(variation[0])] = parseInt(variation[1], 10);
});
}
return output;
}
if (outputType === 'string') {
output = stringify(input);
} else if (outputType === 'object') {
output = parse(input);
} else {
output = undefined;
}
return output;
}
};
return new Smartgamer();
};