shiro
Version:
Online quiz game engine, inspired by russian tv show 'What? Where? When?' (Million Dollar Mind Game).
1,577 lines (1,303 loc) • 38.3 kB
JavaScript
var _ = require('lodash')
, level = require('level')
, async = require('async')
, merge = require('deeply')
// local
, unidecode = require('./unidecode')
// token
, token =
{ meta : 'meta'
, team : 'team'
, state : 'state'
, question: 'question'
}
, templateTeam =
{ login : ''
, name : ''
, password : ''
, online : false
, points : 0
, time_bonus: 0
, adjustment: 0
, visibility: false
, answers : {}
}
, templateQuestion =
{ index : 0
, text : ''
, answer: ''
, played: false
}
// globals
, admin = {} // admin socket.id back reference
, teams = {} // teams socket.id back reference
// making sense to state
, toBeAdmin = {} // keeps reference to the sockets claimed to be an admin
, toBeTeam = {} // keeps reference to the sockets claimed to be a team
;
module.exports = Game;
function Game(options)
{
// environment
this._env = options.env || function(){};
// db file
this._storage = options.storage;
// db instance
this._db = undefined;
// meta data
this.meta = {};
// list of teams
this.team = {};
// questions (with answers)
this.question = {};
// dirty work
this._timerCountdown = this._timerCountdownStub.bind(this);
}
Game.prototype.init = function Game_init(callback)
{
var _game = this
, now
;
// connect to db and get stuff
level(this._storage, {keyEncoding: 'utf8', valueEncoding: 'json'}, function(err, db)
{
if (err) return callback(err);
_game._db = db;
async.series(
{ meta : _game._fetchSlice.bind(_game, token.meta)
, team : _game._fetchSlice.bind(_game, token.team)
, state : _game._fetchSlice.bind(_game, token.state)
, question: _game._fetchSlice.bind(_game, token.question)
}, function(err, res)
{
if (err) return callback(err);
_game.meta = res.meta;
_game.state = res.state;
// filter it thru template object
_game.team = _.each(res.team, function(team, key){ res.team[key] = merge(templateTeam, team); }) && res.team;
_game.question = _.each(res.question, function(question, key){ res.question[key] = merge(templateQuestion, question); }) && res.question;
// keep state sane
if (_game.state['current_question'] && !_game.question[_game.state['current_question']])
{
_game.state['current_question'] = false;
_game.state['display'] = false;
}
// check for ongoing timer
if (_game.state.timer)
{
// and start it again
_game._startTimerCountdown();
_game._showQuestion();
}
// {{{ create game instance id
// to prevent user name collisions
// after db reset
if (!_game.meta.instance)
{
now = process.hrtime();
_game.save('meta', 'instance', Date.now().toString(36) + now[1].toString(36) + now[0].toString(36), callback);
}
else
{
callback(null);
}
});
});
// set event listeners
this._initEventListeners();
}
// saves data to db
Game.prototype.save = function Game_save(channel, key, value, callback)
{
var _game = this;
this._db.put(token[channel]+':'+key, value, function(err)
{
var log = {};
if (err) return callback(err);
// console.log is the best backup/aof
log[token[channel]+':'+key] = value;
console.log(JSON.stringify(log));
// update local
_game[channel][key] = value;
return callback(null);
});
}
// loads data from db
Game.prototype.load = function Game_load(channel, key, callback)
{
var _game = this;
this._db.get(token[channel]+':'+key, function(err, value)
{
if (err)
{
// reset local
if (key in _game[channel])
{
delete _game[channel][key];
}
if (err.notFound)
{
return callback(null, undefined);
}
return callback(err);
}
// update local
_game[channel][key] = value;
return callback(null, value);
});
}
// deletes data from db
Game.prototype.delete = function Game_delete(channel, key, callback)
{
var _game = this;
this._db.del(token[channel]+':'+key, function(err)
{
if (err) return callback(err);
// update local
delete _game[channel][key];
return callback(null);
});
}
// authenticates as admin or team user
Game.prototype.auth = function Game_auth(user, callback)
{
var _game = this
, info
, userData
;
if (!user.login || !user.password) return callback({code: 400, message: 'Missing data.'});
if (user.login == 'admin')
{
if (this._env('admin') != user.password) return callback({code: 400, message: 'Wrong password.'});
userData = {login: 'admin', password: user.password};
userData.socketid = user.socketid;
// store temporarily
admin[user.socketid] = userData;
}
else if (this.team[user.login])
{
if (this.team[user.login].password != user.password) return callback({code: 400, message: 'Wrong team/password combination.'});
// set online flag
this.team[user.login].online = true;
userData = this.team[user.login];
userData.socketid = user.socketid;
// store temporarily
teams[user.socketid] = userData;
}
else
{
return callback({code: 401, message: 'Could not recognize provided login.'});
}
// done here, no need to save it here
// since after restart socketid and online:true flag
// would useless and wrong
callback(null, userData);
}
// cleans up disconnected user
Game.prototype.left = function Game_left(user, callback)
{
var _game = this
, info
, userData
;
if (!user.socketid)
{
return callback({code: 400, message: 'Missing data.', wtf: user});
}
// cleanup
delete toBeAdmin[user.socketid];
delete toBeTeam[user.socketid];
if (teams[user.socketid])
{
info = 'team';
// get team data object
userData = this.team[teams[user.socketid].login];
// cleanup user object
delete teams[user.socketid];
}
else if (admin[user.socketid])
{
info = 'meta';
// get admin data object
userData = this.meta['admin'] || _.omit(admin[user.socketid], 'socketid');
// cleanup user object
delete admin[user.socketid];
}
else
{
// whatever
return callback(null, {});
}
// if team was deleted here is no traces of it
if (!userData) return callback(null, {});
// kill online flag
userData.online = false;
// resave
this.save(info, userData.login, userData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, userData);
});
}
// Attaches reference to known external objects
Game.prototype.attach = function Game_attach(collection)
{
// chat reference
if ('chat' in collection)
{
this._chat = collection['chat'];
}
// websockets reference
if ('sockets' in collection)
{
this._sockets = collection['sockets'];
}
}
// Sets timer on/off
Game.prototype.setTimer = function Game_setTimer(timer, callback)
{
var _game = this
, timerData
;
if (!timer)
{
return callback({code: 400, message: 'Missing data.'});
}
if (timer == 'on' && this.state['timer'])
{
return callback({code: 400, message: 'Timer has already started.'});
}
else if (timer == 'off' && !this.state['timer'])
{
return callback({code: 400, message: 'No timer to turn off.'});
}
if (timer == 'off')
{
timerData = false;
// kill countdown
if (this.state['timer'].countdown)
{
clearInterval(this.state['timer'].countdown);
}
}
else
{
timerData = { start: process.hrtime() };
}
// save
this.save('state', 'timer', timerData, function(err)
{
if (err) return callback({code: 500, message: err});
// start timer countdown
if (timerData)
{
_game._startTimerCountdown();
_game._showQuestion();
}
return callback(null, timerData);
});
}
// Record answer from a team
Game.prototype.addAnswer = function Game_addAnswer(data, callback)
{
var _game = this
, diff
, timeBonus
, timer
, question
, teamData
;
if (!data.login || typeof data.answer != 'object' || !('text' in data.answer))
{
return callback({code: 400, message: 'Missing data.'});
}
if (!(teamData = this.team[data.login]))
{
return callback({code: 404, message: 'Team '+data.login+' does not exist.'});
}
if (!(timer = this.state['timer']) || !(question = this.state['current_question']))
{
return callback({code: 403, message: 'Answers accepted only during minute countdown.'});
}
if (teamData.answers && teamData.answers[question])
{
return callback({code: 403, message: 'Only one answer per question is allowed.'});
}
// calculate time difference
diff = process.hrtime(timer.start);
timeBonus = 60000 - (diff[0] * 1000 + Math.floor(diff[1] / 1e6));
// add answer
teamData.answers[question] = {text: data.answer.text, time: diff, bonus: timeBonus, correct: null};
// save
this.save('team', teamData.login, teamData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, teamData);
});
}
// Evals team's answer
Game.prototype.evalAnswer = function Game_evalAnswer(data, callback)
{
var _game = this
, teamData
;
if (!data.question || !data.team || !data.status)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!(teamData = this.team[data.team]))
{
return callback({code: 404, message: 'Team '+data.team+' does not exist.'});
}
if (!teamData.answers[data.question])
{
return callback({code: 404, message: 'Team '+data.team+' does not have answer for the question #'+data.question+'.'});
}
// set correct flag
teamData.answers[data.question].correct = data.status == 'correct' ? true : false;
// update points
teamData = this._recalculatePoints(teamData);
// save
this.save('team', teamData.login, teamData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, teamData);
});
}
// Adds new team
Game.prototype.addTeam = function Game_addTeam(team, callback)
{
var _game = this
, teamData
;
if (!team.login || !team.name || !team.password)
{
return callback({code: 400, message: 'Missing data.'});
}
if (this.team[team.login])
{
return callback({code: 400, message: 'Team '+team.login+' already exists.'});
}
// create team data
teamData = merge(templateTeam, {login: team.login, name: team.name, password: team.password});
// save
this.save('team', teamData.login, teamData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, teamData);
});
}
// Updates existing team
Game.prototype.updateTeam = function Game_updateTeam(team, callback)
{
var _game = this
, teamData
;
if (!team.login)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!this.team[team.login])
{
return callback({code: 404, message: 'Team '+team.login+' does not exist.'});
}
// calculate adjustment
if ('points' in team)
{
team.adjustment = this.team[team.login].adjustment + (+(team.points || 0) - this.team[team.login].points );
}
// if answers changes recalculate points
if ('answers' in team)
{
team = this._recalculatePoints(team);
}
// deep merge team data
teamData = merge(this.team[team.login], team);
// save
this.save('team', teamData.login, teamData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, teamData);
});
}
// Deletes existing team
Game.prototype.deleteTeam = function Game_deleteTeam(team, callback)
{
var _game = this
, teamData
;
if (!team.login)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!this.team[team.login])
{
return callback({code: 404, message: 'Team '+team.login+' does not exist.'});
}
// last reference to the team data
teamData = this.team[team.login];
// delete
this.delete('team', teamData.login, function(err)
{
var team;
if (err) return callback({code: 500, message: err});
// delete user from chat
_game._chat.deleteUser('_team_'+teamData.login, function(){/* whatever */});
// kick deleted team out
if ((team = _.find(teams, {login: teamData.login})) && team.socketid)
{
_game._sockets.connections[team.socketid].write({ 'you:action': 'refresh' });
}
return callback(null, teamData);
});
}
// --- Questions
// Sets current question (in play)
Game.prototype.setQuestion = function Game_setQuestion(question, callback)
{
var _game = this
, questionData
;
if (!('index' in question))
{
return callback({code: 400, message: 'Missing data.'});
}
if (question.index && !this.question[question.index])
{
return callback({code: 404, message: 'Question '+question.index+' does not exist.'});
}
if (this.state['current_question'] == question.index)
{
if (question.index)
{
return callback({code: 400, message: 'Question '+question.index+' is already in play.'});
}
else
{
return callback({code: 400, message: 'No question to unset.'});
}
}
// mark previous question as played, if answer for the question been shown
if (this.state['current_question'] && this.question[this.state['current_question']] && this.question[this.state['current_question']].answer_shown)
{
this.updateQuestion({index: this.state['current_question'], played: true}, function Game_setQuestion_updateQuestion_callback(err, prevQuestion)
{
if (err) return console.log(['Could not update current question', err, _game.state['current_question']]);
_game._sockets.write({ 'game:question_updated': _.omit(prevQuestion, ['text', 'answer']) });
});
}
// reset displayed questions
this._showQuestion(false);
// get question data or null
questionData = question.index ? this.question[question.index] : {index: false};
// save
this.save('state', 'current_question', questionData.index, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, questionData);
});
}
// Adds new question
Game.prototype.addQuestion = function Game_addQuestion(data, callback)
{
var _game = this
, questionData
;
if (!data.text || !data.answer)
{
return callback({code: 400, message: 'Missing data.'});
}
// create team data
questionData = {text: data.text, answer: data.answer};
// add index
questionData.index = _.keys(this.question).length + 1;
// save
this.save('question', questionData.index, questionData, function(err)
{
if (err) return callback({code: 500, message: err});
return callback(null, questionData);
});
}
// Updates existing question
Game.prototype.updateQuestion = function Game_updateQuestion(question, callback)
{
var _game = this
, questionData
;
if (!question.index)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!this.question[question.index])
{
return callback({code: 404, message: 'Question '+question.index+' does not exist.'});
}
// reset answer_shown along with played
if (this.question[question.index].played && question.played === false)
{
question.answer_shown = false;
}
// deep merge team data
questionData = merge(this.question[question.index], question);
// save
this.save('question', questionData.index, questionData, function(err)
{
if (err) return callback({code: 500, message: err});
// update currently displaying question
if (_game.state['display'] && _game.state['display'].question && _game.state['display'].question.index == questionData.index)
{
// refresh displayed question
_game._showQuestion();
}
return callback(null, questionData);
});
}
// Deletes existing question
Game.prototype.deleteQuestion = function Game_deleteQuestion(question, callback)
{
var _game = this
, questionData
;
if (!question.index)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!this.question[question.index])
{
return callback({code: 404, message: 'Question '+question.index+' does not exist.'});
}
// last reference to the question data
questionData = this.question[question.index];
// delete
this.delete('question', questionData.index, function(err)
{
if (err) return callback({code: 500, message: err});
// reset current question
if (_game.state['current_question'] == questionData.index)
{
// reset displayed questions
_game._showQuestion(false);
// reset current question
_game.state['current_question'] = false;
_game._sockets.write({ 'game:current_question': {index: false} });
}
// update teams
async.eachSeries(_.values(_game.team), function(team, cb)
{
// remove deleted question
if (team.answers && team.answers[questionData.index])
{
delete team.answers[question.index];
// update points
team = _game._recalculatePoints(team);
// update team source
_game.updateTeam(_.pick(team, ['login', 'points', 'time_bonus', 'answers']), function(err, teamData)
{
if (err) return cb(err);
// update team upstream
// send different data to the admin and to others
_.each(_game._sockets.connections, function(s, id)
{
s.write({ 'game:team_updated': _game._teamStripAnswers(id, teamData) });
});
cb(null);
});
}
else
{
cb();
}
}, function(err)
{
if (err) return callback(err);
return callback(null, questionData);
});
});
}
Game.prototype.resetScoreboard = function Game_resetScoreboard(callback)
{
var _game = this
;
// reset teams' answers, current state and questions' played flags
async.series(
{ teams : _game._resetTeams.bind(this)
, state : _game._resetState.bind(this)
, questions: _game._resetQuestions.bind(this)
}, function(err, res)
{
if (err) return callback(err);
callback(null);
});
}
// Fetches question data
Game.prototype.getQuestion = function Game_getQuestion(question, callback)
{
var _game = this
;
if (!question.index)
{
return callback({code: 400, message: 'Missing data.'});
}
if (!this.question[question.index])
{
return callback({code: 404, message: 'Question '+question.index+' does not exist.'});
}
// fetch
this.load('question', question.index, function(err, questionData)
{
if (err) return callback({code: 500, message: err});
return callback(null, questionData);
});
}
Game.prototype._recalculatePoints = function Game__recalculatePoints(team)
{
// update total time_bonus
team['time_bonus'] = _.reduce(team.answers, function(bonus, answer)
{
return answer.correct ? bonus + (answer.bonus || 0) : +(bonus || 0);
}, 0);
// update total points
team.points = _.reduce(team.answers, function(points, answer)
{
return answer.correct ? points+1 : +points;
}, 0);
// apply adjustment
team.points += team.adjustment;
return team;
}
// --- Reset subroutines
Game.prototype._resetTeams = function Game__startTimerCountdown(callback)
{
var _game = this
, channel = 'team'
, ops = []
, key
;
for (key in this.team)
{
this.team[key] = _.merge(this.team[key], {points: 0, time_bonus: 0, adjustment: 0});
// since merge is deep, make it dead simple
this.team[key].answers = {};
ops.push({type: 'put', key: token[channel]+':'+key, value: this.team[key]});
}
// update
this._db.batch(ops, callback);
}
Game.prototype._resetState = function Game__startTimerCountdown(callback)
{
var _game = this
, channel = 'state'
, ops = []
, key
;
for (key in this.state)
{
this.state[key] = false;
ops.push({type: 'put', key: token[channel]+':'+key, value: false});
}
// update
this._db.batch(ops, callback);
}
Game.prototype._resetQuestions = function Game__startTimerCountdown(callback)
{
var _game = this
, channel = 'question'
, ops = []
, key
;
for (key in this.question)
{
this.question[key] = _.merge(this.question[key], {played: false, answer_shown: false});
ops.push({type: 'put', key: token[channel]+':'+key, value: this.question[key]});
}
// update
this._db.batch(ops, callback);
}
// --- Internal logic lives here
// starts countdown interval counter
Game.prototype._startTimerCountdown = function Game__startTimerCountdown()
{
this.state['timer'].countdown = setInterval(this._timerCountdown, 333); // three times a second should be precise enough
// and start it already
setTimeout(this._timerCountdown, 0);
}
Game.prototype._timerCountdownStub = function Game__timerCountdown()
{
var _game = this;
var diff = process.hrtime(this.state['timer'].start);
if (this._sockets)
{
this._sockets.write({'game:timer': {tick: diff[0], nano: diff[1]} });
}
// check the limits
if (diff[0] >= 60)
{
// turn off timer
this.setTimer('off', function Game_setTimer_off_callback(err, timer)
{
if (err) return console.log(['Could not set timer off from countdown interval', err]);
_game._sockets.write({'game:timer': false });
});
}
}
// display current question
Game.prototype._showQuestion = function Game__showQuestion(show)
{
var _game = this
, displayData
;
if (arguments.length < 1)
{
show = true;
}
// check if current question is selected
if (!show || !this.state['current_question'] || !this.question[this.state['current_question']])
{
displayData = false;
}
else
{
// get question without answer
displayData = {question: _.omit(this.question[this.state['current_question']], 'answer')};
}
// save
this.save('state', 'display', displayData, function(err)
{
if (err) return console.log(['Could not set current question to display', err]);
_game._sockets.write({'game:display': displayData });
});
}
// display current question
Game.prototype._showAnswer = function Game__showAnswer(show)
{
var _game = this
, displayData
;
if (arguments.length < 1)
{
show = true;
}
// check if current question is selected
if (!show || !this.state['current_question'] || !this.question[this.state['current_question']])
{
displayData = false;
}
else
{
// get question with answer
displayData = { answer: this.question[this.state['current_question']] };
}
// save
this.save('state', 'display', displayData, function(err)
{
if (err) return console.log(['Could not set answer to display', err]);
// mark answer as shown
if (displayData)
{
_game.updateQuestion({index: displayData.answer.index, answer_shown: true}, function Game__showAnswer_updateQuestion_callback(err, question)
{
if (err) console.log(['Could not update current question from answer', err, displayData]);
});
}
_game._sockets.write({'game:display': displayData });
});
}
// Sends current state to the socket
// hides some data based on the access level
Game.prototype._sendState = function Game__sendState(socket)
{
// if no specific socket - broadcast
if (!socket)
{
_.each(this._sockets.connections, this._sendState.bind(this));
return;
}
// send initial data to the requesting socket
socket.write(
{
game:
{ instance : this.meta.instance
// don't expose password and answers
// and it's ok to show answers to admins
, teams : _.map(this.team, _.partial(this._teamStripAnswers, socket.id))
, questions: _.map(this.question, function(question){ return _.omit(question, ['text', 'answer']); })
, state : _.transform(this.state, function(result, item, key){ result[key] = (item && key == 'timer' ? _.omit(item, 'countdown') : item); })
}
});
}
// Strips sensitive information from team data
// but leaves it for admins
Game.prototype._teamStripAnswers = function Game__teamStripAnswers(socketId, team)
{
return _.transform(team, function(result, item, key)
{
if (key == 'password' || key == 'socketid') return;
result[key] = typeof item != 'object'
? item
: _.transform(item, function(result, item, key)
{
result[key] = admin[socketId]
? item
: _.omit(item, 'text');
});
});
}
// sets event listeners
// Note: all the event handlers bound to primus (websockets) object
Game.prototype._initEventListeners = function Game__initEventListeners()
{
var _game = this;
this.events = {};
// [helo] initial handshake
this.events['helo'] = function Game__initEventListeners_helo(socket, data)
{
// don't talk to anybody else
if (data != 'game' && data != 'team' && data != 'admin') return;
// if it's team or admin ask to auth
if (data == 'team' || data == 'admin')
{
if (data == 'admin')
{
toBeAdmin[socket.id] = socket;
}
else
{
toBeTeam[socket.id] = socket;
}
socket.write({'game:auth': {type: data, instance: _game.meta.instance} });
}
else
{
_game._sendState(socket);
}
};
// [disconnection]
this.events['disconnection'] = function Game__initEventListeners_disconnection(socket)
{
var _sockets = this;
_game.left(
{
socketid: socket.id
}, function Game_left_callback(err, user)
{
if (err) return console.log({ 'game:error': {err: err, origin: 'disconnection', data: user} });
// if it's regular spectator don't raise a fuss
if (user.login)
{
_sockets.write({ 'game:left': {login: user.login} });
}
});
};
// [game:auth]
this.events['game:auth'] = function Game__initEventListeners_game_auth(socket, data)
{
var _sockets = this
, nickname
;
if (!data)
{
// it's something like I don't know
return;
}
// check if user isn't rude and said helo beforehand
if (data.login == 'admin' && !toBeAdmin[socket.id])
{
return socket.write({ 'game:error': {err: {code: 401, message: 'Could not recognize provided login.'}, origin: 'auth'} });
}
else if (data.login != 'admin' && !toBeTeam[socket.id])
{
return socket.write({ 'game:error': {err: {code: 401, message: 'Could not recognize provided login.'}, origin: 'auth'} });
}
// clean up
delete toBeAdmin[socket.id];
delete toBeTeam[socket.id];
_game.auth(
{
login : data.login,
password: data.password,
socketid: socket.id
}, function Game_auth_callback(err, user)
{
if (err) return socket.write({ 'game:error': {err: err, origin: 'auth', data: data} });
if (user.login == 'admin')
{
_sockets.write({ 'game:admin': {login: user.login} });
nickname = '_admin_'+user.login;
}
else
{
_sockets.write({ 'game:team': {login: user.login, name: user.name} });
nickname = '_team_'+user.login;
}
socket.write({ 'game:logged': {login: user.login, password: user.password} });
// Auto-login into chat as game user
if (_game._chat)
{
_game._chat.forceJoin(_sockets, socket, {nickname: nickname, password: data.password});
}
// send state
_game._sendState(socket);
});
};
// --- team events
// [team:visibility]
this.events['team:visibility'] = function Game__initEventListeners_team_visibility(socket, data)
{
var _sockets = this
, login
;
if (typeof data != 'boolean')
{
// it's something like I don't know
return;
}
if (!(login = _game._isTeam(socket, 'visibility')))
{
return;
}
_game.updateTeam({login: login, visibility: !!data}, function Game_teamVisibility_updateTeam_callback(err, team)
{
if (err) return socket.write({ 'team:error': {err: err, origin: 'visibility'} });
_sockets.write({ 'team:visibility': {team: team.login, visibility: team.visibility} });
});
};
// [team:answer]
this.events['team:answer'] = function Game__initEventListeners_team_answer(socket, data)
{
var _sockets = this
, login
;
if (!data)
{
// it's something like I don't know
return;
}
if (!(login = _game._isTeam(socket, 'answer')))
{
return;
}
_game.addAnswer({login: login, answer: data}, function Game_addAnswer_callback(err, team)
{
if (err) return socket.write({ 'team:error': {err: err, origin: 'answer'} });
// send different data to the admin and to others
_.each(_sockets.connections, function(s, id)
{
s.write({ 'game:team_updated': _game._teamStripAnswers(id, team) });
});
});
};
// --- admin stuff
// [admin:eval_answer]
this.events['admin:eval_answer'] = function Game__initEventListeners_admin_eval_answer(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'eval_answer'))
{
return;
}
_game.evalAnswer(data, function Game_evalAnswer_callback(err, team)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'eval_answer', data: data} });
// send different data to the admin and to others
_.each(_sockets.connections, function(s, id)
{
s.write({ 'game:team_updated': _game._teamStripAnswers(id, team) });
});
});
};
// [admin:set_timer]
this.events['admin:set_timer'] = function Game__initEventListeners_admin_set_timer(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'set_timer'))
{
return;
}
_game.setTimer(data, function Game_setTimer_callback(err, timer)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'set_timer', data: data} });
_sockets.write({ 'game:timer': timer });
});
};
// [admin:set_question]
this.events['admin:set_question'] = function Game__initEventListeners_admin_set_question(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'set_question'))
{
return;
}
_game.setQuestion(data, function Game_setQuestion_callback(err, question)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'set_question', data: data} });
_sockets.write({ 'game:current_question': _.omit(question, ['text', 'answer']) });
});
};
// [admin:show_answer]
this.events['admin:show_answer'] = function Game__initEventListeners_admin_show_answer(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'show_answer'))
{
return;
}
if (!data.index || data.index != _game.state['current_question'])
{
socket.write({ 'admin:error': {err: {code: 404, message: 'Cannot display answer for not current question.'}, origin: 'show_answer', data: data} });
return;
}
_game._showAnswer(data.show);
};
// [admin:add_team]
this.events['admin:add_team'] = function Game__initEventListeners_admin_add_team(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'add_team'))
{
return;
}
_game.addTeam({
login : _game._makeHandle(data.name),
name : data.name,
password: data.password
}, function Game_addTeam_callback(err, team)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'add_team', data: data} });
_sockets.write({ 'game:team_added': _.omit(team, ['password', 'answers']) });
});
};
// [admin:update_team]
this.events['admin:update_team'] = function Game__initEventListeners_admin_update_team(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'update_team'))
{
return;
}
_game.updateTeam(data, function Game_updateTeam_callback(err, team)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'update_team', data: data} });
// send different data to the admin and to others
_.each(_sockets.connections, function(s, id)
{
s.write({ 'game:team_updated': _game._teamStripAnswers(id, team) });
});
});
};
// [admin:delete_team]
this.events['admin:delete_team'] = function Game__initEventListeners_admin_delete_team(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'delete_team'))
{
return;
}
_game.deleteTeam(data, function Game_deleteTeam_callback(err, team)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'delete_team', data: data} });
_sockets.write({ 'game:team_deleted': {login: team.login} });
});
};
// --- questions
// [admin:add_question]
this.events['admin:add_question'] = function Game__initEventListeners_admin_add_question(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'add_question'))
{
return;
}
_game.addQuestion(data, function Game_addQuestion_callback(err, question)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'add_question', data: data} });
_sockets.write({ 'game:question_added': _.omit(question, ['text', 'answer']) });
});
};
// [admin:update_question]
this.events['admin:update_question'] = function Game__initEventListeners_admin_update_question(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'update_question'))
{
return;
}
_game.updateQuestion(data, function Game_updateQuestion_callback(err, question)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'update_question', data: data} });
_sockets.write({ 'game:question_updated': _.omit(question, ['text', 'answer']) });
});
};
// [admin:delete_question]
this.events['admin:delete_question'] = function Game__initEventListeners_admin_delete_question(socket, data)
{
var _sockets = this
;
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'delete_question'))
{
return;
}
_game.deleteQuestion(data, function Game_deleteQuestion_callback(err, question)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'delete_question', data: data} });
_sockets.write({ 'game:question_deleted': {index: question.index} });
});
};
// -- special stuff
// [admin:reset_scoreboard]
this.events['admin:reset_scoreboard'] = function Game__initEventListeners_admin_reset_scoreboard(socket, data)
{
// nothing really, but make it uniform
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'reset_scoreboard'))
{
return;
}
_game.resetScoreboard(function Game_resetScoreboard_callback(err)
{
if (err) return socket.write({ 'admin:error': {err: err, origin: 'reset_scoreboard'} });
// refresh the world
_game._sendState(); // to everybody
});
};
// [admin:get_question]
// fetches question data and sends it using "callback"
this.events['admin:get_question'] = function Game__initEventListeners_admin_get_question(socket, data)
{
if (!data)
{
// it's something like I don't know
return;
}
if (!_game._isAdmin(socket, 'get_question'))
{
return;
}
_game.getQuestion({index: data.index}, function Game_getQuestion_callback(err, question)
{
// perform callback
socket.write({ '_:callback': {hash: data.callback, err: err, data: merge(templateQuestion, question)} });
});
}
}
// --- end of init
// Checks if provided socket is a team
// and return team's login
Game.prototype._isTeam = function Game__isTeam(socket, origin, verbose)
{
if (arguments < 3)
{
verbose = true;
}
if (!teams[socket.id])
{
verbose && socket.write({ 'team:error': {err: {code: 403, message: 'Permission denied.'}, origin: origin} });
return false;
}
return teams[socket.id].login;
}
// Checks if provided socket is admin
Game.prototype._isAdmin = function Game__isAdmin(socket, origin, verbose)
{
if (arguments < 3)
{
verbose = true;
}
if (typeof socket == 'string')
{
return !!admin[socket];
}
else if (!admin[socket.id])
{
verbose && socket.write({ 'admin:error': {err: {code: 403, message: 'Permission denied.'}, origin: origin} });
return false;
}
return true;
}
// --- Santa's little helpers
// fetches slice of data from the database
Game.prototype._fetchSlice = function Game__fetchSlice(slice, callback)
{
var results = {}
, sliceRE = new RegExp('^'+slice+'\:')
;
this._db.createReadStream({start: slice+':', end: slice+':~'})
.on('data', function(data)
{
results[data.key.replace(sliceRE, '')] = data.value;
})
.on('error', function(err)
{
callback(err);
})
.on('end', function()
{
callback(null, results);
});
}
Game.prototype._makeHandle = function Game__makeHandle(s)
{
return unidecode.fold(s).toLowerCase().replace(/[^a-z0-9-]/g, '_').replace(/^[0-9-_]*/, '');
}