### # Copyright (c) 2011, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import re import time import operator import threading import supybot.log as log import supybot.conf as conf import supybot.ircdb as ircdb import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.schedule as schedule import supybot.ircutils as ircutils import supybot.callbacks as callbacks from supybot.i18n import PluginInternationalization, internationalizeDocstring _ = PluginInternationalization('Eureka') STATE_STOPPED = 1 STATE_STARTED = 2 STATE_PAUSED = 3 class State: def __init__(self, filename): self.state = STATE_STOPPED self.scores = {} self.issue = 0 filename = os.path.abspath(filename) self.fd = open(filename) self._waitingForAnswer = threading.Event() _matchQuestion = re.compile('(?P[0-9]+) (?P.*)') _matchClue = re.compile('(?P[0-9]+) (?P.*)') _matchAnswer = re.compile('(?P[a-z]) (?P.*)') def loadBlock(self): self._waitingForAnswer.clear() self._waitingForAnswer = threading.Event() self.question = None self.answers = [] if self.issue is None: # Previous question didn't expire for line in self.fd: line = line.strip() if line.startswith('=== '): break self.issue = None for line in self.fd: line = line.strip() if line == '---': break elif line != '': match = self._matchQuestion.match(line) if match is None: log.error('Bad question format for question %r: %r' % (self.question, line)) continue (value, question) = match.group('value', 'question') # We are sure that value is an integer, thanks to the regexp self.question = (int(value), question) self._waitingForAnswer.set() if self.question == '': self.state = STATE_STOPPED return for line in self.fd: line = line.strip() if line == '---': break elif line != '': match = self._matchAnswer.match(line) if match is None: log.error('Bad answer format for question %r: %r' % (self.question, line)) continue (mode, answer) = match.group('mode', 'answer') if mode == 'r': pass elif mode == 'm': answer = re.compile(answer) else: log.error('Unsupported mode: %r. Only \'r\' (raw)'% mode + 'is supported for the moment.') continue self.answers.append((mode, answer)) def getClue(self): for line in self.fd: line = line.strip() if line.startswith('=== '): try: self.issue = int(line[4:]) except ValueError: log.error('Bad end of block for question %r: %r' % (self.question, line)) return (self.issue, None, None) # No more clue elif line != '': match = self._matchClue.match(line) if match is None: log.error('Bad clue format for question %r: %r' % (self.question, line)) continue (delay, clue) = match.group('delay', 'clue') # We are sure that delay is an integer, thanks to the # regexp return (int(delay), clue, self._waitingForAnswer) def adjust(self, nick, count): assert isinstance(count, int) if nick not in self.scores: self.scores[nick] = count else: self.scores[nick] += count @internationalizeDocstring class Eureka(callbacks.Plugin): """Add the help for "@plugin help Eureka" here This should describe *how* to use this plugin.""" states = {} def _ask(self, irc, channel, now=False): assert channel in self.states, \ 'Asked to ask on a channel where Eureka is not enabled.' state = self.states[channel] def event(): state.loadBlock() if state.question is None: state.state = STATE_STOPPED return irc.reply(state.question[1], prefixNick=False) self._giveClue(irc, channel) if now: event() else: schedule.addEvent(event, time.time() + state.issue, 'Eureka-ask-%s' % channel) def _giveClue(self, irc, channel, now=False): state = self.states[channel] (delay, clue, valid) = state.getClue() def event(): try: schedule.removeEvent('Eureka-nextClue-%s' % channel) except KeyError: pass if clue is None: assert valid is None irc.reply(_('Nobody replied with (one of this) ' 'answer(s): %r.') % ', '.join([y for x,y in state.answers if x == 'r']), prefixNick=False) self._ask(irc, channel) else: irc.reply(_('Another clue: %s') % clue, prefixNick=False) self._giveClue(irc, channel) eventName = 'Eureka-nextClue-%s' % channel if now and eventName in schedule.schedule.events: schedule.schedule.events[eventName]() schedule.removeEvent(eventName) schedule.addEvent(event, time.time() + delay, eventName) def doPrivmsg(self, irc, msg): channel = msg.args[0] nick = msg.prefix.split('!')[0] if channel not in self.states: return reply = None state = self.states[channel] for mode, answer in state.answers: if mode == 'r': if msg.args[1].lower() == answer.lower(): state.adjust(nick, state.question[0]) reply = _('Congratulations %s! The answer was %r.') reply %= (nick, answer) elif mode == 'm': if answer.match(msg.args[1]): state.adjust(nick, state.question[0]) reply = _('Congratulations %s! The answer was %r.') reply %= (nick, msg.args[1]) if reply is not None: schedule.removeEvent('Eureka-nextClue-%s' % channel) otherAnswers = [y for x,y in state.answers if x == 'r' and y.lower() != msg.args[1].lower()] if len(otherAnswers) == 1: reply += ' ' + _('Another valid answer is: \'%s\'.') reply %= otherAnswers[0] elif len(otherAnswers) >= 2: reply += ' ' + _('Other valid answers are: \'%s\'.') reply %= '\', \''.join([x for x in otherAnswers]) irc.reply(reply, prefixNick=False) self._ask(irc, channel, True) @internationalizeDocstring def scores(self, irc, msg, args, channel): """[] Return the scores on the . If is not given, it defaults to the current channel.""" if channel not in self.states: irc.error(_('Eureka is not enabled on this channel')) return scores = list(self.states[channel].scores.items()) if scores == []: irc.reply(_('Noone played yet.')) else: scores.sort(key=operator.itemgetter(1)) scores.reverse() irc.reply(', '.join(['%s(%i)' % x for x in scores])) scores = wrap(scores, ['channel']) @internationalizeDocstring def score(self, irc, msg, args, channel, nick): """[] Return the score of on the . If is not given, it defaults to the current channel.""" if channel not in self.states: irc.error(_('Eureka is not enabled on this channel')) return state = self.states[channel] if nick not in state.scores: irc.error(_('This user did not play yet.')) return irc.reply(str(state.scores[nick])) score = wrap(score, ['channel', 'nick']) @internationalizeDocstring def start(self, irc, msg, args, channel): """[] Start the Eureka on the given . If is not given, it defaults to the current channel.""" if channel in self.states and \ self.states[channel].state != STATE_STOPPED: irc.error(_('Eureka is already enabled on this channel')) return state = State(os.path.join(conf.supybot.directories.data(), 'Eureka.%s.questions' % channel)) state.state = STATE_STARTED self.states[channel] = state self._ask(irc, channel, True) start = wrap(start, ['op']) @internationalizeDocstring def stop(self, irc, msg, args, channel): """[] Stop the Eureka on the given . If is not given, it defaults to the current channel.""" if channel not in self.states or \ self.states[channel].state == STATE_STOPPED: irc.error(_('Eureka is not enabled on this channel')) return self.states[channel].state = STATE_STOPPED schedule.removeEvent('Eureka-nextClue-%s' % channel) irc.replySuccess() stop = wrap(stop, ['op']) @internationalizeDocstring def pause(self, irc, msg, args, channel): """[] Pause the Eureka on the given . If is not given, it defaults to the current channel.""" if channel not in self.states or \ self.states[channel].state == STATE_STOPPED: irc.error(_('Eureka is not enabled on this channel')) return state = self.states[channel] if state.state == STATE_PAUSED: irc.error(_('Eureka is already paused.')) return state.state = STATE_PAUSED schedule.removeEvent('Eureka-nextClue-%s' % channel) irc.replySuccess() pause = wrap(pause, ['op']) @internationalizeDocstring def resume(self, irc, msg, args, channel): """[] Resume the Eureka on the given . If is not given, it defaults to the current channel.""" if channel not in self.states or \ self.states[channel].state == STATE_STOPPED: irc.error(_('Eureka is not enabled on this channel')) return state = self.states[channel] if state.state != STATE_PAUSED: irc.error(_('Eureka is not paused.')) return state.state = STATE_STARTED self._giveClue(irc, channel, True) resume = wrap(resume, ['op']) @internationalizeDocstring def adjust(self, irc, msg, args, channel, nick, count): """[] Increase or decrease the score of on the . If is not given, it defaults to the current channel.""" self.states[channel].adjust(nick, count) irc.replySuccess() adjust = wrap(adjust, ['op', 'nick', 'int']) @internationalizeDocstring def skip(self, irc, msg, args, channel): """[] Give up with this question, and switch to the next one.""" if channel not in self.states or \ self.states[channel].state == STATE_STOPPED: irc.error(_('Eureka is not enabled on this channel')) return try: schedule.removeEvent('Eureka-nextClue-%s' % channel) except KeyError: pass self._ask(irc, channel, True) skip = wrap(skip, ['op']) @internationalizeDocstring def clue(self, irc, msg, args, channel): """[] Give the next clue.""" self._giveClue(irc, channel, True) clue = wrap(clue, ['op']) Class = Eureka # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: