UNPKG

shiro

Version:

Online quiz game engine, inspired by russian tv show 'What? Where? When?' (Million Dollar Mind Game).

926 lines (764 loc) 21.5 kB
/* admin level specific js */ // post init will be called after generic init // Note: overrides team level Game.prototype._postInit = function Game__postInit() { var _game = this; // keep track of answered questions this.answered = {}; // socket callback pool this._callbackPool = {}; // prepare stub methods this._drawTeamAnswer = $.partial(this._drawTeamAnswerStub, this); this._sortTeamAnswer = $.partial(this._sortTeamAnswerStub, this); // --- add extra containers // global var – containerGameplay, set in admin.html this.gameplay = typeof containerGameplay == 'string' ? $(containerGameplay) : containerGameplay; // d3 this._gameplayContainer = this.d3.select(this.gameplay[0]); // teams' answers this.teamsAnswersPanel = $('.answer_teams'); // d3 this._teamsAnswersContainer = this.d3.select(this.teamsAnswersPanel[0]); // --- add extra events this.socket.on('data', function primus_onData(data) { // [_:callback] if (data['_:callback']) { if (typeof data['_:callback'] == 'object' && data['_:callback'].hash && typeof _game._callbackPool[data['_:callback'].hash] == 'function') { // pass error and data to the stored callback _game._callbackPool[data['_:callback'].hash].call(_game, data['_:callback'].err, data['_:callback'].data); // cleanup delete _game._callbackPool[data['_:callback'].hash]; } } // [game:auth] if (data['game:auth']) { // compare instances if (_game.instance() != data['game:auth'].instance) { _game.instance(data['game:auth'].instance); _game.user(false); } // check for user if (!_game.user() || !_game.user().password) { _game._toggleAuthModal(true, data['game:auth'].type); } else // try to login { _game.socket.write({ 'game:auth': _game.user() }); } } // [game:logged] if (data['game:logged']) { _game._logged(data['game:logged']); } // [game:current_question] // ['game:team_updated'] // redraw teams answers if (data['game:current_question'] || data['game:team_updated']) { _game._displayTeamsAnswers(); } // [game:error] if (data['game:error']) { _game._handleError(data['game:error']); } // [admin:error] if (data['admin:error']) { _game._handleError(data['admin:error']); } }); // --- add local event handlers // admin stuff // start timer $('.gameplay_start_timer').on('click', function(e) { e.stop(); _game.startStopTimer(); }); // add team action $('.scoreboard_add_team').on('click', function(e) { e.stop(); _game._toggleAddTeamModal(true); }); // reset scoreboard $('.scoreboard_reset_scoreboard').on('click', function(e) { e.stop(); _game._toggleResetScoreboardModal(true); }); // edit team action this.scoreboard.on('click', '.scoreboard_edit_team', function(e) { var el = $(this) , team = el.parents('.scoreboard_team').attr('id').replace(/^scoreboard_team_/, '') ; if (!team) return; e.stop(); _game._toggleEditTeamModal(true, team); }); // delete team action this.scoreboard.on('click', '.scoreboard_delete_team', function(e) { var el = $(this) , team = el.parents('.scoreboard_team').attr('id').replace(/^scoreboard_team_/, '') ; if (!team) return; e.stop(); _game._toggleDeleteTeamModal(true, team); }); // pick question action this.gameplay.on('click', '.gameplay_question', function(e) { var question; // reduce noise if (!$(e.target).hasClass('gameplay_question_controls')) return; question = $(this).attr('id').replace(/^gameplay_question_/, ''); if (!question) return; e.stop(); _game.pickQuestion(question); }); // add question action $('.gameplay_add_question').on('click', function(e) { e.stop(); _game._toggleAddQuestionModal(true); }); // edit question action this.gameplay.on('click', '.gameplay_edit_question', function(e) { var el = $(this) , question = el.parents('.gameplay_question').attr('id').replace(/^gameplay_question_/, '') ; if (!question) return; e.stop(); _game._toggleEditQuestionModal(true, question); }); // delete question action this.gameplay.on('click', '.gameplay_delete_question', function(e) { var el = $(this) , question = el.parents('.gameplay_question').attr('id').replace(/^gameplay_question_/, '') ; if (!question) return; e.stop(); _game._toggleDeleteQuestionModal(true, question); }); // show answer action this.gameplay.on('click', '.gameplay_show_answer', function(e) { var el = $(this) , question = el.parents('.gameplay_question').attr('id').replace(/^gameplay_question_/, '') , show = true ; if (!question) return; e.stop(); // check if reverse action is needed if (_game.answerText.css('display') != 'none') { show = false; } _game.showAnswer(question, show); }); this.teamsAnswersPanel.on('click', '.answer_teams_team_correct', function(e) { var el = $(this) , team = el.parents('.answer_teams_team').attr('id').replace(/^answer_teams_team_/, '') ; if (!team) return; e.stop(); _game.evalAnswer(team, 'correct'); }); this.teamsAnswersPanel.on('click', '.answer_teams_team_wrong', function(e) { var el = $(this) , team = el.parents('.answer_teams_team').attr('id').replace(/^answer_teams_team_/, '') ; if (!team) return; e.stop(); _game.evalAnswer(team, 'wrong'); }); // --- Modals // create auth prompt this.authModal = new FormPrompt( { sticky: true, title: 'enter admin password to login', fields: [ {type: 'password', name: 'password', title: 'password'} ], controls: [ {action: 'submit', title: 'ok'} ] }); // create team prompt this.teamModal = new FormPrompt( { title: 'add new team', fields: [ {type: 'text', name: 'name', title: 'team'}, {type: 'text', name: 'password', title: 'password'} ], controls: [ {action: 'submit', title: 'add'}, {action: 'cancel', title: 'cancel'} ] }); // edit team prompt this.teamEditModal = new FormPrompt( { title: 'edit team', fields: [ {name: 'login', title: 'login', readonly: true}, {name: 'name', title: 'team'}, {name: 'password', title: 'password'}, {name: 'points', title: 'points'}, {name: 'time_bonus', title: 'time bonus'} ], controls: [ {action: 'submit', title: 'update'}, {action: 'cancel', title: 'cancel'} ] }); // confimation modal this.confirmModal = new FormPrompt( { controls: [ {action: 'no', title: 'no'}, {action: 'yes', title: 'yes'} ] }); // --- questions // create team prompt this.questionModal = new FormPrompt( { title: 'add question', fields: [ {type: 'textarea', name: 'text', title: 'question'}, {type: 'textarea', name: 'answer', title: 'answer'} ], controls: [ {action: 'submit', title: 'add'}, {action: 'cancel', title: 'cancel'} ] }); // edit team prompt this.questionEditModal = new FormPrompt( { title: 'edit question', fields: [ {type: 'checkbox', name: 'played', title: 'been played'}, {type: 'textarea', name: 'text', title: 'question'}, {type: 'textarea', name: 'answer', title: 'answer'} ], controls: [ {action: 'submit', title: 'update'}, {action: 'cancel', title: 'cancel'} ] }); } // end of init // Sets customed for admin current game state Game.prototype._admin_commonSetTeams = Game.prototype.setTeams; Game.prototype.setTeams = function Game_setTeams(teams) { // first do everythign else var result = this._admin_commonSetTeams(teams); // teams' answers this._displayTeamsAnswers(); return result; } Game.prototype.startStopTimer = function Game_startStopTimer() { // turn it on if it's off // and vice versa if (this.timerCounting) { this.socket.write({ 'admin:set_timer': 'off' }); } else { this.socket.write({ 'admin:set_timer': 'on' }); } } Game.prototype.pickQuestion = function Game_pickQuestion(index) { if (this.questionInPlay != index) { this.socket.write({ 'admin:set_question': {index: index} }); } else // unset { this.socket.write({ 'admin:set_question': {index: 0} }); } } Game.prototype.showAnswer = function Game_showAnswer(index, show) { if (arguments.length < 2) { show = true; } this.socket.write({ 'admin:show_answer': {index: index, show: !!show} }); } Game.prototype.evalAnswer = function Game_evalAnswer(team, status) { var question; // no current question, team or status, bye bye if (!team || !status || !(question = this.questionInPlay)) return; this.socket.write({ 'admin:eval_answer': {question: question, team: team, status: status} }); } // shows answers for the current questions Game.prototype._displayTeamsAnswers = function Game__displayTeamsAnswers(show) { var _game = this , teams ; if (arguments.length < 1) { show = true; } // sanity check if (!show || !this.questionInPlay) { this.teamsAnswersPanel.hide(); $('.answer_teams_stats').removeAttr('data-teams'); $('.answer_teams_stats').removeAttr('data-answers'); $('.answer_teams_stats').removeAttr('data-evaluated'); return; } else { this.teamsAnswersPanel.show(); } // get only ones answered teams = $.filter(this.teams, function(t){ return t.answers && t.answers[_game.questionInPlay]; }); this._renderTeamsAnswers(teams); } // Override team's login Game.prototype._login = function Game__login(instance) { // if current instance is out of date // reset it if (instance && this.instance() != instance) { this.instance(instance); this.user(false); } // check for user if (!this.user()) { this._toggleAuthModal(true, 'admin'); } else // try to login { this.socket.write({ 'game:auth': this.user() }); } } // --- toggles team modals on/off // add team Game.prototype._toggleAddTeamModal = function Game__toggleAddTeamModal(show) { var _game = this; if (arguments.length < 1) { show = true; } if (show) { this.teamModal.activate(function(action, data) { if (action == 'submit') { _game.socket.write({ 'admin:add_team': {name: data.name, password: data.password} }); } }); } else { this.teamModal.deactivate(); } } // edit team Game.prototype._toggleEditTeamModal = function Game__toggleEditTeamModal(show, team) { var _game = this , team = this.getTeam(team) ; // be on defensive side if (!team) return; if (arguments.length < 1) { show = true; } if (show) { this.teamEditModal.title('Edit team <i>'+team.name+'</i>'); this.teamEditModal.data(team); this.teamEditModal.activate(function(action, data) { var diff; if (action == 'submit') { if (diff = _game._diffObject(team, data)) { _game.socket.write({ 'admin:update_team': $.merge({login: team.login}, diff) }); } } }); } else { this.teamEditModal.deactivate(); } } // delete team Game.prototype._toggleDeleteTeamModal = function Game__toggleDeleteTeamModal(show, team) { var _game = this , team = this.getTeam(team) ; // be on defensive side if (!team) return; if (arguments.length < 1) { show = true; } if (show) { this.confirmModal.title('Are you sure you want to <i>delete</i> team <i>'+team.name+'</i>?'); this.confirmModal.activate(function(action) { if (action == 'yes') { _game.socket.write({ 'admin:delete_team': {login: team.login} }); } }); } else { this.confirmModal.deactivate(); } } // add question Game.prototype._toggleAddQuestionModal = function Game__toggleAddQuestionModal(show) { var _game = this; if (arguments.length < 1) { show = true; } if (show) { this.questionModal.activate(function(action, data) { if (action == 'submit') { _game.socket.write({ 'admin:add_question': {text: data.text, answer: data.answer} }); } }); } else { this.questionModal.deactivate(); } } // edit question Game.prototype._toggleEditQuestionModal = function Game__toggleEditQuestionModal(show, question) { var _game = this , callback = this._generateHash() ; // be on defensive side if (!question) return; if (arguments.length < 1) { show = true; } if (show) { // fetch question data this.socket.write({ 'admin:get_question': {index: question, callback: callback} }); // wait for the response this._onSocketCallback(callback, function(err, question) { if (err) return; this.questionEditModal.title('Edit question <i>'+question.index+'</i>'); this.questionEditModal.data(question); this.questionEditModal.activate(function(action, data) { var diff; if (action == 'submit') { if (diff = _game._diffObject(question, data)) { _game.socket.write({ 'admin:update_question': $.merge({index: question.index}, data) }); } } }); }); } else { this.questionEditModal.deactivate(); } } // delete question Game.prototype._toggleDeleteQuestionModal = function Game__toggleDeleteQuestionModal(show, question) { var _game = this ; // be on defensive side if (!question) return; if (arguments.length < 1) { show = true; } if (show) { this.confirmModal.title('Are you sure you want to <i>delete</i> question <i>'+question+'</i>?'); this.confirmModal.activate(function(action) { if (action == 'yes') { _game.socket.write({ 'admin:delete_question': {index: question} }); } }); } else { this.confirmModal.deactivate(); } } // reset scoreboard Game.prototype._toggleResetScoreboardModal = function Game__toggleResetScoreboardModal(show) { var _game = this ; if (arguments.length < 1) { show = true; } if (show) { this.confirmModal.title('Are you sure you want to <i>reset</i> the scoreboard?'); this.confirmModal.activate(function(action) { if (action == 'yes') { _game.socket.write({ 'admin:reset_scoreboard': true }); } }); } else { this.confirmModal.deactivate(); } } // Custom team html element Game.prototype._drawTeamStub = function Game__drawTeamStub(_game, d) { // this here is a DOM element var el = _game.d3.select(this) , isMe = (_game.user() && d.login == _game.user().login) , frac = d.time_bonus && d.points ? Math.round(d.time_bonus / ((d.points - (d.adjustment || 0)) * 60000) * 1000) : 0 , bonus = $.reduce(d.answers, function(total, a){ return a.correct ? total + a.time[0] : total }, 0) , html = '' ; bonus = bonus < 10 ? '0'+bonus : ''+bonus; html += '<span class="scoreboard_team_name">'+d.name+'</span>'; html += '<span class="scoreboard_team_time_bonus">:'+bonus+'</span>'; html += '<span class="scoreboard_team_points">'+d.points+'<span class="scoreboard_team_fracs">.'+(frac < 10 ? '00'+frac : (frac < 100 ? '0' + frac : frac))+'</span></span>'; html += '<span class="scoreboard_team_controls"><span class="scoreboard_edit_team"></span><span class="scoreboard_delete_team"></span></span>'; el .classed('scoreboard_team', true) .classed('scoreboard_team_mine', isMe) .classed('scoreboard_team_online', d.online) .classed('scoreboard_team_has_focus', d.visibility) .attr('id', 'scoreboard_team_'+d.login) .html(html); } // Custom question html element Game.prototype._drawQuestionStub = function Game__drawQuestionStub(_game, d) { // this here is a DOM element var el = _game.d3.select(this) , html = d.index; ; html += '<span class="gameplay_question_controls"><span class="gameplay_edit_question"></span><span class="gameplay_delete_question"></span><span class="gameplay_show_answer"></span></span>'; el .classed('gameplay_question', true) .classed('gameplay_question_played', !!d.played) .attr('id', 'gameplay_question_'+d.index) .html(html); } // Admin specific timer animation Game.prototype._renderTimer = function Game__renderTimer() { var _game = this , leftover ; // turn it off if (!this.timerCounting) { $('.gameplay_start_timer') .removeAttr('data-timer') .removeClass('timer_running_out') ; return; } // be lazy if (this._lastTick === this.timerCounting.tick) return; this._lastTick = this.timerCounting.tick; leftover = 59-(this.timerCounting.tick || 0); $('.gameplay_start_timer').attr('data-timer', leftover < 10 ? '0'+leftover : leftover); // extra treatment for the last 10 seconds if (leftover < 10) { $('.gameplay_start_timer').addClass('timer_running_out'); } else { $('.gameplay_start_timer').removeClass('timer_running_out'); } } // Draws teams' answers panel Game.prototype._renderTeamsAnswers = function Game__renderTeamsAnswers(teams) { var _game = this , item ; // update stats // TODO: Make it sanely $('.answer_teams_stats').attr('data-teams', this.teams.length); $('.answer_teams_stats').attr('data-answers', '0'); $('.answer_teams_stats').attr('data-evaluated', '0'); // cleanup // TODO: Fix it properly this.teamsAnswersPanel.html(''); item = this._teamsAnswersContainer.selectAll('.answer_teams_team') .data(teams, function(d){ return d.login; }) .sort(this._sortTeamAnswer) .each(this._drawTeamAnswer) ; item.enter().append('span') .sort(this._sortTeamAnswer) .each(this._drawTeamAnswer) ; item.exit() .remove() ; } Game.prototype._sortTeamAnswerStub = function Game__sortTeamAnswerStub(_game, a, b) { var answerA = a.answers[_game.questionInPlay] , answerB = b.answers[_game.questionInPlay] , evaluatedA = typeof answerA.correct == 'boolean' , evaluatedB = typeof answerB.correct == 'boolean' , evaluated = 0 ; if (evaluatedA && !evaluatedB) { evaluated = 1; } else if (!evaluatedA && evaluatedB) { evaluated = -1; } return evaluated ? evaluated : (answerB.bonus - answerA.bonus); } Game.prototype._drawTeamAnswerStub = function Game__drawTeamAnswerStub(_game, d) { // this here is a DOM element var el = _game.d3.select(this) , answer = d.answers[_game.questionInPlay] , seconds = answer.time[0] < 10 ? '0'+answer.time[0] : answer.time[0] , permile = Math.floor(answer.time[1]/1e6) , html = '' , statsPanel = $('.answer_teams_stats') ; // update stats statsPanel.attr('data-answers', +(statsPanel.attr('data-answers') || 0) + 1); statsPanel.attr('data-evaluated', +(statsPanel.attr('data-evaluated') || 0) + (typeof answer.correct == 'boolean' ? 1 : 0)); // add zeros to the end permile = permile < 10 ? permile + '00' : (permile < 100 ? permile + '0' : permile); html += '<button class="answer_teams_control answer_teams_team_correct'+(answer.correct === true ? ' answer_teams_control_selected' : '')+'"></button>' if (typeof answer.correct == 'boolean') { html += '<span class="answer_teams_team_name">'+d.name+'</span>'; } else { html += '<span class="answer_teams_team_time">:'+seconds+'<span class="answer_teams_team_time_permile">.'+permile+'</span></span>'; } html += '<span class="answer_teams_team_answer">'+(answer.text || '[no answer]') +'</span>'; html += '<button class="answer_teams_control answer_teams_team_wrong'+(answer.correct === false ? ' answer_teams_control_selected' : '')+'"></button>' el .classed('answer_teams_team', true) .classed('answer_teams_team_evaluated', typeof answer.correct == 'boolean') .classed('answer_teams_team_evaluated_correct', answer.correct) .attr('id', 'answer_teams_team_'+d.login) .html(html); } // -- Santa's little helpers // Waits for callback event from the server // TOOD: Add timeout Game.prototype._onSocketCallback = function Game__onSocketCallback(hash, callback, options) { this._callbackPool[hash] = callback; } // Object diff method // Shallow and simple version works for the same set of keys (kindof) Game.prototype._diffObject = function Game__diffObject(a, b) { var key , isDifferent = false , result = {} ; for (key in b) { if (!b.hasOwnProperty(key)) continue; if (a[key] != b[key] && (a[key] || b[key])) { isDifferent = true; result[key] = b[key]; } } return isDifferent ? result : false; } // generates (uniqly) random hash // for callback id Game.prototype._generateHash = function Game__generateHash() { var time = Date.now() // get unique number , salt = Math.floor(Math.random() * Math.pow(10, Math.random()*10)) // get variable length prefix , hash = time.toString(36) + salt.toString(36) // construct unique id ; return hash; } // --- custom chat methods // don't block admin's chat // blocks chat's UI Chat.prototype.block = function Chat_block(blocked) { // do nothing }