diff --git a/README b/README.md similarity index 74% rename from README rename to README.md index c03571f..bf33e07 100644 --- a/README +++ b/README.md @@ -1,7 +1,7 @@ + quizbot ======= --------------------------------- Small and simple quizbot for IRC -------------------------------- @@ -9,15 +9,41 @@ Small and simple quizbot for IRC Use === -Hack config.py to your liking (it's pretty self-explanatory). Write questions -in questions.py, as per the format. Note: you need at least eleven question -for quizbot to work properly. +Hack [config.py](config.py) to your liking (it's pretty self-explanatory). + +Write questions in [questions.py](questions.py), as per the format. +*Note*: you need at least eleven question for quizbot to work properly. + +Edit or change common messages by redefining the according variables in [strings.py](strings.py). Start the bot with: - $ ./q -or: - $ python q +`$ ./q` +or +`$ python q` + +Start and keep the bot running in a background task: +`$ nohup ./q &` +or +`$ python ./q &` + +IRC +---- +Join the same channel where quizbot is connected to. + +*Note*: channel users with nick names as defined in the config under `masters=[]` can control the bot, e.g. force a reload of the questions. + +`!help` : lists available commands + +Masters commands: +`!op` : OP a master. +`!deop` : DEOP a master +`!reload` : Reload the question/answer list. + +General commands: +`!score` : Print the top five quizzers +`!hiscore` : Print the top five quizzers of all time +`!botsnack` : Feed quizbot to keep it going Dependencies ============ @@ -27,9 +53,11 @@ below are listed with the oldest versions that are confirmed to work. Older versions *might* work. If they do, please report it to , so that he can update this file. --python 2.7.x --twisted >= 11.0.0 --twisted-words >= 11.0.0 +- python 2.7.x +- twisted >= 11.0.0 +`$ apt-get install python-twisted` +- twisted-words >= 11.0.0 +`$ apt-get install python-twisted-words` A Note on Python 3 ------------------ @@ -38,7 +66,7 @@ The 3.x interpreter will try to run this and fail. You *need* to use a 2.x interpreter (2.7.x is the only one with which quizbot is formally tested). This may be accomplished by specifically invoking a 2.x interpreter on some systems. - $ python2 q +`$ python2 q` quizbot in #quiznode on Freenode @@ -106,5 +134,4 @@ Author Alexander Berntsen - /* vim: set textwidth=78 formatoptions=actw2 autoindent: */ diff --git a/config.py b/config.py index ddb3894..5047c1f 100644 --- a/config.py +++ b/config.py @@ -12,7 +12,17 @@ password = False # IRC nick names that can control the bot masters = [nickname, 'my_nickname'] +# How quickly the bot will get hungry (by asking questions or giving hints) +stamina = 6 +# Patience the bot has until it gives hints (in seconds, minimum 5) +hintpatience = 10 +# Minimum number of players required to start asking questions. +minplayers = 2 +# Threshold representing % of total questions to keep as "recently asked questions" +qrecyclethreshold = 20 # High score database file (is automatically created) -hiscoresdb = 'hiscores.sql' +hiscoresdb = 'hiscores.sqlite' +# Keep a player's scores when IRC nick changes. +keepscore = True # Whether to print 'category - question - answer' to STDOUT verbose = True diff --git a/q b/q index 8e0b7ab..f11583f 100644 --- a/q +++ b/q @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # Copyright (C) 2012, 2013 Alexander Berntsen # Copyright (C) 2012, 2013 Stian Ellingsen @@ -30,6 +31,7 @@ from time import time from twisted.words.protocols import irc from twisted.internet import protocol, reactor +import strings import questions as q @@ -49,17 +51,26 @@ class Bot(irc.IRCClient): self.password = self.factory.password self.username = self.factory.username self.quizzers = {} + self.minplayernum = 2 if config.minplayers is None else config.minplayers + self.hint_patience = 6 if config.hintpatience is None else config.hintpatience self.last_decide = 10 self.answered = 5 self.winner = '' self.question = '' + if config.qrecyclethreshold is None: + self.recently_asked_threshold = ((20*len(q.questions))/100.0) + else: + self.recently_asked_threshold = ((config.qrecyclethreshold*len(q.questions))/100.0) self.recently_asked = [] - self.db = sqlite3.connect(config.hiscoresdb) + self.db = sqlite3.connect(config.hiscoresdb, isolation_level=None) self.dbcur = self.db.cursor() - self.dbcur.execute('CREATE TABLE IF NOT EXISTS hiscore (quizzer TEXT' - ' unique, wins INTEGER)') + try: + self.dbcur.execute('CREATE TABLE IF NOT EXISTS hiscore (quizzer TEXT unique, wins INTEGER)') + except self.db.IntegrityError as e: + print('sqlite error: ', e.args[0]) self.db.commit() self.hunger = 0 + self.stamina = 6 if config.stamina is None else config.stamina self.complained = False irc.IRCClient.connectionMade(self) @@ -95,6 +106,15 @@ class Bot(irc.IRCClient): """Overrides USERRENAMED.""" self.del_quizzer(oldname) self.add_quizzer(newname) + # Change quizzer name in DB to keep score. + if config.keepscore: + self.dbcur.execute('SELECT * FROM hiscore WHERE quizzer=?', + (oldname,)) + row = self.dbcur.fetchone() + if row is not None: + self.dbcur.execute('UPDATE hiscore SET quizzer=? WHERE quizzer=?', + (newname, oldname)) + self.db.commit() def irc_RPL_NAMREPLY(self, prefix, params): """Overrides RPL_NAMEREPLY.""" @@ -135,39 +155,49 @@ class Bot(irc.IRCClient): # Unknown command. elif msg[0] == '!': self.msg(self.factory.channel if channel != self.nickname else - name, '... wat.') + name, strings.unknowncmd) def decide(self): - """Figure out whether to post a question or a hint.""" - t = time() - f, dt = ((self.ask, self.answered + 5 - t) if self.answered else - (self.hint, self.last_decide + 10 - t)) - if dt < 0.5: - f() - self.last_decide = t - dt = 5 - reactor.callLater(min(5, dt), self.decide) + """Wait for enough players.""" + numPlayers = len(self.quizzers) + if numPlayers < self.minplayernum: + self.msg(self.factory.channel, strings.waiting) + reactor.callLater(30, self.decide) + return + else: + numPlayers += 1 + if numPlayers >= self.minplayernum: + """Figure out whether to post a question or a hint.""" + t = time() + f, dt = ((self.ask, self.answered + 5 - t) if self.answered else + (self.hint, self.last_decide + self.hint_patience - t)) + if dt < 0.5: + f() + self.last_decide = t + dt = 5 + reactor.callLater(min(5, dt), self.decide) def ask(self): - """Ask a question.""" + """Make bot hungy.""" self.hunger += 1 - if self.hunger > 6: + if self.hunger > self.stamina: if not self.complained: self.msg(self.factory.channel, - "I'm hungry. Please feed me with !botsnack.") + strings.botsnack) self.complained = True return + """Ask a question.""" # Make sure there have been ten questions in between this question. while self.question in self.recently_asked or not self.question: cqa = choice(q.questions) self.question = cqa[1] self.category = cqa[0] - # This num should be changed depending on how many questions you have. - if len(self.recently_asked) >= 10: + # Clear recently asked questions when threshold is reached + if len(self.recently_asked) >= self.recently_asked_threshold: self.recently_asked.pop(0) self.recently_asked.append(self.question) self.answer = cqa[2] - self.msg(self.factory.channel, 'TOPIC: %s - Q: %s' % + self.msg(self.factory.channel, strings.question % (self.category, self.question)) if config.verbose: print '%s - %s - %s' % (self.category, self.question, self.answer) @@ -191,7 +221,7 @@ class Bot(irc.IRCClient): # Max 5 hints, and don't give hints when the answer is so short. if len(str(self.answer)) <= self.hint_num + 1 or self.hint_num >= 5: if (len(str(self.answer)) == 1 and self.hint_num == 0): - self.msg(self.factory.channel, 'HINT: only one character!') + self.msg(self.factory.channel, strings.hintone) self.hint_num += 1 else: self.fail() @@ -207,19 +237,19 @@ class Bot(irc.IRCClient): self.answer_hint = ''.join( '*' if idx in self.answer_masks and c is not ' ' else c for idx, c in enumerate(str(self.answer))) - self.msg(self.factory.channel, 'HINT: %s' % self.answer_hint) + self.msg(self.factory.channel, strings.hint % self.answer_hint) self.hint_num += 1 def fail(self): """Timeout/giveup on answer.""" - self.msg(self.factory.channel, 'the answer was: "%s"' % self.answer) - self.msg(self.factory.channel, 'better luck with the next question!') + self.msg(self.factory.channel, strings.rightanswer % self.answer) + self.msg(self.factory.channel, strings.wishluck) self.answered = time() def award(self, awardee): """Gives a point to awardee.""" self.quizzers[awardee] += 1 - self.msg(self.factory.channel, '%s is right! congratulations, %s!' % + self.msg(self.factory.channel, strings.correctanswer % (self.answer, awardee)) if self.quizzers[awardee] == self.target_score: self.win(awardee) @@ -239,20 +269,23 @@ class Bot(irc.IRCClient): if numAnswerers > 1: winner = quizzersByPoints[0][0] self.dbcur.execute('SELECT * FROM hiscore WHERE quizzer=?', - (winner,)) + (winner,)) wins = 1 row = self.dbcur.fetchone() if row is not None: wins = row[1] + 1 - sql = 'UPDATE hiscore SET wins = ? WHERE quizzer = ?' + sql = 'UPDATE hiscore SET wins=? WHERE quizzer=?' else: - sql = 'INSERT INTO hiscore (wins,quizzer) VALUES (?,?)' - self.dbcur.execute(sql, (wins, winner)) + sql = 'INSERT INTO hiscore (wins, quizzer) VALUES (?, ?)' + try: + self.dbcur.execute(sql, (wins, winner)) + except self.db.IntegrityError as e: + print('sqlite error: ', e.args[0]) self.db.commit() self.winner = winner self.msg(self.factory.channel, - 'congratulations to %s, you\'re winner!!!' % self.winner) + strings.winner % self.winner) self.reset() def help(self, user): @@ -260,15 +293,12 @@ class Bot(irc.IRCClient): # Prevent spamming to non-quizzers, AKA random Freenode users. if user not in self.quizzers: return - self.msg(user, self.factory.channel + ' is a quiz channel.') - self.msg(user, 'I am ' + self.nickname + ', and *I* ask the' + - ' questions around here! :->') - self.msg(user, '!score prints the current top 5 quizzers.') - self.msg(user, '!hiscore prints the all time top 5 quizzers.') - self.msg(user, 'happy quizzing!') - self.msg(user, '(o, and BTW, I\'m hungry, like *all* the freaking' + - ' time.') - self.msg(user, 'you can feed me with !botsnack. please do. often.)') + + self.msg(user, strings.help_channelinfo % self.factory.channel) + self.msg(user, strings.help_botinfo % self.nickname) + + for msgline in strings.help: + self.msg(user, msgline) def reload_questions(self, user): """Reload the question/answer list.""" @@ -280,7 +310,7 @@ class Bot(irc.IRCClient): """Feed quizbot.""" self.hunger = 0 self.complained = False - self.msg(self.factory.channel, 'ta. :-)') + self.msg(self.factory.channel, strings.thanks) def op(self, user): """OP a master.""" @@ -301,7 +331,7 @@ class Bot(irc.IRCClient): if points: if points != prev_points: j = i - self.msg(self.factory.channel, '%d. %s: %d points' % + self.msg(self.factory.channel, strings.score % (j, quizzer, points)) prev_points = points @@ -310,7 +340,7 @@ class Bot(irc.IRCClient): self.dbcur.execute('SELECT * FROM hiscore ORDER by wins DESC LIMIT 5') hiscore = self.dbcur.fetchall() for i, (quizzer, wins) in enumerate(hiscore): - self.msg(self.factory.channel, '%d. %s: %d points' % + self.msg(self.factory.channel, strings.score % (i + 1, quizzer.encode('UTF-8'), wins)) def set_topic(self): @@ -319,11 +349,9 @@ class Bot(irc.IRCClient): if alltime is None: alltime = ["no one", 0] self.topic( - self.factory.channel, - 'happy quizzing. :-> target score: %d. previous winner: %s. ' - 'all-time winner: %s (%d).' % - (self.target_score, self.winner, alltime[0].encode('UTF-8'), - alltime[1])) + self.factory.channel, strings.channeltopic % + (self.target_score, self.winner, alltime[0].encode('UTF-8'), + alltime[1])) def reset(self): """Set all quizzers' points to 0 and change topic.""" @@ -356,8 +384,8 @@ class Bot(irc.IRCClient): return True if role == self.quizzers: return False - self.msg(self.factory.channel, 'not on my watch, %s!' % name) - self.kick(self.factory.channel, name, 'lol.') + self.msg(self.factory.channel, strings.invalidname % name) + self.kick(self.factory.channel, name, strings.kickmsg) self.del_quizzer(name) return False @@ -379,7 +407,9 @@ class BotFactory(protocol.ClientFactory): self.channel = channel self.nickname = config.nickname self.username = config.username - if config.password: + if config.password and isinstance(config.password, str): + self.password = config.password + elif config.password and config.password is True: self.password = getpass('enter password (will not be echoed): ') self.masters = config.masters diff --git a/questions.py b/questions.py index c51e2f7..d0c94ab 100644 --- a/questions.py +++ b/questions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- questions = [ ['Category', 'Question 1?', 'Answer'], ['Category', 'Question 2?', 'Answer'], diff --git a/strings.py b/strings.py new file mode 100644 index 0000000..be74d02 --- /dev/null +++ b/strings.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# Default message strings - DON'T change these! +question = 'TOPIC: %s - Q: %s' +waiting = "Playing alone is no fun. Let's wait for others..." +botsnack = "I'm hungry. Please feed me with !botsnack." +hint = 'HINT: %s' +hintone = 'HINT: only one character!' +rightanswer = 'the answer was: "%s"' +wishluck = 'better luck with the next question!' +correctanswer = '%s is right! congratulations, %s!' +winner = 'congratulations to %s, you\'re winner!!!' +thanks = 'ta. :-)' +score = '%d. %s: %d points' +channeltopic = 'happy quizzing. :-> target score: %d. previous winner: %s. all-time winner: %s (%d).' +invalidname = 'not on my watch, %s!' +kickmsg = 'lol.' +help_channelinfo = '%s is a quiz channel.' +help_botinfo = 'I am %s, and *I* ask the questions around here! :->' +help = [ + '!score prints the current top 5 quizzers.', + '!hiscore prints the all time top 5 quizzers.', + 'happy quizzing!', + '(o, and BTW, I\'m hungry, like *all* the freaking time.', + 'you can feed me with !botsnack. please do. often.)' +] +unknowncmd = '... wat.' + +# Message strings - customize these as you desire +question = 'TOPIC: %s - Q: %s' +botsnack = "I'm hungry. Please feed me with !botsnack." +#...