Rainbow.prototype.respond = function (context) { context.setCustomData('noPrefix', true); if(context.argText().length) { return c.rainbow(context.argText()); } return null; };
var Game = function Game(channel, client, config) { var self = this; // properties self.waitCount = 0; // number of times waited until enough players self.round = 0; // round number self.players = []; // list of players self.channel = channel; // the channel this game is running on self.client = client; // reference to the irc client self.config = config; // configuration data self.state = STATES.STARTED; // game state storage self.pauseState = []; // pause state storage self.points = []; self.notifyUsersPending = false; console.log('Loaded', config.cards.length, 'cards:'); var questions = _.filter(config.cards, function(card) { return card.type.toLowerCase() === 'question'; }); console.log(questions.length, 'questions'); var answers = _.filter(config.cards, function(card) { return card.type.toLowerCase() === 'answer'; }); console.log(answers.length, 'answers'); // init decks self.decks = { question: new Cards(questions), answer: new Cards(answers) }; // init discard piles self.discards = { question: new Cards(), answer: new Cards() }; // init table slots self.table = { question: null, answer: [] }; // shuffle decks self.decks.question.shuffle(); self.decks.answer.shuffle(); /** * Stop game */ self.stop = function (player) { self.state = STATES.STOPPED; if (typeof player !== 'undefined') { self.say(player.nick + ' stopped the game.'); } else { self.say('Game has been stopped.'); } if(self.round > 1) { // show points if played more than one round self.showPoints(); } // clear all timers clearTimeout(self.startTimeout); clearTimeout(self.stopTimeout); clearTimeout(self.turnTimer); clearTimeout(self.winnerTimer); // Destroy cards & players delete self.players; delete self.config; delete self.client; delete self.channel; delete self.round; delete self.decks; delete self.discards; delete self.table; // set topic self.setTopic(c.bold.yellow('No game is running. Type !start to begin one!')); }; /** * Pause game */ self.pause = function () { // check if game is already paused if (self.state === STATES.PAUSED) { self.say('Game is already paused. Type !resume to begin playing again.'); return false; } // only allow pause if game is in PLAYABLE or PLAYED state if (self.state !== STATES.PLAYABLE && self.state !== STATES.PLAYED) { self.say('The game cannot be paused right now.'); return false; } // store state and pause game var now = new Date(); self.pauseState.state = self.state; self.pauseState.elapsed = now.getTime() - self.roundStarted.getTime(); self.state = STATES.PAUSED; self.say('Game is now paused. Type !resume to begin playing again.'); // clear turn timers clearTimeout(self.turnTimer); clearTimeout(self.winnerTimer); }; /** * Resume game */ self.resume = function () { // make sure game is paused if (self.state !== STATES.PAUSED) { self.say('The game is not paused.'); return false; } // resume game var now = new Date(); var newTime = new Date(); newTime.setTime(now.getTime() - self.pauseState.elapsed); self.roundStarted = newTime; self.state = self.pauseState.state; self.say('Game has been resumed.'); // resume timers if (self.state === STATES.PLAYED) { // check if czar quit during pause if(self.players.indexOf(self.czar) < 0) { // no czar self.say('The czar quit the game during pause. I will pick the winner on this round.'); // select winner self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else { self.winnerTimer = setInterval(self.winnerTimerCheck, 10 * 1000); } } else if (self.state === STATES.PLAYABLE) { self.turnTimer = setInterval(self.turnTimerCheck, 10 * 1000); } }; /** * Start next round */ self.nextRound = function () { clearTimeout(self.stopTimeout); if (self.players.length < 3) { self.say('Not enough players to start a round (need at least 3). Waiting for others to join. Stopping in 3 minutes if not enough players.'); self.state = STATES.WAITING; // stop game if not enough pleyers in 3 minutes self.stopTimeout = setTimeout(self.stop, 3 * 60 * 1000); return false; } self.round++; console.log('Starting round ', self.round); self.setCzar(); self.deal(); self.say('Round ' + self.round + '! ' + self.czar.nick + ' is the card czar.'); self.playQuestion(); // show cards for all players (except czar) _.each(self.players, function (player) { if (player.isCzar !== true) { self.showCards(player); } }); self.state = STATES.PLAYABLE; }; /** * Set a new czar * @returns Player The player object who is the new czar */ self.setCzar = function () { if (self.czar) { console.log('Old czar:', self.czar.nick); } self.czar = self.players[self.players.indexOf(self.czar) + 1] || self.players[0]; console.log('New czar:', self.czar.nick); self.czar.isCzar = true; return self.czar; }; /** * Deal cards to fill players' hands */ self.deal = function () { _.each(self.players, function (player) { console.log(player.nick + '(' + player.hostname + ') has ' + player.cards.numCards() + ' cards. Dealing ' + (10 - player.cards.numCards()) + ' cards'); for (var i = player.cards.numCards(); i < 10; i++) { self.checkDecks(); var card = self.decks.answer.pickCards(); player.cards.addCard(card); card.owner = player; } }, this); }; /** * Clean up table after round is complete */ self.clean = function () { // move cards from table to discard self.discards.question.addCard(self.table.question); self.table.question = null; // var count = self.table.answer.length; _.each(self.table.answer, function (cards) { _.each(cards.getCards(), function (card) { card.owner = null; self.discards.answer.addCard(card); cards.removeCard(card); }, this); }, this); self.table.answer = []; // reset players var removedNicks = []; _.each(self.players, function (player) { player.hasPlayed = false; player.isCzar = false; // check inactive count & remove after 3 if (player.inactiveRounds >= 3) { self.removePlayer(player, {silent: true}); removedNicks.push(player.nick); } }); if (removedNicks.length > 0) { self.say('Removed inactive players: ' + removedNicks.join(', ')); } // reset state self.state = STATES.STARTED; }; /** * Play new question card on the table */ self.playQuestion = function () { self.checkDecks(); var card = self.decks.question.pickCards(); // replace all instance of %s with underscores for prettier output var value = card.value.replace(/\%s/g, '___'); // check if special pick & draw rules if (card.pick > 1) { value += c.bold(' [PICK ' + card.pick + ']'); } if (card.draw > 0) { value += c.bold(' [DRAW ' + card.draw + ']'); } self.say(c.bold('CARD: ') + value); self.table.question = card; // draw cards if (self.table.question.draw > 0) { _.each(_.where(self.players, {isCzar: false}), function (player) { for (var i = 0; i < self.table.question.draw; i++) { self.checkDecks(); var c = self.decks.answer.pickCards(); player.cards.addCard(c); c.owner = player; } }); } // start turn timer, check every 10 secs clearInterval(self.turnTimer); self.roundStarted = new Date(); self.turnTimer = setInterval(self.turnTimerCheck, 10 * 1000); }; /** * Play a answer card from players hand * @param cards card indexes in players hand * @param player Player who played the cards */ self.playCard = function (cards, player) { // don't allow if game is paused if (self.state === STATES.PAUSED) { self.say('Game is currently paused.'); return false; } console.log(player.nick + ' played cards', cards.join(', ')); // make sure different cards are played cards = _.uniq(cards); if (self.state !== STATES.PLAYABLE || player.cards.numCards() === 0) { self.say(player.nick + ': Can\'t play at the moment.'); } else if (typeof player !== 'undefined') { if (player.isCzar === true) { self.say(player.nick + ': You are the card czar. The czar does not play. The czar makes other people do his dirty work.'); } else { if (player.hasPlayed === true) { self.say(player.nick + ': You have already played on this round.'); } else if (cards.length != self.table.question.pick) { // invalid card count self.say(player.nick + ': You must pick ' + self.table.question.pick + ' different cards.'); } else { // get played cards var playerCards; try { playerCards = player.cards.pickCards(cards); } catch (error) { self.notice(player.nick, 'Invalid card index'); return false; } self.table.answer.push(playerCards); player.hasPlayed = true; player.inactiveRounds = 0; self.notice(player.nick, 'You played: ' + self.getFullEntry(self.table.question, playerCards.getCards())); // show entries if all players have played if (self.checkAllPlayed()) { self.showEntries(); } } } } else { console.warn('Invalid player tried to play a card'); } }; /** * Check the time that has elapsed since the beinning of the turn. * End the turn is time limit is up */ self.turnTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 3 * 60 * 1000; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Round elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('The round timed out'); self.say('Time is up!'); self.markInactivePlayers(); // show end of turn self.showEntries(); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say('10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say('30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say('Hurry up, 1 minute left!'); self.showStatus(); } }; /** * Show the entries */ self.showEntries = function () { // clear round timer clearInterval(self.turnTimer); self.state = STATES.PLAYED; // Check if 2 or more entries... if (self.table.answer.length === 0) { self.say('No one played on this round.'); // skip directly to next round self.clean(); self.nextRound(); } else if (self.table.answer.length === 1) { self.say('Only one player played and is the winner by default.'); self.selectWinner(0); } else { self.say('Everyone has played. Here are the entries:'); // shuffle the entries self.table.answer = _.shuffle(self.table.answer); _.each(self.table.answer, function (cards, i) { self.say(i + ": " + self.getFullEntry(self.table.question, cards.getCards())); }, this); // check that czar still exists var currentCzar = _.findWhere(this.players, {isCzar: true}); if (typeof currentCzar === 'undefined') { // no czar, random winner (TODO: Voting?) self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else { self.say(self.czar.nick + ': Select the winner (!winner <entry number>)'); // start turn timer, check every 10 secs clearInterval(self.winnerTimer); self.roundStarted = new Date(); self.winnerTimer = setInterval(self.winnerTimerCheck, 10 * 1000); } } }; /** * Check the time that has elapsed since the beinning of the winner select. * End the turn is time limit is up */ self.winnerTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 2 * 60 * 1000; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Winner selection elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('the czar is inactive, selecting winner'); self.say('Time is up. I will pick the winner on this round.'); // Check czar & remove player after 3 timeouts self.czar.inactiveRounds++; // select winner self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say(self.czar.nick + ': 10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say(self.czar.nick + ': 30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say(self.czar.nick + ': Hurry up, 1 minute left!'); } }; /** * Pick an entry that wins the round * @param index Index of the winning card in table list * @param player Player who said the command (use null for internal calls, to ignore checking) */ self.selectWinner = function (index, player) { // don't allow if game is paused if (self.state === STATES.PAUSED) { self.say('Game is currently paused.'); return false; } // clear winner timer clearInterval(self.winnerTimer); var winner = self.table.answer[index]; if (self.state === STATES.PLAYED) { if (typeof player !== 'undefined' && player !== self.czar) { client.say(player.nick + ': You are not the card czar. Only the card czar can select the winner'); } else if (typeof winner === 'undefined') { self.say('Invalid winner'); } else { self.state = STATES.ROUND_END; var owner = winner.cards[0].owner; owner.points++; // update points object _.findWhere(self.points, {player: owner}).points = owner.points; // announce winner self.say(c.bold('Winner is: ') + owner.nick + ' with "' + self.getFullEntry(self.table.question, winner.getCards()) + '" and gets one awesome point! ' + owner.nick + ' has ' + owner.points + ' awesome points.'); self.clean(); self.nextRound(); } } }; /** * Get formatted entry * @param question * @param answers * @returns {*|Object|ServerResponse} */ self.getFullEntry = function (question, answers) { var args = [question.value]; _.each(answers, function (card) { args.push(card.value); }, this); return util.format.apply(this, args); }; /** * Check if all active players played on the current round * @returns Boolean true if all players have played */ self.checkAllPlayed = function () { var allPlayed = false; if (self.getNotPlayed().length === 0) { allPlayed = true; } return allPlayed; }; /** * Check if decks are empty & reset with discards */ self.checkDecks = function () { // check answer deck if (self.decks.answer.numCards() === 0) { console.log('answer deck is empty. reset from discard.'); self.decks.answer.reset(self.discards.answer.reset()); self.decks.answer.shuffle(); } // check question deck if (self.decks.question.numCards() === 0) { console.log('question deck is empty. reset from discard.'); self.decks.question.reset(self.discards.question.reset()); self.decks.question.shuffle(); } }; /** * Add a player to the game * @param player Player object containing new player's data * @returns The new player or false if invalid player */ self.addPlayer = function (player) { if (typeof self.getPlayer({user: player.user, hostname: player.hostname}) === 'undefined') { self.players.push(player); self.say(player.nick + ' has joined the game'); // check if player is returning to game var pointsPlayer = _.findWhere(self.points, {user: player.user, hostname: player.hostname}); if (typeof pointsPlayer === 'undefined') { // new player self.points.push({ user: player.user, // user and hostname are used for matching returning players hostname: player.hostname, player: player, // reference to player object saved to points object as well points: 0 }); } else { // returning player pointsPlayer.player = player; player.points = pointsPlayer.points; } // check if waiting for players if (self.state === STATES.WAITING && self.players.length >= 3) { // enough players, start the game self.nextRound(); } return player; } else { console.log('Player tried to join again', player.nick, player.user, player.hostname); } return false; }; /** * Find player * @param search * @returns {*} */ self.getPlayer = function (search) { return _.findWhere(self.players, search); }; /** * Remove player from game * @param player * @param options Extra options * @returns The removed player or false if invalid player */ self.removePlayer = function (player, options) { options = _.extend({}, options); if (typeof player !== 'undefined') { console.log('removing' + player.nick + ' from the game'); // get cards in hand var cards = player.cards.reset(); // remove player self.players = _.without(self.players, player); // put player's cards to discard _.each(cards, function (card) { console.log('Add card ', card.text, 'to discard'); self.discards.answer.addCard(card); }); if (options.silent !== true) { self.say(player.nick + ' has left the game'); } // check if remaining players have all player if (self.state === STATES.PLAYABLE && self.checkAllPlayed()) { self.showEntries(); } // check czar if (self.state === STATES.PLAYED && self.czar === player) { self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } return player; } return false; }; /** * Get all player who have not played * @returns Array list of Players that have not played */ self.getNotPlayed = function () { return _.where(_.filter(self.players, function (player) { // check only players with cards (so players who joined in the middle of a round are ignored) return player.cards.numCards() > 0; }), {hasPlayed: false, isCzar: false}); }; /** * Check for inactive players * @param options */ self.markInactivePlayers = function (options) { _.each(self.getNotPlayed(), function (player) { player.inactiveRounds++; }, this); }; /** * Show players cards to player * @param player */ self.showCards = function (player) { if (typeof player !== 'undefined') { var cards = ""; _.each(player.cards.getCards(), function (card, index) { cards += c.bold(' [' + index + '] ') + card.value; }, this); self.notice(player.nick, 'Your cards are:' + cards); } }; /** * Show points for all players */ self.showPoints = function () { var sortedPlayers = _.sortBy(self.points, function (point) { return -point.player.points; }); var output = ""; _.each(sortedPlayers, function (point) { output += point.player.nick + " " + point.points + " awesome points, "; }); self.say('The most horrible people: ' + output.slice(0, -2)); }; /** * Show status */ self.showStatus = function () { var playersNeeded = Math.max(0, 3 - self.players.length), // amount of player needed to start the game timeLeft = 30 - Math.round((new Date().getTime() - self.startTime.getTime()) / 1000), // time left until first round activePlayers = _.filter(self.players, function (player) { // only players with cards in hand are active return player.cards.numCards() > 0; }), played = _.where(activePlayers, {isCzar: false, hasPlayed: true}), // players who have already played notPlayed = _.where(activePlayers, {isCzar: false, hasPlayed: false}); // players who have not played yet switch (self.state) { case STATES.PLAYABLE: self.say(c.bold('Status: ') + self.czar.nick + ' is the czar. Waiting for players to play: ' + _.pluck(notPlayed, 'nick').join(', ')); break; case STATES.PLAYED: self.say(c.bold('Status: ') + 'Waiting for ' + self.czar.nick + ' to select the winner.'); break; case STATES.ROUND_END: self.say(c.bold('Status: ') + 'Round has ended and next one is starting.'); break; case STATES.STARTED: self.say(c.bold('Status: ') + 'Game starts in ' + timeLeft + ' seconds. Need ' + playersNeeded + ' more players to start.'); break; case STATES.STOPPED: self.say(c.bold('Status: ') + 'Game has been stopped.'); break; case STATES.WAITING: self.say(c.bold('Status: ') + 'Not enough players to start. Need ' + playersNeeded + ' more players to start.'); break; case STATES.PAUSED: self.say(c.bold('Status: ') + 'Game is paused.'); break; } }; /** * Set the channel topic */ self.setTopic = function (topic) { // ignore if not configured to set topic if (typeof config.setTopic === 'undefined' || !config.setTopic) { return false; } // construct new topic var newTopic = topic; if (typeof config.topicBase !== 'undefined') { newTopic = topic + ' ' + config.topicBase; } // set it client.send('TOPIC', channel, newTopic); }; /** * List all players in the current game */ self.listPlayers = function () { self.say('Players currently in the game: ' + _.pluck(self.players, 'nick').join(', ')); }; /** * Handle player quits and parts * @param channel * @param nick * @param reason * @param message */ self.playerLeaveHandler = function (channel, nick, reason, message) { console.log('Player ' + nick + ' left'); var player = self.getPlayer({nick: nick}); if (typeof player !== 'undefined') { self.removePlayer(player); } }; /** * Handle player nick changes * @param oldnick * @param newnick * @param channels * @param message */ self.playerNickChangeHandler = function (oldnick, newnick, channels, message) { console.log('Player changed nick from ' + oldnick + ' to ' + newnick); var player = self.getPlayer({nick: oldnick}); if (typeof player !== 'undefined') { player.nick = newnick; } }; /** * Notify users in channel that game has started */ self.notifyUsers = function() { // request names client.send('NAMES', channel); // signal handler to send notifications self.notifyUsersPending = true; }; /** * Handle names response to notify users * @param nicks */ self.notifyUsersHandler = function(nicks) { // ignore if we haven't requested this if (self.notifyUsersPending === false) { return false; } // don't message nicks with these modes var exemptModes = ['~', '&']; // loop through and send messages _.each(nicks, function(mode, nick) { if (_.indexOf(exemptModes, mode) < 0 && nick !== config.nick) { self.notice(nick, nick + ': A new game of Cards Against Humanity just began in ' + channel + '. Head over and !join if you\'d like to get in on the fun!'); } }); // reset self.notifyUsersPending = false; }; /** * Public message to the game channel * @param string */ self.say = function (string) { self.client.say(self.channel, string); }; self.pm = function (nick, string) { self.client.say(nick, string); }; self.notice = function (nick, string) { self.client.notice(nick, string); }; // set topic self.setTopic(c.bold.lime('A game is running. Type !join to get in on it!')); // announce the game on the channel self.say('A new game of ' + c.rainbow('Cards Against Humanity') + '. The game starts in 30 seconds. Type !join to join the game any time.'); // notify users if (typeof config.notifyUsers !== 'undefined' && config.notifyUsers) { self.notifyUsers(); } // wait for players to join self.startTime = new Date(); self.startTimeout = setTimeout(self.nextRound, 30000); // client listeners client.addListener('part', self.playerLeaveHandler); client.addListener('quit', self.playerLeaveHandler); client.addListener('nick', self.playerNickChangeHandler); client.addListener('names'+channel, self.notifyUsersHandler); };
var Game = function Game(channel, client, config) { var self = this; // properties self.waitCount = 0; // number of times waited until enough players self.round = 0; // round number self.players = []; // list of players self.channel = channel; // the channel this game is running on self.client = client; // reference to the irc client self.config = config; // configuration data self.state = STATES.STARTED; // game state storage self.points = []; console.log('whites', config.cards.whites); // init decks self.decks = { white: new Cards(config.cards.whites), black: new Cards(config.cards.blacks) }; // init discard piles self.discards = { white: new Cards(), black: new Cards() }; // init table slots self.table = { white: null, black: [] }; // shuffle decks self.decks.white.shuffle(); self.decks.black.shuffle(); /** * Stop game */ self.stop = function (player) { self.state = STATES.STOPPED; if (typeof player !== 'undefined') { self.say(player.nick + ' stopped the game.'); } else { self.say('Game has been stopped.'); } self.showPoints(); clearTimeout(self.startTimeout); // TODO: Destroy cards & players delete self.players; delete self.config; delete self.client; delete self.channel; delete self.round; delete self.decks; delete self.discards; delete self.table; }; /** * Start next round */ self.nextRound = function () { clearTimeout(self.stopTimeout); if (self.players.length < 3) { self.say('Not enough players to start a round (need at least 3). Waiting for others to join. Stopping in 3 minutes if not enough players.'); self.state = STATES.WAITING; // stop game if not enough pleyers in 3 minutes self.stopTimeout = setTimeout(self.stop, 3 * 60 * 1000); return false; } self.round++; console.log('Starting round ', self.round); self.setCzar(); self.deal(); self.say('Round ' + self.round + '! ' + self.czar.nick + ' is the card czar.'); self.playWhite(); // show cards for all players (except czar) var timeout = 0; _.each(_.where(self.players, {isCzar: false}), function (player) { setTimeout(self.showCards, timeout + 1000, player); }); self.state = STATES.PLAYABLE; }; /** * Set a new czar * @returns Player The player object who is the new czar */ self.setCzar = function () { if (self.czar) { console.log('Old czar:', self.czar.nick); } self.czar = self.players[self.players.indexOf(self.czar) + 1] || self.players[0]; console.log('New czar:', self.czar.nick); self.czar.isCzar = true; return self.czar; }; /** * Deal cards to fill players' hands */ self.deal = function () { _.each(self.players, function (player) { console.log(player.nick + '(' + player.hostname + ') has ' + player.cards.numCards() + ' cards. Dealing ' + (10 - player.cards.numCards()) + ' cards'); for (var i = player.cards.numCards(); i < 10; i++) { self.checkDecks(); var card = self.decks.black.pickCards(); player.cards.addCard(card); card.owner = player; } }, this); }; /** * Clean up table after round is complete */ self.clean = function () { // move cards from table to discard self.discards.white.addCard(self.table.white); self.table.white = null; // var count = self.table.black.length; _.each(self.table.black, function (cards) { _.each(cards.getCards(), function (card) { card.owner = null; self.discards.black.addCard(card); cards.removeCard(card); }, this); }, this); self.table.black = []; // reset players var removedNicks = []; _.each(self.players, function (player) { player.hasPlayed = false; player.isCzar = false; // check inactive count & remove after 3 if(player.inactiveRounds >= 3) { self.removePlayer(player, {silent: true}); removedNicks.push(player.nick); } }); if(removedNicks.length > 0) { self.say('Removed inactive players: ' + removedNicks.join(', ')); } // reset state self.state = STATES.STARTED; }; /** * Play new white card on the table */ self.playWhite = function () { self.checkDecks(); var card = self.decks.white.pickCards(); // replace all instance of %s with underscores for prettier output var text = card.text.replace(/\%s/g, '___'); // check if special pick & draw rules if (card.pick > 1) { text += c.bold(' [PICK ' + card.pick + ']'); } if (card.draw > 0) { text += c.bold(' [DRAW ' + card.draw + ']'); } self.say(c.bold('CARD: ') + text); self.table.white = card; // draw cards if (self.table.white.draw > 0) { _.each(_.where(self.players, {isCzar: false}), function (player) { for (var i = 0; i < self.table.white.draw; i++) { self.checkDecks(); var c = self.decks.black.pickCards(); player.cards.addCard(c); c.owner = player; } }); } // start turn timer, check every 10 secs clearInterval(self.turnTimer); self.roundStarted = new Date(); self.turnTimer = setInterval(self.turnTimerCheck, 10 * 1000); }; /** * Play a black card from players hand * @param cards card indexes in players hand * @param player Player who played the cards */ self.playCard = function (cards, player) { console.log(player.nick + ' played cards', cards.join(', ')); // make sure different cards are played cards = _.uniq(cards); if (self.state !== STATES.PLAYABLE || player.cards.numCards() === 0) { self.say(player.nick + ': Can\'t play at the moment.'); } else if (typeof player !== 'undefined') { if (player.isCzar === true) { self.say(player.nick + ': You are the card czar. The czar does not play. The czar makes other people do his dirty work.'); } else { if (player.hasPlayed === true) { self.say(player.nick + ': You have already played on this round.'); } else if (cards.length != self.table.white.pick) { // invalid card count self.say(player.nick + ': You must pick ' + self.table.white.pick + ' different cards.'); } else { // get played cards var playerCards; try { playerCards = player.cards.pickCards(cards); } catch (error) { self.notice(player.nick, 'Invalid card index'); return false; } self.table.black.push(playerCards); player.hasPlayed = true; player.inactiveRounds = 0; self.notice(player.nick, 'You played: ' + self.getFullEntry(self.table.white, playerCards.getCards())); // show entries if all players have played if (self.checkAllPlayed()) { self.showEntries(); } } } } else { console.warn('Invalid player tried to play a card'); } }; /** * Check the time that has elapsed since the beinning of the turn. * End the turn is time limit is up */ self.turnTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 3 * 60 * 1000; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Round elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('The round timed out'); self.say('Time is up!'); self.markInactivePlayers(); // show end of turn self.showEntries(); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say('10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say('30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say('Hurry up, 1 minute left!'); } }; /** * Show the entries */ self.showEntries = function () { // clear round timer clearInterval(self.turnTimer); self.state = STATES.PLAYED; // Check if 2 or more entries... if (self.table.black.length === 0) { self.say('No one played on this round.'); // skip directly to next round self.clean(); self.nextRound(); } else if (self.table.black.length === 1) { self.say('Only one player played and is the winner by default.'); self.selectWinner(0); } else { self.say('Everyone has played. Here are the entries:'); // shuffle the entries self.table.black = _.shuffle(self.table.black); _.each(self.table.black, function (cards, i) { self.say(i + ": " + self.getFullEntry(self.table.white, cards.getCards())); }, this); // check that czar still exists var currentCzar = _.findWhere(this.players, {isCzar: true}); if (typeof currentCzar === 'undefined') { // no czar, random winner (TODO: Voting?) self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.black.length - 1))); } else { self.say(self.czar.nick + ': Select the winner (!winner <entry number>)'); // start turn timer, check every 10 secs clearInterval(self.winnerTimer); self.roundStarted = new Date(); self.winnerTimer = setInterval(self.winnerTimerCheck, 10 * 1000); } } }; /** * Check the time that has elapsed since the beinning of the winner select. * End the turn is time limit is up */ self.winnerTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 2 * 60 * 1000; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Winner selecgtion elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('the czar is inactive, selecting winner'); self.say('Time is up. I will pick the winner on this round.'); // Check czar & remove player after 3 timeouts self.czar.inactiveRounds++; // select winner self.selectWinner(Math.round(Math.random() * (self.table.black.length - 1))); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say(self.czar.nick + ': 10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say(self.czar.nick + ': 30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say(self.czar.nick + ': Hurry up, 1 minute left!'); } }; /** * Pick an entry that wins the round * @param index Index of the winning card in table list * @param player Player who said the command (use null for internal calls, to ignore checking) */ self.selectWinner = function (index, player) { // clear winner timer clearInterval(self.winnerTimer); var winner = self.table.black[index]; if (self.state === STATES.PLAYED) { if (typeof player !== 'undefined' && player !== self.czar) { client.say(player.nick + ': You are not the card czar. Only the card czar can select the winner'); } else if (typeof winner === 'undefined') { self.say('Invalid winner'); } else { self.state = STATES.ROUND_END; var owner = winner.cards[0].owner; owner.points++; // update points object _.findWhere(self.points, {player: owner}).points = owner.points; // announce winner self.say(c.bold('Winner is: ') + owner.nick + ' with "' + self.getFullEntry(self.table.white, winner.getCards()) + '" and gets one awesome point! ' + owner.nick + ' has ' + owner.points + ' awesome points.'); self.clean(); self.nextRound(); } } }; /** * Get formatted entry * @param white * @param blacks * @returns {*|Object|ServerResponse} */ self.getFullEntry = function (white, blacks) { var args = [white.text]; _.each(blacks, function (card) { args.push(card.text); }, this); return util.format.apply(this, args); }; /** * Check if all active players played on the current round * @returns Boolean true if all players have played */ self.checkAllPlayed = function () { var allPlayed = false; if (self.getNotPlayed().length === 0) { allPlayed = true; } return allPlayed; }; /** * Check if decks are empty & reset with discards */ self.checkDecks = function () { // check black deck if (self.decks.black.numCards() === 0) { console.log('black deck is empty. reset from discard.'); self.decks.black.reset(self.discards.black.reset()); self.decks.black.shuffle(); console.log(self.decks.black.numCards()); } // check white deck if (self.decks.white.numCards() === 0) { console.log('white deck is empty. reset from discard.'); self.decks.white.reset(self.discards.white.reset()); self.decks.white.shuffle(); console.log(self.decks.white.numCards()); } }; /** * Add a player to the game * @param player Player object containing new player's data * @returns The new player or false if invalid player */ self.addPlayer = function (player) { if (typeof self.getPlayer({hostname: player.hostname}) === 'undefined') { self.players.push(player); self.say(player.nick + ' has joined the game'); // check if player is returning to game var pointsPlayer = _.findWhere(self.points, {hostname: player.hostname}); if (typeof pointsPlayer === 'undefined') { // new player self.points.push({ hostname: player.hostname, // user for searching player: player, points: 0 }); } else { // returning player pointsPlayer.player = player; player.points = pointsPlayer.points; } // check if waiting for players if (self.state === STATES.WAITING && self.players.length >= 3) { // enough players, start the game self.nextRound(); } return player; } else { console.log('Player tried to join again', player.nick, player.hostname); } return false; }; /** * Find player * @param search * @returns {*} */ self.getPlayer = function (search) { return _.findWhere(self.players, search); }; /** * Remove player from game * @param player * @param options Extra options * @returns The removed player or false if invalid player */ self.removePlayer = function (player, options) { options = _.extend({}, options); if (typeof player !== 'undefined') { self.players = _.without(self.players, player); if (options.silent !== true) { self.say(player.nick + ' has left the game'); } // check if remaining players have all player if (self.state === STATES.PLAYABLE && self.checkAllPlayed()) { self.showEntries(); } // check czar if (self.state === STATES.PLAYED && self.czar === player) { self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.black.length - 1))); } return player; } return false; }; /** * Get all player who have not played * @returns Array list of Players that have not played */ self.getNotPlayed = function () { return _.where(_.filter(self.players, function (player) { // check only players with cards (so players who joined in the middle of a round are ignored) return player.cards.numCards() > 0; }), {hasPlayed: false, isCzar: false}); }; /** * Check for inactive players * @param options */ self.markInactivePlayers = function(options) { _.each(self.getNotPlayed(), function (player) { player.inactiveRounds++; }, this); }; /** * Show players cards to player * @param player */ self.showCards = function (player) { if (typeof player !== 'undefined') { var cards = ""; _.each(player.cards.getCards(), function (card, index) { cards += c.bold(' [' + index + '] ') + card.text; }, this); self.notice(player.nick, 'Your cards are:' + cards); } }; /** * Show points for all players */ self.showPoints = function () { var sortedPlayers = _.sortBy(self.points, function (point) { return -point.player.points; }); var output = ""; _.each(sortedPlayers, function (point) { output += point.player.nick + " " + point.points + " awesome points, "; }); self.say('The most horrible people: ' + output.slice(0, -2)); }; /** * List all players in the current game */ self.listPlayers = function () { self.say('Players currently in the game: ' + _.pluck(self.players, 'nick').join(', ')); }; /** * Handle player quits and parts * @param channel * @param nick * @param reason * @param message */ self.playerLeaveHandler = function(channel, nick, reason, message) { console.log('Player ' + nick + ' left'); var player = self.getPlayer({nick: nick}); if(typeof player !== 'undefined') { self.removePlayer(player); } }; /** * Handle player nick changes * @param oldnick * @param newnick * @param channels * @param message */ self.playerNickChangeHandler = function(oldnick, newnick, channels, message) { console.log('Player changed nick from ' + oldnick + ' to ' + newnick); var player = self.getPlayer({nick: oldnick}); if(typeof player !== 'undefined') { player.nick = newnick; } }; /** * Public message to the game channel * @param string */ self.say = function (string) { self.client.say(self.channel, string); }; self.pm = function (nick, string) { self.client.say(nick, string); }; self.notice = function (nick, string) { self.client.notice(nick, string); }; // announce the game on the channel self.say('A new game of ' + c.rainbow('Cards Against Humanity') + '. The game starts in 30 seconds. Type !join to join the game any time.'); // wait for players to join self.startTimeout = setTimeout(self.nextRound, 30000); // client listeners client.addListener('part', self.playerLeaveHandler); client.addListener('quit', self.playerLeaveHandler); client.addListener('nick', self.playerNickChangeHandler); };
var Game = function Game(channel, client, config, cmdArgs, dbModels) { var self = this; // properties self.waitCount = 0; // number of times waited until enough players self.round = 0; // round number self.players = []; // list of players self.playersToAdd = [] // list of players to add after deferring because the game doesn't exist in the database yet self.channel = channel; // the channel this game is running on self.client = client; // reference to the irc client self.config = config; // configuration data self.state = STATES.STARTED; // game state storage self.pauseState = []; // pause state storage self.notifyUsersPending = false; self.pointLimit = 0; // point limit for the game, defaults to 0 (== no limit) self.dbModels = dbModels; /* * * Database functions * */ self.updateOrCreateInstance = function(model, query, createFields, updateFields) { model.findOne(query).then(function (instance) { if (instance === null && createFields !== null) { model.create(createFields); } else if (instance !== null && updateFields !== null) { instance.update(updateFields); } }); }; self.updatePointsDatabaseTable = function() { self.players.forEach(function (player) { self.dbModels.Player.findOne({where: {nick: player.nick}}).then(function (dbPlayer) { self.updateOrCreateInstance( self.dbModels.Points, {where: {player_id: dbPlayer.id, game_id: self.dbGame.id}}, {player_id: dbPlayer.id, game_id: self.dbGame.id, points: player.points}, {points: player.points} ) }); }); }; self.updateCardComboTable = function(id, playerCards) { var round = self.dbCurrentRound; var cardString = []; self.dbModels.Answer.findAll({ where: { text: { in: _.map(playerCards, function(card) { return card.value; }) } } }).then(function (cards) { if (playerCards.length === 1) { cardString = cards[0].id; } else { var cardString = []; playerCards.forEach(function (playerCard) { cards.forEach(function (card) { if (playerCard.value === card.text) { cardString.push(card.id); } }); }); cardString = cardString.join(','); } self.updateOrCreateInstance(self.dbModels.CardCombo, { where: { game_id: self.dbGame.id, player_id: id, question_id: round.question_id } }, { game_id: self.dbGame.id, player_id: id, question_id: round.question_id, answer_ids: cardString, winner: false }, null ); // Finally update each of the cards times played count cards.forEach(function (card) { card.update({times_played: card.times_played + 1}); }); }); }; self.createRound = function(question_id) { self.dbModels.Round.create({ game_id: self.dbGame.id, round_number: self.round, num_active_players: _.filter(self.players, function (player) {return player.isActive}).length, total_players: self.players.length, question_id: question_id }).then(function (round) { self.dbCurrentRound = round; }); }; // Adding game to database self.dbModels.Game.create({num_rounds: self.round}).then(function (game) { self.dbGame = game; self.playersToAdd.forEach(function (player) { self.addPlayer(player); }); }); console.log('Loaded', config.cards.length, 'cards:'); var questions = _.filter(config.cards, function(card) { return card.type.toLowerCase() === 'question'; }); console.log(questions.length, 'questions'); var answers = _.filter(config.cards, function(card) { return card.type.toLowerCase() === 'answer'; }); console.log(answers.length, 'answers'); // init decks self.decks = { question: new Cards(questions), answer: new Cards(answers) }; // init discard piles self.discards = { question: new Cards(), answer: new Cards() }; // init table slots self.table = { question: null, answer: [] }; // shuffle decks self.decks.question.shuffle(); self.decks.answer.shuffle(); // parse point limit from configuration file if(typeof config.gameOptions.pointLimit !== 'undefined' && !isNaN(config.gameOptions.pointLimit)) { console.log('Set game point limit to ' + config.gameOptions.pointLimit + ' from config'); self.pointLimit = parseInt(config.gameOptions.pointLimit); } // parse point limit from command arguments if(typeof cmdArgs[0] !== 'undefined' && !isNaN(cmdArgs[0])) { console.log('Set game point limit to ' + cmdArgs[0] + ' from arguments'); self.pointLimit = parseInt(cmdArgs[0]); } /** * Stop game */ self.stop = function (player, pointLimitReached) { self.state = STATES.STOPPED; if (typeof player !== 'undefined' && player !== null) { self.say(player.nick + ' stopped the game.'); } if(self.round > 1) { // show points if played more than one round self.showPoints(); } if (pointLimitReached !== true) { self.say('Game has been stopped.'); self.dbGame.update({ended_at: new Date(), num_rounds: self.round, winner_id: null}); } else { winner = self.getPlayer({points: self.pointLimit}); self.dbModels.Player.findOne({where: {nick: winner.nick}}).then(function (player) { self.dbGame.update({ended_at: new Date(), num_rounds: self.round, winner_id: player.id}); }); } // Update points table self.updatePointsDatabaseTable(); // clear all timers clearTimeout(self.startTimeout); clearTimeout(self.stopTimeout); clearTimeout(self.turnTimer); clearTimeout(self.winnerTimer); // Remove listeners client.removeListener('part', self.playerPartHandler); client.removeListener('quit', self.playerQuitHandler); client.removeListener('kick' + self.channel, self.playerKickHandler); client.removeListener('nick', self.playerNickChangeHandler); client.removeListener('names'+ self.channel, self.notifyUsersHandler); // Destroy game properties delete self.players; delete self.config; delete self.client; delete self.channel; delete self.round; delete self.decks; delete self.discards; delete self.table; // set topic self.setTopic(c.bold.yellow('No game is running. Type !start to begin one!')); }; /** * Pause game */ self.pause = function () { // check if game is already paused if (self.state === STATES.PAUSED) { self.say('Game is already paused. Type !resume to begin playing again.'); return false; } // only allow pause if game is in PLAYABLE or PLAYED state if (self.state !== STATES.PLAYABLE && self.state !== STATES.PLAYED) { self.say('The game cannot be paused right now.'); return false; } // store state and pause game var now = new Date(); self.pauseState.state = self.state; self.pauseState.elapsed = now.getTime() - self.roundStarted.getTime(); self.state = STATES.PAUSED; self.say('Game is now paused. Type !resume to begin playing again.'); // clear turn timers clearTimeout(self.turnTimer); clearTimeout(self.winnerTimer); }; /** * Resume game */ self.resume = function () { // make sure game is paused if (self.state !== STATES.PAUSED) { self.say('The game is not paused.'); return false; } // resume game var now = new Date(); var newTime = new Date(); newTime.setTime(now.getTime() - self.pauseState.elapsed); self.roundStarted = newTime; self.state = self.pauseState.state; self.say('Game has been resumed.'); // resume timers if (self.state === STATES.PLAYED) { // check if czar quit during pause if(self.players.indexOf(self.czar) < 0) { // no czar self.say('The czar quit the game during pause. I will pick the winner on this round.'); // select winner self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else { self.winnerTimer = setInterval(self.winnerTimerCheck, 10 * 1000); } } else if (self.state === STATES.PLAYABLE) { self.turnTimer = setInterval(self.turnTimerCheck, 10 * 1000); } }; /** * Start next round */ self.nextRound = function () { clearTimeout(self.stopTimeout); // check if any player reached the point limit if(self.pointLimit > 0) { var winner = _.findWhere(self.players, {points: self.pointLimit}); if(winner) { self.say(winner.nick + ' has the limit of ' + self.pointLimit + ' awesome ' + inflection.inflect('points', self.pointLimit) + ' and is the winner of the game! Congratulations!'); self.stop(null, true); return false; } } // check that there's enough players in the game if (_.where(self.players, { isActive: true}).length < 3) { self.say('Not enough players to start a round (need at least 3). Waiting for others to join. Stopping in ' + config.gameOptions.roundMinutes + ' ' + inflection.inflect('minutes', config.gameOptions.roundMinutes) + ' if not enough players.'); self.state = STATES.WAITING; // stop game if not enough pleyers in however many minutes in the config self.stopTimeout = setTimeout(self.stop, 60 * 1000 * config.gameOptions.roundMinutes); return false; } self.updatePointsDatabaseTable(); self.round++; self.dbGame.update({num_rounds: self.round}); console.log('Starting round ', self.round); self.setCzar(); self.deal(); self.say('Round ' + self.round + '! ' + self.czar.nick + ' is the card czar.'); self.playQuestion(); // show cards for all players (except czar) _.each(self.players, function (player) { if (player.isCzar !== true && player.isActive === true) { self.showCards(player); } }); self.state = STATES.PLAYABLE; }; /** * Set a new czar * @returns Player The player object who is the new czar */ self.setCzar = function () { if (self.czar) { console.log('Old czar: ' + self.czar.nick); var nextCzar; self.players.forEach(function (player) { console.log(player.nick + ': ' + player.isActive); }); for (var i = (self.players.indexOf(self.czar) + 1) % self.players.length; i !== self.players.indexOf(self.czar); i = (i + 1) % self.players.length) { console.log(i + ': ' + self.players[i].nick + ': ' + self.players[i].isActive); if (self.players[i].isActive === true) { nextCzar = i; break; } } self.czar = self.players[i]; } else { self.czar = _.where(self.players, { isActive: true })[0]; } console.log('New czar:', self.czar.nick); self.czar.isCzar = true; return self.czar; }; /** * Deal cards to fill players' hands */ self.deal = function (player, num) { if (typeof player === 'undefined') { _.each(self.players, function (player) { if (player.isActive) { console.log(player.nick + '(' + player.hostname + ') has ' + player.cards.numCards() + ' cards. Dealing ' + (10 - player.cards.numCards()) + ' cards'); for (var i = player.cards.numCards(); i < 10; i++) { self.checkDecks(); var card = self.decks.answer.pickCards(); player.cards.addCard(card); card.owner = player; } } }, this); } else { if (typeof num !== 'undefined') { for (var i = player.cards.numCards(); i < num; i++) { self.checkDecks(); var card = self.decks.answer.pickCards(); player.cards.addCard(card); card.owner = player; } } } }; /** * Clean up table after round is complete */ self.clean = function () { // move cards from table to discard self.discards.question.addCard(self.table.question); self.table.question = null; // var count = self.table.answer.length; _.each(self.table.answer, function (cards) { _.each(cards.getCards(), function (card) { card.owner = null; self.discards.answer.addCard(card); cards.removeCard(card); }, this); }, this); self.table.answer = []; // reset players var removedNicks = []; _.each(self.players, function (player) { player.hasPlayed = false; player.hasDiscarded = false; player.isCzar = false; // check if idled and remove if (player.inactiveRounds >= 1) { player.inactiveRounds=0; self.removePlayer(player, {silent: true}); removedNicks.push(player.nick); } }); if (removedNicks.length > 0) { self.say('Removed inactive ' + inflection.inflect('players', removedNicks.length) + ': ' + removedNicks.join(', ')); } // reset state self.state = STATES.STARTED; }; /** * Play new question card on the table */ self.playQuestion = function () { self.checkDecks(); var card = self.decks.question.pickCards(); // replace all instance of %s with underscores for prettier output var value = card.value.replace(/\%s/g, '___'); // check if special pick & draw rules if (card.pick > 1) { value += c.bold(' [PICK ' + card.pick + ']'); } if (card.draw > 0) { value += c.bold(' [DRAW ' + card.draw + ']'); } self.say(c.bold('CARD: ') + value); self.table.question = card; // Record card and round in the database self.dbModels.Question.findOne({where: {text: card.value}}).then(function (instance) { instance.update({times_played: instance.times_played + 1}).then(function (q) { self.createRound(q.id); }); }); // PM Card to players _.each(_.where(self.players, {isCzar: false, isActive: true}), function(player) { self.pm(player.nick, c.bold('CARD: ') + value); }); // draw cards if (self.table.question.draw > 0) { _.each(_.where(self.players, {isCzar: false, isActive: true}), function (player) { for (var i = 0; i < self.table.question.draw; i++) { self.checkDecks(); var c = self.decks.answer.pickCards(); player.cards.addCard(c); c.owner = player; } }); } // start turn timer, check every 10 secs clearInterval(self.turnTimer); self.roundStarted = new Date(); self.turnTimer = setInterval(self.turnTimerCheck, 10 * 1000); }; /** * Play a answer card from players hand * @param cards card indexes in players hand * @param player Player who played the cards */ self.playCard = function (cards, player) { // don't allow if game is paused if (self.state === STATES.PAUSED) { self.say('Game is currently paused.'); return false; } console.log(player.nick + ' played cards', cards.join(', ')); // make sure different cards are played cards = _.uniq(cards); if (self.state !== STATES.PLAYABLE || player.cards.numCards() === 0) { self.say(player.nick + ': Can\'t play at the moment.'); } else if (typeof player !== 'undefined') { if (player.isCzar === true) { self.say(player.nick + ': You are the card czar. The czar does not play. The czar makes other people do their dirty work.'); } else { if (player.hasPlayed === true) { self.say(player.nick + ': You have already played on this round.'); } else if (cards.length != self.table.question.pick) { // invalid card count self.say(player.nick + ': You must pick ' + inflection.inflect('cards', self.table.question.pick, '1 card', self.table.question.pick + ' different cards') + '.'); } else { // get played cards var playerCards; try { playerCards = player.cards.pickCards(cards); } catch (error) { self.pm(player.nick, 'Invalid card index'); return false; } self.table.answer.push(playerCards); player.hasPlayed = true; player.inactiveRounds = 0; self.pm(player.nick, 'You played: ' + self.getFullEntry(self.table.question, playerCards.getCards())); // Update card combo table self.dbModels.Player.findOne({where: {nick: player.nick}}).then(function (dbPlayer) { self.updateCardComboTable(dbPlayer.id, playerCards.getCards()); }); // show entries if all players have played if (self.checkAllPlayed()) { self.showEntries(); } } } } else { console.warn('Invalid player tried to play a card'); } }; /** * Allow a player to discard a number of cards once per turn * @param cards Array of card indexes to discard * @param player The player who discarded */ self.discard = function (cards, player) { if (self.state === STATES.PAUSED) { self.say('Game is currently paused'); return false; } console.log(player.nick + ' discarded ' + cards.join(', ')); cards = _.uniq(cards); if (self.state !== STATES.PLAYABLE || player.cards.numCards() === 0) { self.say(player.nick + ': Can\'t discard at the moment.'); } else if (typeof player !== 'undefined') { if (player.isCzar === true) { self.say(player.nick + ': You are the card czar. You cannot discard cards until you are a regular player.'); } else { if (player.hasDiscarded === true) { self.say(player.nick + ': You may only discard once per turn.'); } else if (player.points < 1) { self.say(player.nick + ': You must have at least one awesome point to discard.'); } else { var playerCards; if (cards.length === 0){ cards = []; for (var i = 0; i < player.cards.numCards(); i++) { cards[i] = i; } } try { playerCards = player.cards.pickCards(cards); } catch (error) { self.pm(player.nick, 'Invalid card index.'); return false; } self.deal(player, player.cards.numCards() + playerCards.numCards()); // Add the cards to the discard pile, and reduce points, and mark the player as having discarded _.each(playerCards.getCards(), function (card) { card.owner = null; self.discards.answer.addCard(card); playerCards.removeCard(card); }); player.hasDiscarded = true; player.points--; self.pm(player.nick, 'You have discarded, and have ' + player.points + ' ' + inflection.inflect('points', player.points) + ' remaining'); self.showCards(player); } } } else { console.warn('Invalid player tried to discard cards') } } /** * Check the time that has elapsed since the beinning of the turn. * End the turn is time limit is up */ self.turnTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 60 * 1000 * config.gameOptions.roundMinutes; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Round elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('The round timed out'); self.say('Time is up!'); self.markInactivePlayers(); // show end of turn self.showEntries(); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say('10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say('30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say('Hurry up, 1 minute left!'); self.showStatus(); } }; /** * Show the entries */ self.showEntries = function () { // clear round timer clearInterval(self.turnTimer); self.state = STATES.PLAYED; // Check if 2 or more entries... if (self.table.answer.length === 0) { self.say('No one played on this round.'); // skip directly to next round self.clean(); self.nextRound(); } else if (self.table.answer.length === 1) { self.say('Only one player played and is the winner by default.'); self.selectWinner(0); } else { self.say('Everyone has played. Here are the entries:'); // shuffle the entries self.table.answer = _.shuffle(self.table.answer); _.each(self.table.answer, function (cards, i) { self.say(i + ": " + self.getFullEntry(self.table.question, cards.getCards())); }, this); // check that czar still exists var currentCzar = _.findWhere(this.players, {isCzar: true, isActive: true}); if (typeof currentCzar === 'undefined') { // no czar, random winner (TODO: Voting?) self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else { self.say(self.czar.nick + ': Select the winner (!winner <entry number>)'); // start turn timer, check every 10 secs clearInterval(self.winnerTimer); self.roundStarted = new Date(); self.winnerTimer = setInterval(self.winnerTimerCheck, 10 * 1000); } } }; /** * Check the time that has elapsed since the beinning of the winner select. * End the turn is time limit is up */ self.winnerTimerCheck = function () { // check the time var now = new Date(); var timeLimit = 60 * 1000 * config.gameOptions.roundMinutes; var roundElapsed = (now.getTime() - self.roundStarted.getTime()); console.log('Winner selection elapsed:', roundElapsed, now.getTime(), self.roundStarted.getTime()); if (roundElapsed >= timeLimit) { console.log('the czar is inactive, selecting winner'); self.say('Time is up. I will pick the winner on this round.'); // Check czar & remove player after 3 timeouts self.czar.inactiveRounds++; // select winner self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } else if (roundElapsed >= timeLimit - (10 * 1000) && roundElapsed < timeLimit) { // 10s ... 0s left self.say(self.czar.nick + ': 10 seconds left!'); } else if (roundElapsed >= timeLimit - (30 * 1000) && roundElapsed < timeLimit - (20 * 1000)) { // 30s ... 20s left self.say(self.czar.nick + ': 30 seconds left!'); } else if (roundElapsed >= timeLimit - (60 * 1000) && roundElapsed < timeLimit - (50 * 1000)) { // 60s ... 50s left self.say(self.czar.nick + ': Hurry up, 1 minute left!'); } }; /** * Pick an entry that wins the round * @param index Index of the winning card in table list * @param player Player who said the command (use null for internal calls, to ignore checking) */ self.selectWinner = function (index, player) { // don't allow if game is paused if (self.state === STATES.PAUSED) { self.say('Game is currently paused.'); return false; } // clear winner timer clearInterval(self.winnerTimer); var winner = self.table.answer[index]; if (self.state === STATES.PLAYED) { if (typeof player !== 'undefined' && player !== self.czar) { client.say(player.nick + ': You are not the card czar. Only the card czar can select the winner'); } else if (typeof winner === 'undefined') { self.say('Invalid winner'); } else { self.state = STATES.ROUND_END; var owner = winner.cards[0].owner; owner.points++; // announce winner self.say(c.bold('Winner is: ') + owner.nick + ' with "' + self.getFullEntry(self.table.question, winner.getCards()) + '" and gets one awesome point! ' + owner.nick + ' has ' + owner.points + ' awesome ' + inflection.inflect('point', owner.points) + '.'); var round = self.dbCurrentRound; self.dbModels.Player.findOne({where: {nick: owner.nick}}).then(function (dbPlayer) { round.update({winner_id: dbPlayer.id}); }); self.clean(); self.nextRound(); } } }; /** * Get formatted entry * @param question * @param answers * @returns {*|Object|ServerResponse} */ self.getFullEntry = function (question, answers) { var args = [question.value]; _.each(answers, function (card) { args.push(card.value); }, this); return util.format.apply(this, args); }; /** * Check if all active players played on the current round * @returns Boolean true if all players have played */ self.checkAllPlayed = function () { var allPlayed = false; if (self.getNotPlayed().length === 0) { allPlayed = true; } return allPlayed; }; /** * Check if decks are empty & reset with discards */ self.checkDecks = function () { // check answer deck if (self.decks.answer.numCards() === 0) { console.log('answer deck is empty. reset from discard.'); self.decks.answer.reset(self.discards.answer.reset()); self.decks.answer.shuffle(); } // check question deck if (self.decks.question.numCards() === 0) { console.log('question deck is empty. reset from discard.'); self.decks.question.reset(self.discards.question.reset()); self.decks.question.shuffle(); } }; /** * Add a player to the game * @param player Player object containing new player's data * @returns The new player or false if invalid player */ self.addPlayer = function (player) { if (typeof self.dbGame === 'undefined') { self.playersToAdd.push(player); } else if (typeof self.getPlayer({nick: player.nick, hostname: player.hostname, isActive: true}) === 'undefined' ) { // Returning players var oldPlayer = _.findWhere(self.players, {nick: player.nick, hostname: player.hostname, isActive: false}); if (typeof oldPlayer !== 'undefined') { if (oldPlayer.idleCount >= config.gameOptions.idleLimit) { self.say(player.nick + ': You have idled too much and have been banned from this game.'); return false; } oldPlayer.isActive = true; } else { self.players.push(player); } self.say(player.nick + ' has joined the game'); // check if waiting for players if (self.state === STATES.WAITING && _.where(self.players, { isActive: true }).length >= 3) { // enough players, start the game self.nextRound(); } self.updateOrCreateInstance( self.dbModels.Player, {where: {nick: player.nick}}, {nick: player.nick, last_game_id: self.dbGame.id}, {last_game_id: self.dbGame.id} ); return player; } return false; }; /** * Find player * @param search * @returns {*} */ self.getPlayer = function (search) { return _.findWhere(self.players, search); }; /** * Remove player from game * @param player * @param options Extra options * @returns The removed player or false if invalid player */ self.removePlayer = function (player, options) { options = _.extend({}, options); if (typeof player !== 'undefined' && player.isActive) { console.log('removing' + player.nick + ' from the game'); // get cards in hand var cards = player.cards.reset(); // remove player player.isActive = false; // put player's cards to discard _.each(cards, function (card) { console.log('Add card ', card.text, 'to discard'); self.discards.answer.addCard(card); }); if (options.silent !== true) { self.say(player.nick + ' has left the game'); } // check if remaining players have all player if (self.state === STATES.PLAYABLE && self.checkAllPlayed()) { self.showEntries(); } // check czar if (self.state === STATES.PLAYED && self.czar === player) { self.say('The czar has fled the scene. So I will pick the winner on this round.'); self.selectWinner(Math.round(Math.random() * (self.table.answer.length - 1))); } return player; } return false; }; /** * Get all player who have not played * @returns Array list of Players that have not played */ self.getNotPlayed = function () { return _.where(_.filter(self.players, function (player) { // check only players with cards (so players who joined in the middle of a round are ignored) return player.cards.numCards() > 0; }), {hasPlayed: false, isCzar: false, isActive: true}); }; /** * Check for inactive players * @param options */ self.markInactivePlayers = function (options) { _.each(self.getNotPlayed(), function (player) { player.inactiveRounds++; }, this); }; /** * Show players cards to player * @param player */ self.showCards = function (player) { if (typeof player !== 'undefined') { var cardsZeroToSix = 'Your cards are:'; var cardsSevenToTwelve = ''; _.each(player.cards.getCards(), function (card, index) { if (index < 7) { cardsZeroToSix += c.bold(' [' + index + '] ') + card.value; } else { cardsSevenToTwelve += c.bold('[' + index + '] ') + card.value + ' '; } }, this); self.pm(player.nick, cardsZeroToSix); self.pm(player.nick, cardsSevenToTwelve); } }; /** * Show points for all players */ self.showPoints = function () { var sortedPlayers = _.sortBy(self.players, function (player) { return -player.points; }); var output = ''; _.each(sortedPlayers, function (player) { output += player.nick + ' ' + player.points + ' awesome ' + inflection.inflect('point', player.points) + ', '; }); self.say('The most horrible people: ' + output.slice(0, -2)); }; /** * Show status */ self.showStatus = function () { var // amount of player needed to start the game timeLeft = config.gameOptions.secondsBeforeStart - Math.round((new Date().getTime() - self.startTime.getTime()) / 1000), activePlayers = _.filter(self.players, function (player) { return player.isActive; }), playersNeeded = Math.max(0, 3 - activePlayers.length), played = _.where(activePlayers, {isCzar: false, hasPlayed: true, isActive: true}), // players who have already played notPlayed = _.where(activePlayers, {isCzar: false, hasPlayed: false, isActive: true}); // players who have not played yet switch (self.state) { case STATES.PLAYABLE: self.say(c.bold('Status: ') + self.czar.nick + ' is the czar. Waiting for ' + inflection.inflect('players', _.pluck(notPlayed, 'nick').length) + ' to play: ' + _.pluck(notPlayed, 'nick').join(', ')); break; case STATES.PLAYED: self.say(c.bold('Status: ') + 'Waiting for ' + self.czar.nick + ' to select the winner.'); break; case STATES.ROUND_END: self.say(c.bold('Status: ') + 'Round has ended and next one is starting.'); break; case STATES.STARTED: self.say(c.bold('Status: ') + 'Game starts in ' + timeLeft + ' ' + inflection.inflect('seconds', timeLeft) + '. Need ' + playersNeeded + ' more ' + inflection.inflect('players', playersNeeded) + ' to start.'); break; case STATES.STOPPED: self.say(c.bold('Status: ') + 'Game has been stopped.'); break; case STATES.WAITING: self.say(c.bold('Status: ') + 'Not enough players to start. Need ' + playersNeeded + ' more ' + inflection.inflect('players', playersNeeded) + ' to start.'); break; case STATES.PAUSED: self.say(c.bold('Status: ') + 'Game is paused.'); break; } }; /** * Set the channel topic */ self.setTopic = function (topic) { // ignore if not configured to set topic if (typeof config.gameOptions.setTopic === 'undefined' || !config.gameOptions.setTopic) { return false; } // construct new topic var newTopic = topic; if (typeof config.gameOptions.topicBase !== 'undefined') { newTopic = topic + ' ' + config.gameOptions.topicBase; } // set it client.send('TOPIC', channel, newTopic); }; /** * List all players in the current game */ self.listPlayers = function () { var activePlayers = _.filter(self.players, function (player) { return player.isActive; }); if (activePlayers.length > 0) { self.say('Players currently in the game: ' + _.pluck(activePlayers, 'nick').join(', ')); } else { self.say('No players currently in the game'); } }; /** * Helper function for the handlers below */ self.findAndRemoveIfPlaying = function (nick) { var player = self.getPlayer({nick: nick}); if (typeof player !== 'undefined') { self.removePlayer(player); } }; /** * Handle player parts * @param channel * @param nick * @param reason * @param message */ self.playerPartHandler = function (channel, nick, reason, message) { console.log('Player ' + nick + ' left'); self.findAndRemoveIfPlaying(nick); }; /** * Handle player kicks * @param nick * @param by * @param reason * @param message */ self.playerKickHandler = function (nick, by, reason, message) { console.log('Player ' + nick + ' was kicked by ' + by); self.findAndRemoveIfPlaying(nick); }; /** * Handle player kicks * @param nick * @param reason * @param channel * @param message */ self.playerQuitHandler = function (nick, reason, channel, message) { console.log('Player ' + nick + ' left'); self.findAndRemoveIfPlaying(nick); }; /** * Handle player nick changes * @param oldnick * @param newnick * @param channels * @param message */ self.playerNickChangeHandler = function (oldnick, newnick, channels, message) { console.log('Player changed nick from ' + oldnick + ' to ' + newnick); var player = self.getPlayer({nick: oldnick}); if (typeof player !== 'undefined') { player.nick = newnick; } self.updateOrCreateInstance( self.dbModels.Player, {where: {nick: oldnick}}, null, {nick: newnick} ); }; /** * Notify users in channel that game has started */ self.notifyUsers = function() { // request names client.send('NAMES', channel); // signal handler to send notifications self.notifyUsersPending = true; }; /** * Handle names response to notify users * @param nicks */ self.notifyUsersHandler = function(nicks) { // ignore if we haven't requested this if (self.notifyUsersPending === false) { return false; } // don't message nicks with these modes var exemptModes = ['~', '&']; // loop through and send messages _.each(nicks, function(mode, nick) { if (_.indexOf(exemptModes, mode) < 0 && nick !== config.botOptions.nick) { self.notice(nick, nick + ': A new game of Cards Against Humanity just began in ' + channel + '. Head over and !join if you\'d like to get in on the fun!'); } }); // reset self.notifyUsersPending = false; }; /** * Public message to the game channel * @param string */ self.say = function (string) { self.client.say(self.channel, string); }; self.pm = function (nick, string) { self.client.say(nick, string); }; self.notice = function (nick, string) { self.client.notice(nick, string); }; // set topic self.setTopic(c.bold.lime('A game is running. Type !join to get in on it!')); // announce the game on the channel self.say('A new game of ' + c.rainbow('Cards Against Humanity') + '. The game starts in ' + config.gameOptions.secondsBeforeStart + ' ' + inflection.inflect('seconds', config.gameOptions.secondsBeforeStart) + '. Type !join to join the game any time.'); // notify users if (typeof config.gameOptions.notifyUsers !== 'undefined' && config.gameOptions.notifyUsers) { self.notifyUsers(); } // wait for players to join self.startTime = new Date(); self.startTimeout = setTimeout(self.nextRound, config.gameOptions.secondsBeforeStart * 1000); // client listeners client.addListener('part', self.playerPartHandler); client.addListener('quit', self.playerQuitHandler); client.addListener('kick'+channel, self.playerKickHandler); client.addListener('nick', self.playerNickChangeHandler); client.addListener('names'+channel, self.notifyUsersHandler); };