UNPKG

shiro

Version:

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

633 lines (519 loc) 14.9 kB
// --- game function Game(options) { this.gameplay = typeof options.gameplay == 'string' ? $(options.gameplay) : options.gameplay; this.scoreboard = typeof options.scoreboard == 'string' ? $(options.scoreboard) : options.scoreboard; this.teamsList = typeof options.teamsList == 'string' ? $(options.teamsList) : options.teamsList; this.timerPanel = typeof options.timer == 'string' ? $(options.timer) : options.timer; this.questionPanel = typeof options.question == 'string' ? $(options.question) : options.question; this.questionText = typeof options.questionText == 'string' ? $(options.questionText) : options.questionText; this.answerPanel = typeof options.answer == 'string' ? $(options.answer) : options.answer; this.answerText = typeof options.answerText == 'string' ? $(options.answerText) : options.answerText; // game play type this.type = options.type || 'game'; // websockets this.socket = options.transport; // -- teams // d3 this.d3 = options.d3; // teams container this._container = this.d3.select(this.teamsList[0]); // team drawing function this._drawTeam = $.partial(this._drawTeamStub, this); // question drawing function this._drawQuestion = $.partial(this._drawQuestionStub, this); // team answer chart this._drawTeamAnswerChart = $.partial(this._drawTeamAnswerChartStub, this); // -- gameplay // d3 this._gameplayContainer = this.d3.select(this.gameplay[0]); // drawing function this._drawQuestion = $.partial(this._drawQuestionStub, this); // -- timer // create list of ticks // one per second this._timerTicksList = this.d3.range(60); this._timerContainer = this.d3.select(this.timerPanel[0]); // --- current data storage this.teams = []; this.questions = []; this.questionInPlay = 0; // init this.init(); } Game.prototype.init = function Game_init() { var _game = this ; // connect to the server this.socket.on('open', function primus_onOpen() { // say hello _game.socket.write({ helo: _game.type }); }); // events come from data this.socket.on('data', function primus_onData(data) { // [game] welcome message if (data.game) { // if current instance is out of date // reset it if (_game.instance() != data.game.instance) { _game.instance(data.game.instance); _game.user(false); } _game.setQuestions(data.game.questions); _game.setState(data.game.state); _game.setTeams(data.game.teams); } // [game:team_added] if (data['game:team_added']) { _game.addTeam(data['game:team_added']); } // [game:team_updated] if (data['game:team_updated']) { _game.updateTeam(data['game:team_updated']); } // [game:team_deleted] if (data['game:team_deleted']) { _game.deleteTeam(data['game:team_deleted']); } // [game:question_added] if (data['game:question_added']) { _game.addQuestion(data['game:question_added']); } // [game:question_updated] if (data['game:question_updated']) { _game.updateQuestion(data['game:question_updated']); } // [game:team_deleted] if (data['game:question_deleted']) { _game.deleteQuestion(data['game:question_deleted']); } // [game:current_question] if (data['game:current_question']) { _game.currentQuestion(data['game:current_question'].index); } // [game:timer] if ('game:timer' in data) { _game.updateTimer(data['game:timer']); } // [game:display] if ('game:display' in data) { _game.display(data['game:display']); } // [game:team] if (data['game:team']) { _game.teamOnline(data['game:team'], true); } // [game:left] if (data['game:left']) { _game.teamOnline(data['game:left'], false); } // [team:visibility] if (data['team:visibility']) { _game.teamVisibility(data['team:visibility']); } // [game:current_question] // ['game:team_updated'] // redraw teams chart if (data['game:current_question'] || data['game:team_updated']) { _game._displayTeamsChart(); } }); // extra post init if (typeof this._postInit == 'function') { this._postInit(); } } // --- getters/setters Game.prototype.user = function Game_user(value) { if (arguments.length > 0) { $.cookie('game:user', value, {months: 1}); } return $.cookie('game:user'); } Game.prototype.instance = function Game_instance(value) { if (arguments.length > 0) { $.cookie('game:instance', value, {months: 1}); } return $.cookie('game:instance'); } // Attaches reference to known external objects Game.prototype.attach = function Game_attach(collection) { // so far it's only chat if ('chat' in collection) { this._chat = collection['chat']; } } // Fetches team object Game.prototype.getTeam = function Game_getTeam(handle) { return $.find(this.teams, {login: handle}); } // more meaningful methods Game.prototype.addTeam = function Game_addTeam(team) { this.teams.push(team); this._renderTeams(); } Game.prototype.updateTeam = function Game_updateTeam(team) { // merge in updated team data this.teams = $.transform(this.teams, function(result, t){ if (t.login == team.login) { result.push(team) } else { result.push(t); } return result; }); this._renderTeams(); } Game.prototype.deleteTeam = function Game_deleteTeam(team) { var _game = this; // find and kill $.find(this.teams, function(t, i){ if (t.login == team.login) { _game.teams.splice(i, 1); return true; } }); this._renderTeams(); } Game.prototype.setTeams = function Game_setTeams(teams) { this.teams = teams; this._renderTeams(); // display answers stats chart this._displayTeamsChart(); } Game.prototype.addQuestion = function Game_addQuestion(question) { this.questions.push(question); this._renderQuestions(); } Game.prototype.updateQuestion = function Game_updateQuestion(question) { // merge in updated question data this.questions = $.each(this.questions, function(q){ if (q.index == question.index) { $.merge(q, question); } }); // render question element directly this._drawQuestionStub.call($('#gameplay_question_'+question.index)[0], this, question); } Game.prototype.deleteQuestion = function Game_deleteQuestion(question) { var _game = this; // find and kill $.find(this.questions, function(q, i){ if (q.index == question.index) { _game.questions.splice(i, 1); return true; } }); this._renderQuestions(); } Game.prototype.setQuestions = function Game_setQuestions(questions) { this.questions = questions; this._renderQuestions(); } Game.prototype.currentQuestion = function Game_currentQuestion(index) { if (this.questionInPlay != index) { // unselect previous one if (this.questionInPlay) { $('#gameplay_question_'+this.questionInPlay).removeClass('gameplay_question_playing'); } // don't do anything if current question was reset if (this.questionInPlay = index) { $('#gameplay_question_'+this.questionInPlay).addClass('gameplay_question_playing'); } } return this.questionInPlay; } // Sets current game state Game.prototype.setState = function Game_setState(state) { // current question if ('current_question' in state) { this.currentQuestion(state['current_question']); } // timer this.updateTimer(state['timer']); // display this.display(state['display']); } Game.prototype.updateTimer = function Game_updateTimer(timer) { if (timer) { this.timerCounting = timer; this._chat.block(true); } else { this.timerCounting = false; this._chat.block(false); } this._renderTimer(); } // displaying question or question + answer // TODO: make it less coupled Game.prototype.display = function Game_display(data) { // question if (data && data['question']) { this.questionText.html(data['question'].text); this.questionPanel.show(); } else { this.questionPanel.hide(); this.questionText.html(''); } // answer if (data && data['answer']) { this.answerText.html(data['answer'].answer).show(); this.questionText.html(data['answer'].text); this.questionPanel.show(); } else if (!data) { this.answerText.hide().html(''); this.questionPanel.hide(); this.questionText.html(''); } } // Updates team's online status Game.prototype.teamOnline = function Game_teamOnline(data, online) { if (arguments.length < 2) { online = true; } if (typeof data != 'object' || !data.login) { return; } $('#scoreboard_team_'+data.login)[online ? 'addClass' : 'removeClass']('scoreboard_team_online'); } // Updates team's visibility status Game.prototype.teamVisibility = function Game_teamVisibility(data) { if (typeof data != 'object' || !data.team || !('visibility' in data)) { return; } $('#scoreboard_team_'+data.team)[data.visibility ? 'addClass' : 'removeClass']('scoreboard_team_has_focus'); } // --- demi-private methods Game.prototype._renderTeams = function Game__renderTeams() { var item; // sort this.teams.sort(this._sortTeams); item = this._container.selectAll('.scoreboard_team') .data(this.teams) .order() .each(this._drawTeam) ; item.enter().append('span') .order() .each(this._drawTeam) ; item.exit() .remove() ; } 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 , html = '' ; html += '<span class="scoreboard_team_name">'+d.name+'</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>'; 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); } Game.prototype._renderQuestions = function Game__renderQuestions() { var item; // sort this.questions = $.sortBy(this.questions, 'index'); item = this._gameplayContainer.selectAll('.gameplay_question') .data(this.questions) .order() .each(this._drawQuestion) ; item.enter().append('span') .order() .each(this._drawQuestion) ; item.exit() .remove() ; } Game.prototype._drawQuestionStub = function Game__drawQuestionStub(_game, d) { // this here is a DOM element var el = _game.d3.select(this) ; el .classed('gameplay_question', true) .classed('gameplay_question_played', !!d.played) .attr('id', 'gameplay_question_'+d.index) .text(d.index); } Game.prototype._renderTimer = function Game__renderTimer() { var _game = this , item , totalWidth , tickWidth , tickMargin ; // check if timer stopped // and cleanup if (!this.timerCounting) { this.timerPanel.hide(); this.timerPanel.html(''); // show gameplay this.gameplay.show(); // reset last tick this._lastTick = null; return; } else { // hide gameplay this.gameplay.hide(); this.timerPanel.show(); } // be lazy if (this._lastTick === this.timerCounting.tick) return; this._lastTick = this.timerCounting.tick; totalWidth = this.timerPanel.dim().width; // width – 1% within 5px - 20px range tickWidth = Math.min(20, Math.max(5, Math.floor(totalWidth/100))); // margin - 0.5% within 2px - 10px range tickMargin = Math.min(10, Math.max(2, Math.floor(totalWidth/200))); // extra treatment for the last 10 seconds this._timerContainer.classed('timer_running_out', (_game.timerCounting.tick > 49)) item = this._timerContainer.selectAll('.timer_tick') .data(this._timerTicksList) .order() .classed('timer_tick', true) .classed('timer_tick_passed', function(d) { return d < 60 - _game.timerCounting.tick}) .style('width', tickWidth + 'px') .style('margin-left', tickMargin + 'px') ; item.enter().append('span') .order() .classed('timer_tick', true) .classed('timer_tick_passed', function(d) { return d < 60 - _game.timerCounting.tick}) .style('width', tickWidth + 'px') .style('margin-left', tickMargin + 'px') ; item.exit() .remove() ; } // TODO: Make it less of a hack Game.prototype._displayTeamsChart = function Game__displayTeamsChart() { var _game = this , item , d3 = this.d3 , panel = $('.answer_teams_chart') , width = 100 / this.teams.length , teams = $.sortBy(this.teams, 'name') ; // sanity check if (!this.questionInPlay) { panel.hide(); return; } else { panel.show(); } if (!this._chartBase) { this._chartBase = this._createChartBase(panel); } item = this._chartBase.selectAll('.answer_teams_chart_team') .data(teams, function(d){ return d.login; }) .order() .style('width', width+'%') .each(this._drawTeamAnswerChart) ; item.enter().append('span') .order() .style('width', width+'%') .each(this._drawTeamAnswerChart) ; item.exit() .remove() ; } Game.prototype._drawTeamAnswerChartStub = function Game__drawTeamAnswerChartStub(_game, d) { var el = _game.d3.select(this) , answer = d.answers[_game.questionInPlay] , time = answer && answer.time ? answer.time[0] : 0 , html = '' ; html += '<span class="answer_teams_chart_team_name">'+d.name+'</span>'; html += '<span style="height: '+(time / 60 * 100)+'%" class="answer_teams_chart_team_bar"></span>'; el .classed('answer_teams_chart_team', true) .classed('answer_teams_chart_team_correct', answer && typeof answer.correct == 'boolean' && answer.correct) .classed('answer_teams_chart_team_wrong', answer && typeof answer.correct == 'boolean' && !answer.correct) .html(html) ; } Game.prototype._createChartBase = function Game__createChartBase(panel) { var chart = this.d3.select(panel[0]) ; // create Y-axis panel.html('<span class="answer_teams_chart_axis_y"><i>'+([60, 45, 30, 15, 0].join('</i><i>'))+'</i></span>'); return chart; } Game.prototype._sortTeams = function Game__sortTeams(a, b) { var comp = 0; // check points first if ((comp = b.points - a.points) == 0) { // check time_bonus if ((comp = b.time_bonus - a.time_bonus) == 0) { // check names comp = (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)); } } return comp; }