'use strict';
var Coordinate = require('./coordinate');
var C = require('./constants');
var util = require('./util');
/**
* Go board class for storing intersection states. Also has listeners that
* are notified on any changes to the board via setType() and setMark().
*
* @param {int} width The width of the board
* @param {int} [height] The height of the board
* @constructor
*/
var Board = function(width, height) {
this.width = width;
if(height !== undefined)
this.height = height;
else { //noinspection JSSuspiciousNameCombination
this.height = this.width;
}
this.listeners = [];
this.stones = [];
this.marks = [];
// Initialize stones and marks
for(var i=0; i<this.width; ++i) {
var stoneArr = [], markArr = [];
for(var j=0; j<this.height; ++j) {
stoneArr.push(C.CLEAR);
markArr.push(C.MARK.NONE);
}
this.stones.push(stoneArr);
this.marks.push(markArr);
}
};
/**
* Add listener to the board. Listeners are passed an event object with
* event type ('type' or 'mark'), coordinate and board members, and new
* and old values as newVal and oldVal. Event object should be
* considered read only.
*
* @param {function} func A listener callback.
*/
Board.prototype.addListener = function(func) {
this.listeners.push(func);
};
/**
* Remove listener from the board.
*
* @param {function} func A listener callback.
*/
Board.prototype.removeListener = function(func) {
var index = this.listeners.indexOf(func);
if(index != -1) this.listeners.splice(index, 1);
};
/**
* Create coordinate from "J18" type of notation that depend from board size.
*
* @param {string} s The coordinate string.
*/
Board.prototype.getCoordinate = function(s) {
return new Coordinate(C.COORDINATES.indexOf(s.toUpperCase().substr(0,1)),
this.height - parseInt(s.substr(1)));
};
/**
* Make a human readable "J18" type string representation of the coordinate.
*
* @param {Coordinate} c Coordinate.
* @returns {string} representation.
*/
Board.prototype.toString = function(c) {
return C.COORDINATES[c.i] + (this.height-c.j);
};
/**
* Simple iteration over all coordinates.
*
* @param {func} func The iterator method, which is called with the
* coordinate, type and mark parameters.
* @param {int} [i1] Column start.
* @param {int} [j1] Row start.
* @param {int} [i2] Colunm end.
* @param {int} [j2] Row end.
*/
Board.prototype.each = function(func, i1, j1, i2, j2) {
var c = new Coordinate();
if(i1 === undefined) i1 = 0;
if(j1 === undefined) j1 = 0;
if(i2 === undefined) i2 = this.width-1;
if(j2 === undefined) j2 = this.height-1;
for(c.j=j1; c.j<=j2; c.j++)
for(c.i=i1; c.i<=i2; c.i++)
func(c.copy(), this.stones[c.i][c.j], this.marks[c.i][c.j]);
};
/**
* Clear board.
*/
Board.prototype.clear = function() {
this.each(function(c) {
this.setType(c, C.CLEAR);
this.setMark(c, C.MARK.NONE);
}.bind(this));
};
/**
* Set the intersection type at given coordinate(s).
*
* @param {Object} c A Coordinate or Array of them.
* @param {Object} t New type, e.g. CLEAR, BLACK, ...
*/
Board.prototype.setType = function(c, t) {
if(c instanceof Coordinate) {
var old = this.stones[c.i][c.j];
if(old == t) return; // no change
this.stones[c.i][c.j] = t;
var ev = { type: 'type', coordinate: c, board: this,
oldVal: old, newVal: t };
this.listeners.forEach(function(l) { l(ev); });
} else if(c instanceof Array) {
for(var i=0, len=c.length; i<len; ++i)
this.setType(c[i], t); // use ourself to avoid duplicate code
}
};
/**
* Set the intersection mark at given coordinate(s).
*
* @param {Object} c A Coordinate or Array of them.
* @param {Object} m New mark, e.g. MARK.NONE, MARK.TRIANGLE, ...
*/
Board.prototype.setMark = function(c, m) {
if(c instanceof Coordinate) {
var old = this.marks[c.i][c.j];
if(old == m) return; // no change
this.marks[c.i][c.j] = m;
var ev = { type: 'mark', coordinate: c, board: this,
oldVal: old, newVal: m };
this.listeners.forEach(function(l) { l(ev); });
} else if(c instanceof Array) {
for(var i=0, len=c.length; i<len; ++i)
this.setMark(c[i], m); // use ourself to avoid duplicate code
}
};
/**
* Get the intersection type(s) at given coordinate(s).
*
* @param {Object} c A Coordinate or an Array of them.
* @returns {Object} Type or array of types.
*/
Board.prototype.getType = function(c) {
var ret;
if(c instanceof Coordinate) {
ret = this.stones[c.i][c.j];
} else if(c instanceof Array) {
ret = [];
for(var i=0, len=c.length; i<len; ++i)
ret.push(this.stones[c[i].i][c[i].j]);
}
return ret;
};
/**
* Get the intersection mark(s) at given coordinate(s).
*
* @param {Object} c A Coordinate or an Array of them.
* @returns {Object} Mark or array of marks.
*/
Board.prototype.getMark = function(c) {
var ret;
if(c instanceof Coordinate) {
ret = this.marks[c.i][c.j];
} else if(c instanceof Array) {
ret = [];
for(var i=0, len=c.length; i<len; ++i)
ret.push(this.marks[c[i].i][c[i].j]);
}
return ret;
};
/**
* Get neighboring coordinates on board.
*
* @param {Coordinate} c The coordinate
* @returns {Array} The array of adjacent coordinates of given type (may be an empty array)
*/
Board.prototype.getAdjacent = function(c) {
var coordinates = [], i = c.i, j = c.j;
if(i>0)
coordinates.push(new Coordinate(i-1, j));
if(i+1<this.width)
coordinates.push(new Coordinate(i+1, j));
if(j>0)
coordinates.push(new Coordinate(i, j-1));
if(j+1<this.height)
coordinates.push(new Coordinate(i, j+1));
return coordinates;
};
/**
* Filter coordinates based on intersection type.
*
* @param {Object} c An array of Coordinates.
* @param {Object} t A type filter (return only matching type).
* @returns {Object} Object with attributes 'type' and 'mark', array or false.
*/
Board.prototype.filter = function(c, t) {
var ret = [];
for(var i=0, len=c.length; i<len; ++i)
if(this.stones[c[i].i][c[i].j] == t)
ret.push(c);
return ret;
};
/**
* Check if coordinates contain given type.
*
* @param {Object} c An array of Coordinates.
* @param {Object} t A type filter (return only matching type).
* @returns {bool} True or false.
*/
Board.prototype.hasType = function(c, t) {
for(var i=0, len=c.length; i<len; ++i)
if(this.stones[c[i].i][c[i].j] == t)
return true;
return false;
};
/**
* Search all intersections of similar type, return group and edge coordinates.
*
* @param {Coordinate} coord The coordinate from which to start search.
* @param {int} [overrideType] Treat current coordinate as this type.
* @returns {Object} Two arrays of coordinates in members 'group' and 'neighbors'.
*/
Board.prototype.getGroup = function(coord, overrideType) {
var type = overrideType || this.getType(coord), seen = {},
group = [coord.copy()], neighbors = [],
queue = this.getAdjacent(coord);
seen[coord.toString()] = true;
while(queue.length) {
var c = queue.shift();
if(c.toString() in seen)
continue; // seen already
else
seen[c.toString()] = true; // seen now
if(this.getType(c) == type) { // check if type is correct
group.push(c);
queue = queue.concat(this.getAdjacent(c)); // add prospects
} else
neighbors.push(c);
}
return {group: group, neighbors: neighbors};
};
/**
* Get a raw copy of board contents. Will not include any listeners!
*
* @returns {Object} Board contents.
*/
Board.prototype.getRaw = function() {
return {
width: this.width,
height: this.height,
stones: util.extend({}, this.stones),
marks: util.extend({}, this.marks)
};
};
/**
* Set a raw copy of board contents. Will not change or call any listeners!
*
* @param {Object} raw Board contents.
*/
Board.prototype.setRaw = function(raw) {
this.width = raw.width;
this.height = raw.height;
this.stones = raw.stones;
this.marks = raw.marks;
};
/**
* Calculate impact of a move on board. Returns a data structure outlining
* validness of move (success & errorMsg) and possible captures and ko
* coordinate.
*
* @param {Board} jboard Board to play the move on (stays unchanged).
* @param {Coordinate} coord Coordinate to play or null for pass.
* @param {int} stone Stone to play - BLACK or WHITE.
* @param {Coordinate} [ko] Coordinate of previous ko.
* @returns {Object} Move result data structure.
*/
Board.prototype.playMove = function(coord, stone, ko) {
var oppType = (stone == C.BLACK ? C.WHITE : C.BLACK),
captures = [], adjacent;
if(!coord) // pass
return { success: true, captures: [], ko: false };
if(this.getType(coord) != C.CLEAR)
return { success: false,
errorMsg: 'Cannot play on existing stone!' };
if(ko && coord.equals(ko))
return { success: false,
errorMsg: 'Cannot retake ko immediately!' };
adjacent = this.getAdjacent(coord); // find adjacent coordinates
for(var i=0; i<adjacent.length; i++) {
var c = adjacent[i];
if(this.getType(c) == oppType) { // potential capture
var g = this.getGroup(c);
if(this.filter(g.neighbors, C.CLEAR).length == 1)
captures = captures.concat(g.group);
}
}
// Suicide not allowed
if(captures.length === 0 &&
!this.hasType(this.getGroup(coord, stone).neighbors, C.CLEAR))
return { success: false,
errorMsg: 'Suicide is not allowed!' };
// Check for ko. Note that captures were not removed so there should
// be zero liberties around this stone in case of a ko.
if(captures.length == 1 && this.filter(adjacent, C.CLEAR).length === 0)
return { success: true, captures: captures, ko: captures[0].copy() };
return { success: true, captures: captures, ko: false };
};
module.exports = Board;