Supybot-plugins/Eureka/plugin.py

368 lines
14 KiB
Python

###
# 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<value>[0-9]+) (?P<question>.*)')
_matchClue = re.compile('(?P<delay>[0-9]+) (?P<clue>.*)')
_matchAnswer = re.compile('(?P<mode>[a-z]) (?P<answer>.*)')
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):
"""[<channel>]
Return the scores on the <channel>. If <channel> 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):
"""[<channel>] <nick>
Return the score of <nick> on the <channel>. If <channel> 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):
"""[<channel>]
Start the Eureka on the given <channel>. If <channel> 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):
"""[<channel>]
Stop the Eureka on the given <channel>. If <channel> 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):
"""[<channel>]
Pause the Eureka on the given <channel>. If <channel> 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):
"""[<channel>]
Resume the Eureka on the given <channel>. If <channel> 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):
"""[<channel>] <nick> <number>
Increase or decrease the score of <nick> on the <channel>.
If <channel> 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):
"""[<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):
"""[<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: