'use strict';
/*jslint browser: true */
var model = require('./model.js');
var comm = require('./comm.js');
var io = require('socket.io-client');
require('../app/browser/js/SimpleBoardUI.js');
require('./rules/rule.js');
require('./rules/RuleBgCasual.js');
require('./rules/RuleBgGulbara.js');
require('./rules/RuleBgTapa.js');
/**
* Backgammon client
* @constructor
* @param {Object} config - Configuration object
* @param {string} config.containerID - ID of HTML container tag (defaults to 'backgammon')
* @param {string} config.boardID - ID of board tag (defaults to 'board')
* @param {string} config.rulePath - path to rules directory, relative to lib directory (defaults to './rules/')
* @param {string} config.boardUI - board UI filename, relative to project root (defaults to '../app/browser/js/SimpleBoardUI.js')
*/
function Client(config) {
/**
* Client's socket object
* @type {Socket}
*/
this._socket = null;
/**
* Counter used to generate unique sequence number for messages in client's session
* @type {number}
*/
this._clientMsgSeq = 0;
/**
* Map of callback functions to be executed after server replies to a message
* @type {Object}
*/
this._callbackList = {};
/**
* Dictionary of arrays, containing subscriptions for reception of messages by id/type.
* The key of the dictionary is the message ID.
* The value of the dictionary is an array with callback functions to execute when message is received.
* @type {{Array}}
*/
this._msgSubscriptions = {};
/**
* Client's player object
* @type {Player}
*/
this.player = null;
/**
* Other player object
* @type {Player}
*/
this.otherPlayer = null;
/**
* Current match
* @type {Match}
*/
this.match = null;
/**
* Rule used in current match
* @type {Rule}
*/
this.rule = null;
/**
* Default client configuration
* @type {{containerID: string, boardID: string, rulePath: string, boardUI: string}}
*/
this.config = {
'containerID': 'backgammon',
'boardID': 'board',
'rulePath': './rules/',
'boardUI': '../app/browser/js/SimpleBoardUI.js'
};
/**
* Initialize client
* @param {Object} config - Configuration object
* @param {string} config.containerID - ID of HTML container tag (defaults to 'backgammon')
* @param {string} config.boardID - ID of board tag (defaults to 'board')
* @param {string} config.rulePath - path to rules directory, relative to lib directory (defaults to './rules/')
* @param {string} config.boardUI - board UI filename, relative to project root (defaults to '../app/browser/js/SimpleBoardUI.js')
*/
this.init = function (config) {
for (var attrname in config) { this.config[attrname] = config[attrname]; }
var boardUIClass = require(this.config.boardUI);
this.boardUI = new boardUIClass(this);
this.boardUI.init();
this._openSocket();
};
/**
* Prepare socket and attach message handlers
*/
this._openSocket = function () {
var self = this;
var serverURL = this.config.serverURL;
if (!serverURL) {
serverURL = window.location.host;
}
this._socket = io.connect(serverURL, {'force new connection': true});
this._socket.on(comm.Message.CONNECT, function(){
self.handleConnect();
self.updateUI();
});
this._socket.on(comm.Message.CREATE_GUEST, function(params){
self.handleMessage(comm.Message.CREATE_GUEST, params);
});
this._socket.on(comm.Message.GET_MATCH_LIST, function(params){
self.handleMessage(comm.Message.GET_MATCH_LIST, params);
});
this._socket.on(comm.Message.PLAY_RANDOM, function(params){
self.handleMessage(comm.Message.PLAY_RANDOM, params);
});
this._socket.on(comm.Message.CREATE_MATCH, function(params){
self.handleMessage(comm.Message.CREATE_MATCH, params);
});
this._socket.on(comm.Message.JOIN_MATCH, function(params){
self.handleMessage(comm.Message.JOIN_MATCH, params);
});
this._socket.on(comm.Message.ROLL_DICE, function(params){
self.handleMessage(comm.Message.ROLL_DICE, params);
});
this._socket.on(comm.Message.MOVE_PIECE, function(params){
self.handleMessage(comm.Message.MOVE_PIECE, params);
});
this._socket.on(comm.Message.EVENT_PLAYER_JOINED, function(params){
self.handleMessage(comm.Message.EVENT_PLAYER_JOINED, params);
});
this._socket.on(comm.Message.EVENT_TURN_START, function(params){
self.handleMessage(comm.Message.EVENT_TURN_START, params);
});
this._socket.on(comm.Message.EVENT_DICE_ROLL, function(params){
self.handleMessage(comm.Message.EVENT_DICE_ROLL, params);
});
this._socket.on(comm.Message.EVENT_PIECE_MOVE, function(params){
self.handleMessage(comm.Message.EVENT_PIECE_MOVE, params);
});
this._socket.on(comm.Message.EVENT_MATCH_START, function(params){
self.handleMessage(comm.Message.EVENT_MATCH_START, params);
});
this._socket.on(comm.Message.EVENT_GAME_OVER, function(params){
self.handleMessage(comm.Message.EVENT_GAME_OVER, params);
});
this._socket.on(comm.Message.EVENT_MATCH_OVER, function(params){
self.handleMessage(comm.Message.EVENT_MATCH_OVER, params);
});
this._socket.on(comm.Message.EVENT_GAME_RESTART, function(params){
self.handleMessage(comm.Message.EVENT_GAME_RESTART, params);
});
this._socket.on(comm.Message.EVENT_UNDO_MOVES, function(params){
self.handleMessage(comm.Message.EVENT_UNDO_MOVES, params);
});
};
/**
* Message callback
*
* @callback messageCallback
* @param {number} msg - An integer.
* @param {number} clientMsgSeq - An integer.
* @param {Object} reply - Object containing reply data.
* @param {boolean} reply.result - Result of command execution
*/
/**
* Send message to server.
* @param {string} msg - Message ID
* @param {Object} [params] - Object map with message parameters
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply to this message
*/
this.sendMessage = function (msg, params, callback) {
params = params || {};
params.clientMsgSeq = ++this._clientMsgSeq;
// Store reference to callback. It will be executed when server replies to this message
this._callbackList[params.clientMsgSeq] = callback;
console.log('Sending message ' + msg + ' with ID ' + params.clientMsgSeq);
this._socket.emit(msg, params);
};
/**
* Handle connection to server.
*/
this.handleConnect = function () {
console.log('Client connected');
if (!this.player) {
this.sendMessage(comm.Message.CREATE_GUEST);
}
};
/**
* Handle reply/event message.
* @param {string} msg - Message ID
* @param {Object} params - Message parameters
*/
this.handleMessage = function (msg, params) {
console.log('Reply/event received: ' + msg);
console.log(params);
// Update match object
if ((params) && (params.match) && (this.match) &&
(this.match.id == params.match.id)) {
this.updateMatch(params.match);
}
// Process message
if (msg == comm.Message.CREATE_GUEST) {
this.handleCreateGuest(params);
}
else if (msg == comm.Message.GET_MATCH_LIST) {
this.handleGetMatchList(params);
}
else if (msg == comm.Message.PLAY_RANDOM) {
this.handlePlayRandom(params);
}
else if (msg == comm.Message.CREATE_MATCH) {
this.handleCreateMatch(params);
}
else if (msg == comm.Message.JOIN_MATCH) {
this.handleJoinMatch(params);
}
else if (msg == comm.Message.ROLL_DICE) {
this.handleRollDice(params);
}
else if (msg == comm.Message.MOVE_PIECE) {
this.handleMovePiece(params);
}
else if (msg == comm.Message.EVENT_PLAYER_JOINED) {
this.handleEventPlayerJoined(params);
}
else if (msg == comm.Message.EVENT_PIECE_MOVE) {
this.handleEventPieceMove(params);
}
else if (msg == comm.Message.EVENT_TURN_START) {
this.handleEventTurnStart(params);
}
else if (msg == comm.Message.EVENT_DICE_ROLL) {
this.handleEventDiceRoll(params);
}
else if (msg == comm.Message.EVENT_MATCH_START) {
this.handleEventMatchStart(params);
}
else if (msg == comm.Message.EVENT_GAME_OVER) {
this.handleEventGameOver(params);
}
else if (msg == comm.Message.EVENT_MATCH_OVER) {
this.handleEventMatchOver(params);
}
else if (msg == comm.Message.EVENT_GAME_RESTART) {
this.handleEventGameRestart(params);
}
else if (msg == comm.Message.EVENT_UNDO_MOVES) {
this.handleEventUndoMoves(params);
}
else {
console.log('Unknown message!');
return;
}
if (params.clientMsgSeq) {
var callback = this._callbackList[params.clientMsgSeq];
if (callback) {
callback(msg, params.clientMsgSeq, params);
delete this._callbackList[params.clientMsgSeq];
}
}
this._notify(msg, params);
this.updateUI();
};
/**
* Handle reply - Guest player created
* @param {Object} params - Message parameters
* @param {Player} params.player - Player object created
*/
this.handleCreateGuest = function (params) {
this.player = params.player;
// TODO: update UI
console.log('Created guest player (ID): ' + this.player.id);
// Store player ID as cookie. It will be used to retrieve the player
// object later, if page is reloaded.
document.cookie = 'player_id=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
document.cookie = 'player_id=' + this.player.id;
};
/**
* Handle reply - List of matfches returned
* @param {Object} params - Message parameters
*/
this.handleGetMatchList = function (params) {
// TODO: update UI
console.log('List of matches (IDs): ' + params.list.length);
};
/**
* Handle reply - Start random match
* @param {Object} params - Message parameters
*/
this.handlePlayRandom = function (params) {
// TODO: update UI
};
/**
* Handle reply - New match has been created
* @param {Object} params - Message parameters
*/
this.handleCreateMatch = function (params) {
if (!params.result) {
return;
}
console.log('Created match with ID ' + params.match.id + ' and rule ' + params.ruleName);
this.updatePlayer(params.player);
this.updateMatch(params.match);
this.updateRule(this.loadRule(params.ruleName));
this.resetBoard(this.match, this.rule);
};
/**
* Handle reply - Joined new match
* @param {Object} params - Message parameters
*/
this.handleJoinMatch = function (params) {
if (!params.result) {
return;
}
console.log('Joined match with ID ' + params.match.id + ' and rule ' + params.ruleName);
this.updatePlayer(params.guest);
this.updateOtherPlayer(params.host);
this.updateMatch(params.match);
this.updateRule(this.loadRule(params.ruleName));
this.resetBoard(this.match, this.rule);
};
/**
* Handle reply - Dice rolled
* @param {Object} params - Message parameters
*/
this.handleRollDice = function (params) {
console.log('Dice rolled');
console.log(params);
console.log(this.match.currentGame);
};
/**
* Handle reply - Piece moved
* @param {Object} params - Message parameters
*/
this.handleMovePiece = function (params) {
console.log('Piece move');
if (!params.result) {
this.boardUI.notifyError(params.errorMessage);
}
};
/**
* Handle event - Another player joined match
* @param {Object} params - Message parameters
*/
this.handleEventPlayerJoined = function (params) {
console.log('Player ' + params.guest.id + ' joined match ' + this.match.id);
this.updateOtherPlayer(params.guest);
this.boardUI.notifyInfo('Player ' + params.guest.id + ' joined match ');
};
/**
* Handle event - New turn started
* @param {Object} params - Message parameters
*/
this.handleEventTurnStart = function (params) {
console.log('Turn start');
this.boardUI.handleTurnStart();
};
/**
* Handle event - Dice rolled
* @param {Object} params - Message parameters
*/
this.handleEventDiceRoll = function (params) {
console.log('Dice rolled');
};
/**
* Handle event - Piece moved
* @param {Object} params - Message parameters
* @param {number} params.position - Position of piece being moved
* @param {PieceType} params.type - Type of piece being moved
* @param {number} params.steps - Number steps the piece is moved with
* @param {MoveAction[]} params.moveActionList - List of actions that have to be played in UI
*/
this.handleEventPieceMove = function (params) {
console.log('Piece moved');
this.boardUI.playActions(params.moveActionList);
};
/**
* Handle event - Piece moved
* @param {Object} params - Message parameters
* @param {number} params.match - Match that has been started
*/
this.handleEventMatchStart = function (params) {
console.log('Match started');
if (model.Match.isHost(params.match, this.player)) {
this.updatePlayer(params.match.host);
this.updateOtherPlayer(params.match.guest);
}
else {
this.updatePlayer(params.match.guest);
this.updateOtherPlayer(params.match.host);
}
this.updateMatch(params.match);
this.updateRule(this.loadRule(params.match.ruleName));
this.resetBoard(this.match, this.rule);
};
/**
* Handle event - Game over. Current game is over. Prepare for next game of match, if any.
* @param {Object} params - Message parameters
* @param {number} params.match - Match that has been started
*/
this.handleEventGameOver = function (params) {
console.log('Game is over. Winner:', params.winner);
this.boardUI.showGameEndMessage(params.winner);
};
/**
* Handle event - Match is over. Offer rematch or starting a new game.
* @param {Object} params - Message parameters
* @param {number} params.match - Match that has been started
*/
this.handleEventMatchOver = function (params) {
console.log('Match is over. Winner:', params.winner);
this.boardUI.showGameEndMessage(params.winner);
};
/**
* Handle event - Game restart. Current game in match is over. Match is not finished, so start next game.
* @param {Object} params - Message parameters
*/
this.handleEventGameRestart = function (params) {
console.log('Restarting game for match ' + this.match.id + ' with rule ' + this.rule.name);
this.resetBoard(this.match, this.rule);
this.boardUI.handleEventGameRestart();
};
/**
* Handle event - Undo moves
* @param {Object} params - Message parameters
*/
this.handleEventUndoMoves = function (params) {
console.log('Undoing moves for match ' + this.match.id + ' with rule ' + this.rule.name);
this.boardUI.handleEventUndoMoves();
this.resetBoard(this.match, this.rule);
};
/**
* Load rule module
* @param {string} ruleName - Rule's name, equal to rule's class name (eg. RuleBgCasual)
* @returns {Rule} - Corresponding rule object
*/
this.loadRule = function (ruleName) {
var fileName = model.Utils.sanitizeName(ruleName);
var file = this.config.rulePath + fileName + '.js';
var rule = require(file);
rule.name = fileName;
return rule;
};
/**
* Init game
* @param {Match} match - Game
* @param {Rule} rule - Rule object to use
*/
this.resetBoard = function (match, rule) {
this.boardUI.resetBoard(match, rule);
};
/**
* Update player.
*
* After an object has been updated, an update to UI should also be triggered.
*
* @param {Player} player - Updated player's object to use
*/
this.updatePlayer = function (player) {
this.player = player;
};
/**
* Update other player.
*
* After an object has been updated, an update to UI should also be triggered.
*
* @param {Player} player - Updated other player's object to use
*/
this.updateOtherPlayer = function (player) {
this.otherPlayer = player;
};
/**
* Update match object.
*
* After an object has been updated, an update to UI should also be triggered.
*
* @param {Match} match - Updated match object to use
*/
this.updateMatch = function (match) {
this.match = match;
this.boardUI.match = match;
};
/**
* Update rule object.
*
* After an object has been updated, an update to UI should also be triggered.
*
* @param {Rule} rule - Updated rule object to use
*/
this.updateRule = function (rule) {
this.rule = rule;
this.boardUI.rule = rule;
};
/**
* Trigger update of board's UI.
*/
this.updateUI = function () {
this.boardUI.updateControls();
this.boardUI.updateScoreboard();
};
/**
* Subscribe for notification on message reception
* @param {number} msgID - The type of message to subscribe for
* @param {messageCallback} [callback] - Callback function to be called on reception of this message
*/
this.subscribe = function (msgID, callback) {
this._msgSubscriptions[msgID] = this._msgSubscriptions[msgID] || [];
this._msgSubscriptions[msgID].push(callback);
console.log(this._msgSubscriptions);
};
/**
* Subscribe for notification on message reception
* @param {number} msg - The ID of the message received
* @param {Object} params - Message parameters
*/
this._notify = function (msg, params) {
var callbackList = this._msgSubscriptions[msg];
if (callbackList) {
for (var i = 0; i < callbackList.length; i++) {
console.log(callbackList[i]);
callbackList[i](msg, params);
}
}
};
/**
* Request playing a match with random player - from waiting queue.
* @param {string} ruleName - Name of rule to use (eg. RuleBgCasual)
* @param {Object} [params] - Object map with message parameters
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqPlayRandom = function (ruleName, params, callback) {
if (typeof params === 'undefined') {
params = {};
}
params.ruleName = ruleName;
this.sendMessage(
comm.Message.PLAY_RANDOM,
params,
callback
);
};
/**
* Request creating a new match.
* @param {string} ruleName - Name of rule to use (eg. RuleBgCasual)
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqCreateMatch = function (ruleName, callback) {
this.sendMessage(
comm.Message.CREATE_MATCH,
{
'ruleName': ruleName
},
callback
);
};
/**
* Request joining a specific match.
* @param {number} matchID - ID of match to join
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqJoinMatch = function (matchID, callback) {
this.sendMessage(
comm.Message.JOIN_MATCH,
{
'matchID': matchID
},
callback
);
};
/**
* Request rolling dice
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqRollDice = function (callback) {
this.sendMessage(comm.Message.ROLL_DICE, undefined, callback);
};
/**
* Confirm moves made
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqConfirmMoves = function (callback) {
this.sendMessage(comm.Message.CONFIRM_MOVES, undefined, callback);
};
/**
* Undo moves made since last confirm
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqUndoMoves = function (callback) {
this.sendMessage(comm.Message.UNDO_MOVES, undefined, callback);
};
/**
* Request moving a piece.
* @param {Piece} piece - Denormalized position from which a piece has to be moved
* @param {number} steps - Number of steps to move forward to first home position
* @param {messageCallback} [callback] - Callback function to be called when server sends a reply
*/
this.reqMove = function (piece, steps, callback) {
console.log('Move sequence: ', this.match.currentGame.moveSequence);
this.sendMessage(
comm.Message.MOVE_PIECE,
{
'piece': piece,
'steps': steps,
'moveSequence': this.match.currentGame.moveSequence
},
callback
);
};
/**
* Notify UI that DOM was rezised and UI may have to be updated
*/
this.resizeUI = function () {
this.boardUI.resizeUI();
};
this.init(config);
}
module.exports.Client = Client;