var crypto = require('crypto');
/**
* Current version supports only two colours of checkers and two players as
* the author is not yet aware of rules with three or more types of checkers
* and/or players.
* @readonly
* @enum {number}
*/
var PieceType = {
/** White piece */
WHITE : 0,
/** Black piece */
BLACK : 1
};
/**
* Common utilities
* @constructor
*/
function Utils() {
}
/**
* Generate unique ID for an object in model
* @returns {number}
*/
Utils.generateID = function () {
// TODO: Use a better approach - eg. a database ObjectID?
return Math.floor(Math.random() * 99999999);
};
/**
* Sanitize rule's name so that it is safe to use as a filename
* @param {string} name - Rule's name to sanitize (eg. 'RuleBgCasual')
* @returns {string}
*/
Utils.sanitizeName = function (name) {
return name.replace(/[^-_A-Za-z0-9]/gi, "");
};
/**
* Load rule object by path and class name
* @param {string} path - Path where rule files are stored ('../../lib/rules/' in browser sample)
* @param {string} ruleName - Name of rule - equal to rule's class name (eg. 'RuleBgCasual')
* @returns {Rule} - Rule object
*/
Utils.loadRule = function (ruleName) {
var path = './rules/';
var fileName = Utils.sanitizeName(ruleName);
var file = path + fileName + '.js';
console.log('Loading rule in file ' + file);
var rule = require(file);
rule.name = fileName;
console.log(rule);
return rule;
};
/**
* Extract item object from array
* @param {Array} array - Array of elements
* @param {Object} item - Element to remove from array
* @returns {} - The element removed from array or null, if not found
*/
Utils.extractItem = function (array, item) {
for (var i = 0; i < array.length; i++) {
if (array[i] === item) {
array.splice(i, 1);
return;
}
}
};
/**
* Remove item from array
* @param {Array} array - Array of elements
* @param {Object} item - Element to remove from array
*/
Utils.removeItem = function (array, item) {
Utils.extractItem(array, item);
};
/**
* Rotate array elements left.
*
* Example:
* [6, 4, 1] becomes [4, 1, 6]
*
* @param {Array} array - Array of elements
*/
Utils.rotateLeft = function (array) {
array.unshift(array.pop());
};
/**
* Create shallow copy of object.
*
* @param {Object} oldObj - Object to copy
* @returns {Object} - Shallow copy of object
*/
Utils.shallowCopy = function (oldObj) {
var newObj = {};
for(var i in oldObj) {
if(oldObj.hasOwnProperty(i)) {
newObj[i] = oldObj[i];
}
}
return newObj;
};
/**
* Create deep copy of a value object.
* The object should have no functions/methods.
*
* @param {Object} oldObj - Object to copy
* @returns {Object} - Deep copy of object
*/
Utils.deepCopy = function (oldObj) {
return JSON.parse(JSON.stringify(oldObj));
};
/**
* Simulate server load/hosting throttling
*
* @param {Object} oldObj - Object to copy
* @returns {Object} - Deep copy of object
*/
Utils.simulateServerLoad = function () {
for (var i = 0; i < 100000; i++) {
for (var k = 1; k < 1000; k++) {
var q = Math.sqrt(1000000007);
}
}
};
/**
* Get random element from an array
*
* @param {Array} arr - Array to choose element from
* @returns {Object} - Random element in array
*/
Utils.getRandomElement = function (arr) {
var idx = Math.floor(Math.random() * arr.length);
return arr[idx];
};
/**
* Random generator.
* @constructor
*/
function Random() {
}
/**
* Get random number from 1 to 6
* @returns {number} - Random value from 1 to 6
*/
Random.get = function() {
// TODO: replace with quality random generator
// Combine Math random generator with crypto one
var buffer = crypto.randomBytes(1);
var value = buffer.readUInt8(0);
console.log(value);
if (value > 255) {
value = 255;
}
var k = value / 256;
return (Math.floor(k * 6) + 1);
};
/**
* Pieces are round checkers that are being moved around the board.
* @constructor
* @param {PieceType} type - Type of piece
* @param {number} id - ID of piece
*/
function Piece(type, id) {
/**
* Type of piece (white/black)
* @type {PieceType}
*/
this.type = type;
/**
* ID of piece
* @type {number}
*/
this.id = id;
}
/**
* Dice with basic functionality to roll using good random generator.
* @constructor
*/
function Dice() {
/**
* Values of the two dice
* @type {Array}
*/
this.values = [0, 0];
/**
* List of moves the player can make. Usually moves are equal to values,
* but in most rules doubles (eg. 6:6) are played four times, instead of
* two, in which case moves array will contain four values in stead of
* only two (eg. [6, 6, 6, 6]).
* @type {Array}
*/
this.moves = [];
/**
* After dice is rolled, movesLeft contains the same values as moves.
* When the player makes a move, the corresponding value is removed from
* movesLeft array. If the player wants to undo the moves made, movesLeft is
* replaced with moves.
* @type {Array}
*/
this.movesLeft = [];
/**
* After a piece is moved, the value of the die used is added to movesPlayed array.
* @type {Array}
*/
this.movesPlayed = [];
}
/**
* Roll dice and return result as a new Dice object
* @returns {Dice} - New dice with random values
*/
Dice.roll = function() {
var dice = new Dice();
dice.values[0] = Random.get();
dice.values[1] = Random.get();
dice.values.sort(function (a, b) { return b - a; });
return dice;
};
/**
* Roll dice and return result as a new Dice object
* @param {Dice} dice - New dice with random values
* @param {number} move - New dice with random values
*/
Dice.markAsPlayed = function (dice, move) {
for (var i = 0; i < dice.movesLeft.length; i++) {
if (dice.movesLeft[i] === move) {
dice.movesPlayed.push(dice.movesLeft[i]);
dice.movesLeft.splice(i, 1);
return;
}
}
throw new Error("No such move!");
};
/**
* Check if the dice object has double (equal) values.
* @param {Dice} dice - New dice with random values
* @returns {boolean} - True if dice object has dobule values, false otherwise
*/
Dice.isDouble = function (dice) {
return dice.values[0] === dice.values[1];
};
/**
* Get remaining moves from dice object - moves that have not been played.
* @param {Dice} dice - Dice object
* @returns {Array} - Array containing remaining move values
*/
Dice.getRemainingMoves = function (dice) {
var remaining = [];
var played = [];
played = played.concat(dice.movesPlayed);
for (var i = 0; i < dice.moves.length; i++) {
var index = played.indexOf(dice.moves[i]);
if (index >= 0) {
played.splice(index, 1);
}
else {
remaining.push(dice.moves[i]);
}
}
return remaining;
};
/**
* State contains points and pieces and very basic methods to move pieces
* around without enforcing any rules. Those methods are responsible for
* required changes to internal state only, the UI layer should handle
* graphical movement of pieces itself.
* @constructor
*/
function State() {
/**
* All popular variants of the game have a total of 24 positions on the board
* and two positions outside - the place on the bar where pieces go when
* hit and the place next to board where pieces go when beared off.
* Number of positions is not strictly defined here to allow more options
* when creating new rules.
* The points, bar, outside and pieces properties should be initialized by the
* Rule object. Each element in those properties should contain a stack
* (last in, first out).
* @type {Array}
*/
this.points = [];
/**
* Players have separate bar places and so separate list.
* First element of array is for white pieces and second one for black.
* @type {Array[]}
*/
this.bar = [[],[]];
this.whiteBar = this.bar[PieceType.WHITE];
this.blackBar = this.bar[PieceType.BLACK];
/**
* Players have separate outside places and so separate list.
* First element of array is for white pieces and second one for black.
* @type {Array[]}
*/
this.outside = [[],[]];
this.whiteOutside = this.outside[PieceType.WHITE];
this.blackOutside = this.outside[PieceType.BLACK];
/**
* A two dimensional array is also used to store references to all white and
* black pieces independent of their position - just for convenience.
*/
this.pieces = [[],[]];
this.whitePieces = this.pieces[PieceType.WHITE];
this.blackPieces = this.pieces[PieceType.BLACK];
/**
* Counter for generating unique IDs for pieces within this state
* @type {number}
*/
this.nextPieceID = 1;
}
/**
* Clear state
* @param {State} state - Board state
*/
State.clear = function(state) {
state.nextPieceID = 1;
for (var i = 0; i < state.points.length; i++) {
state.points[i].length = 0;
}
state.whiteBar.length = 0;
state.blackBar.length = 0;
state.whiteOutside.length = 0;
state.blackOutside.length = 0;
state.whitePieces.length = 0;
state.blackPieces.length = 0;
};
/**
* Count number of pieces of specified type at selected position
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @param {PieceType} type - Piece type
* @returns {number} - Number of pieces of specified type
*/
State.countAtPos = function(state, position, type) {
var cnt = 0;
for (var i = 0; i < state.points[position].length; i++) {
if (state.points[position][i].type == type) {
cnt++;
}
}
return cnt;
};
/**
* Count number of all pieces at selected position, regardless of type
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @returns {number} - Number of pieces of specified type
*/
State.countAllAtPos = function(state, position) {
var cnt = 0;
for (var i = 0; i < state.points[position].length; i++) {
cnt++;
}
return cnt;
};
/**
* Check if there are no pieces at the specified point
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @returns {boolean} - Returns true if there are no pieces at that point
*/
State.isPosFree = function(state, position) {
return state.points[position].length <= 0;
};
/**
* Get top piece, checking type in the process
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @param {PieceType} type - Piece type
* @returns {boolean} - Returns true if top piece at position is of the specified type. Returns false if there are no pieces at that point.
*/
State.checkTopPieceType = function(state, position, type) {
if (state.points[position].length > 0) {
var numPieces = state.points[position].length;
var piece = state.points[position][numPieces - 1];
if (piece.type === type) {
return true;
}
}
return false;
};
/**
* Get top piece at specified position
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @returns {Piece} - Returns type of top piece or null if there are no pieces at that position
*/
State.getTopPiece = function(state, position) {
var point = state.points[position];
//console.log(state, position);
if (point.length === 0) {
return null;
}
return point[point.length - 1];
};
/**
* Get type of top piece at specified position
* @param {State} state - Board state
* @param {number} position - Denormalized point position
* @returns {PieceType} - Returns type of top piece or null if there are no pieces at that position
*/
State.getTopPieceType = function(state, position) {
var point = state.points[position];
//console.log(state, position);
if (point.length === 0) {
return null;
}
return point[point.length - 1].type;
};
/**
* Check if there are any pieces on the bar.
* @param {State} state - State to check
* @param {PieceType} type - Type of piece (white/black)
* @returns {boolean} - True if there are any pieces on the bar
*/
State.havePiecesOnBar = function(state, type) {
return state.bar[type].length > 0;
};
/**
* Check if a specific piece is on the bar.
* @param {State} state - State to check
* @param {Piece} piece - Piece
* @returns {boolean} - True if there are any pieces on the bar
*/
State.isPieceOnBar = function(state, piece) {
var bar = state.bar[piece.type];
for (var i = 0; i < bar.length; i++) {
if (bar[i].id === piece.id) {
return true;
}
}
return false;
};
/**
* Check if the piece is outside.
* @param {State} state - State to check
* @param {Piece} piece - Piece
* @returns {boolean} - True if there are any pieces on the bar
*/
State.isPieceOutside = function(state, piece) {
var outside = state.outside[piece.type];
for (var i = 0; i < outside.length; i++) {
if (outside[i].id === piece.id) {
return true;
}
}
return false;
};
/**
* Get top piece at bar
* @param {State} state - Board state
* @param {PieceType} type - Type of piece (white/black)
* @returns {Piece} - Returns type of top piece or null if there are no pieces at that position
*/
State.getBarTopPiece = function(state, type) {
var point = state.bar[type];
if (point.length === 0) {
return null;
}
return point[point.length - 1];
};
/**
* Get position of piece on board. Return null if the piece is on bar or outside the board.
* @param {State} state - Game
* @param {Piece} piece - Piece
* @returns {number} - Position of piece on board or null if on bar/outside the board
*/
State.getPiecePos = function (state, piece) {
var i, k;
for (i = 0; i < state.points.length; i++) {
for (k = 0; k < state.points[i].length; k++) {
if (state.points[i][k].id === piece.id) {
return i;
}
}
}
return null;
};
/**
* Creates a deep copy of state object
* @param {State} state - Game state
* @returns {State} - New state object
*/
State.clone = function (state) {
var newState = Utils.deepCopy(state);
newState.whiteBar = newState.bar[PieceType.WHITE];
newState.blackBar = newState.bar[PieceType.BLACK];
newState.whiteOutside = newState.outside[PieceType.WHITE];
newState.blackOutside = newState.outside[PieceType.BLACK];
newState.whitePieces = newState.pieces[PieceType.WHITE];
newState.blackPieces = newState.pieces[PieceType.BLACK];
return newState;
};
/**
* Player's statistics
* @constructor
*/
function PlayerStats() {
/**
* Total number of wins
* @type {number}
*/
this.wins = 0;
/**
* Total number of loses
* @type {number}
*/
this.loses = 0;
/**
* Percent of doubles rolled relative to total number of dice rolled
* @type {number}
*/
this.doubles = 0;
}
/**
* Player
* @constructor
*/
function Player() {
/**
* Unique ID
* @type {number}
*/
this.id = 0;
/**
* Username
* @type {string}
*/
this.name = '';
/**
* Reference to current match
* @type {Match}
*/
this.currentMatch = null;
/**
* Reference to current game
* @type {Game}
*/
this.currentGame = null;
/**
* Reference to rule for current game
* @type {Rule}
*/
this.currentRule = null;
/**
* Player's piece type for current game
* @type {PieceType}
*/
this.currentPieceType = null;
/**
* Player's statistics
* @type {PlayerStats}
*/
this.stats = new PlayerStats();
// TODO: Remove socketID from this class
/**
* ID of player's socket
* @type {string}
*/
this.socketID = null;
}
/**
* Create new player object with unique ID.
* Player object is not saved to database.
* @returns {Player} - New player object
*/
Player.createNew = function() {
var player = new Player();
player.id = Utils.generateID();
return player;
};
/**
* Game
* @constructor
*/
function Game() {
/**
* Unique ID of game
* @type {number}
*/
this.id = 0;
/**
* Board state
* @type {State}
*/
this.state = null;
/**
* Flag that shows if game has started
* @type {boolean}
*/
this.hasStarted = false;
/**
* Flag that shows if game is over/has finished
* @type {boolean}
*/
this.isOver = false;
/**
* Number (index) of turn
* @type {number}
*/
this.turnNumber = 0;
/**
* Show which player's turn it is
* @type {Player}
*/
this.turnPlayer = null;
/**
* Dice for current turn. Should be null if dice haven't been rolled yet.
* @type {Dice}
*/
this.turnDice = null;
/**
* Flag that shows if the moves made in current turn have been confirmed by the player.
* @type {boolean}
*/
this.turnConfirmed = false;
/**
* Previous game state, used for undoing moves
*/
this.previousState = null;
/**
* Previous dice state, used for undoing moves
*/
this.previousTurnDice = null;
/**
* Sequence number that is incremented each time a piece is moved during the game
*/
this.moveSequence = 0;
}
/**
* Create new game object with unique ID and initialize it.
* Game object is not saved in database.
* @param {Rule} rule - Rule object to use
* @returns {Game} - A new game object with unique ID
*/
Game.createNew = function(rule) {
var game = new Game();
game.id = Utils.generateID();
Game.init(game, rule);
return game;
};
/**
* Initialize game object
* @param {Game} game - Game to initialize
* @param {Rule} rule - Rule to use
*/
Game.init = function (game, rule) {
game.state = new State();
rule.initialize(game.state);
rule.resetState(game.state);
};
/**
* Check if it is specified player's turn
* @param {Game} game - Game
* @param {Player} player - Specified player
* @returns {boolean} - True if it is specified player's turn
*/
Game.isPlayerTurn = function (game, player) {
return (game.turnPlayer) &&
(player) &&
(game.turnPlayer.id === player.id);
};
/**
* Check if it is specified player's turn, but check by their piece type, and not player object
* @param {Game} game - Game
* @param {PieceType} type - Player's piece type
* @returns {boolean} - True if it is specified player's turn
*/
Game.isTypeTurn = function (game, type) {
return (game.turnPlayer) &&
(game.turnPlayer.currentPieceType === type);
};
/**
* Check if dice has been rolled
* @param {Game} game - Game
* @returns {boolean} - True if dice has been rolled (turnDice is not null)
*/
Game.diceWasRolled = function (game) {
return (game.turnDice != null);
};
/**
* Check if there are more moves to make
* @param {Game} game - Game
* @returns {boolean} - True if there are any moves left to make
*/
Game.hasMoreMoves = function (game) {
return (game.turnDice) &&
(game.turnDice.movesLeft.length > 0);
};
/**
* Check if a specific move value is available in movesLeft
* @param {Game} game - Game
* @param {number} value - Move value to check for
* @returns {boolean} - True if specified move value is available
*/
Game.hasMove = function (game, value) {
return (game.turnDice) &&
(game.turnDice.movesLeft.indexOf(value) > -1);
};
/**
* Store current game state - in case the player wants to undo moves later
* @param {Game} game - Game
*/
Game.snapshotState = function (game) {
game.previousState = State.clone(game.state);
game.previousTurnDice = Utils.deepCopy(game.turnDice);
};
/**
* Restore game state from last snapshot - if player requested to undoing of moves
* @param {Game} game - Game
*/
Game.restoreState = function (game) {
game.state = State.clone(game.previousState);
game.turnDice = Utils.deepCopy(game.previousTurnDice);
};
/**
* Match
* @constructor
*/
function Match() {
/**
* Unique ID of match object
* @type {number}
*/
this.id = 0;
/**
* Player that created the match
* @type {Player}
*/
this.host = null;
/**
* Player that joined the match
* @type {Player}
*/
this.guest = null;
/**
* List of all players participating in the match
* @type {Array}
*/
this.players = [];
/**
* Name of the rule used for current match.
* Equals the class name of the rule (eg. 'RuleBgCasual').
* @type {string}
*/
this.ruleName = '';
/**
* Match length - the score needed to win the match.
* @type {number}
*/
this.length = 5;
/**
* Score of players for current match
* @type {Array}
*/
this.score = [];
/**
* Current game
* @type {Game}
*/
this.currentGame = null;
/**
* Is match over
* @type {boolean}
*/
this.isOver = false;
}
/**
* Create new match object with unique ID and initialize it.
* Match object is not saved in database.
* @param {Rule} rule - Rule object to use
* @returns {Match} - A new match object with unique ID
*/
Match.createNew = function(rule) {
var match = new Match();
match.id = Utils.generateID();
match.ruleName = rule.name;
match.score = [0, 0];
return match;
};
/**
* Create new game for this match. Fill game object with data from match
* Game object is not saved in database.
* @param {Rule} rule - Rule object to use
* @returns {Match} - A new match object with unique ID
*/
Match.createNewGame = function(match, rule) {
var game = Game.createNew(rule);
match.currentGame = game;
return game;
};
/**
* Add host player to match
* @param {Match} match - Match to add player to
* @param {Player} player - Player to add
* @throws Throws error if the match already has a host player
*/
Match.addHostPlayer = function (match, player) {
if (match.host)
{
throw new Error("Match already has a host player!");
}
match.host = player;
match.players.push(player);
};
/**
* Add guest player
* @param {Match} match - Match to add player to
* @param {Player} player - Player to add
* @throws Throws error if the match already has a guest player
*/
Match.addGuestPlayer = function (match, player) {
if (match.guest)
{
throw new Error("Match already has a guest player!");
}
match.guest = player;
match.players.push(player);
};
/**
* Check if specified player is the host of the match
* @param {Match} match - Match
* @param {Player} player - Specified player
* @returns {boolean} - True if there is a host player and their ID matches that of the player parameter
*/
Match.isHost = function (match, player) {
return (match.host) &&
(player) &&
(match.host.id === player.id);
};
/**
* Check if another player has joined the match
* @param {Match} match - Match
* @returns {boolean} - True if a another player has joined the match
*/
Match.hasGuestJoined = function (match) {
return (match.guest != null);
};
/**
* Move action types are determined by rules. This is only a default list
* of actions that are shared by most rules.
* @readonly
* @enum {string}
*/
var MoveActionType = {
/** MOVE: Move piece from one point to another */
MOVE: 'move',
/** RECOVER: Recover piece from bar and place it on board */
RECOVER: 'recover',
/** HIT: Hit opponent's piece and sent it to bar */
HIT: 'hit',
/** BEAR: Bear piece - move it outside the board */
BEAR: 'bear'
};
/**
* Actions that can result from making a piece move.
* Rules can assign additional properties to those, depending on the action
* type
* @constructor
*/
function MoveAction() {
/**
* Action type, depends on rule (eg. move, bear, hit)
* @type {MoveActionType|string}
*/
this.type = '';
}
module.exports = {
'PieceType': PieceType,
'Utils': Utils,
'Random': Random,
'Piece': Piece,
'Dice': Dice,
'State': State,
'PlayerStats': PlayerStats,
'Player': Player,
'Game': Game,
'Match': Match,
'MoveActionType': MoveActionType,
'MoveAction': MoveAction
};