Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions README → README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@

quizbot
=======

--------------------------------
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
============
Expand All @@ -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 <alexander@plaimi.net>,
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
------------------
Expand All @@ -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
Expand Down Expand Up @@ -106,5 +134,4 @@ Author

Alexander Berntsen <alexander@plaimi.net>


/* vim: set textwidth=78 formatoptions=actw2 autoindent: */
12 changes: 11 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
128 changes: 79 additions & 49 deletions q
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2012, 2013 Alexander Berntsen <alexander@plaimi.net>
# Copyright (C) 2012, 2013 Stian Ellingsen <stian@plaimi.net>
Expand Down Expand Up @@ -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


Expand All @@ -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)

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -239,36 +269,36 @@ 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):
"""Message help message to the user."""
# 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."""
Expand All @@ -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."""
Expand All @@ -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

Expand All @@ -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):
Expand All @@ -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."""
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions questions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
questions = [
['Category', 'Question 1?', 'Answer'],
['Category', 'Question 2?', 'Answer'],
Expand Down
Loading