Rewrite to support basic quiz gameplay.

This commit is contained in:
Jonathan Harker 2015-11-19 23:38:54 +13:00
parent bbb90264b4
commit 52fd6a5b37
2 changed files with 345 additions and 352 deletions

558
lolbot.py Executable file → Normal file
View file

@ -1,390 +1,264 @@
#!/usr/bin/env python #! /usr/bin/env python
#
# LolBot
#
# New version based on Twisted IRC.
""" """
Useful bot for folks stuck behind censor walls at work LOLBOT 2
Logs a channel and collects URLs for later.
- die: Let the bot cease to exist.
- ask: Ask a MoxQuizz question.
- list: list some URLs
""" """
from __future__ import print_function # unicode_literals from __future__ import print_function, unicode_literals
try: import sqlite3
import sys import random
import os import time
import string import irc.strings
import random from irc.bot import SingleServerIRCBot
import time from sqlalchemy import create_engine
import getopt from sqlalchemy.orm import sessionmaker
import sqlite3 from models import Log, Url, Model
from twisted.words.protocols import irc from pymoxquizz import QuestionBank, Question
from twisted.internet import protocol from os import listdir, path
from twisted.internet import reactor
from datetime import datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pymoxquizz import QuestionBank, Question
from models import Url, Log, Model
except ImportError:
print("Some modules missing: Lolbot relies on Twisted IRC, Mechanize and SQLAlchemy.\n")
sys.exit
# Exclamations - wrong input
exclamations = [
"Zing!",
"Burns!",
"Tard!",
"Lol.",
"Crazy!",
"WTF?",
]
# Ponderings
ponderings = [
"Hi, can I have a medium lamb roast, with just potatoes.",
"Can I slurp on your Big Cock?",
"Quentin Tarantino is so awesome I want to have his babies.",
"No it's a week night 8pm is past my bedtime.",
]
class LolBot(irc.IRCClient): DEBUG = True
def debug(msg):
if DEBUG:
print(msg)
class LolBot(SingleServerIRCBot):
""" """
The Lolbot itself. An IRC bot to entertain the troops with MoxQuizz questions, log URLs, and
other shenanigans.
""" """
def _get_connection(self): qb = list()
connection = sqlite3.Connection(self.config['db.file'])
connection.text_factory = str def __init__(self, channel, nickname, server, database, port=6667):
return connection debug("Instantiating SingleServerIRCBot")
SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
self.channel = channel
def created(self, when):
# load some MoxQuizz questions # load some MoxQuizz questions
self.qb = QuestionBank('/home/johnno/questions.doctorlard.en').questions qfiles = [f for f in listdir('questions') if path.isfile(path.join('questions', f))]
debug("Loading MoxQuizz questions")
for f in qfiles:
qfile = path.abspath(path.join('questions', f))
debug(" - from MoxQuizz bank '%s'" % qfile)
self.qb += QuestionBank(qfile).questions
random.shuffle(self.qb) random.shuffle(self.qb)
self.quiz = 0
self.question = None self.question = None
# connect to the database # connect to the database
debug("Connecting to SQLite database '%s'" % database)
self.dbfile = database
self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection) self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection)
Model.metadata.bind = self.dbengine Model.metadata.bind = self.dbengine
Model.metadata.create_all() Model.metadata.create_all()
self.get_session = sessionmaker(bind=self.dbengine) self.db_session = sessionmaker(bind=self.dbengine)
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message." self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); <url> - adds the URL to the list; help - this message."
debug("Exiting lolbot constructor")
def now(self): def _get_connection(self):
return datetime.today().strftime("%Y-%m-%d %H:%M:%S") """Creator function for SQLAlchemy."""
connection = sqlite3.Connection(self.dbfile)
connection.text_factory = str
debug("Creating SQLAlchemy connection")
return connection
def save_url(self, nickname, url): def on_nicknameinuse(self, connection, event):
title = False nick = connection.get_nickname()
try: debug("Nick '%s' in use, trying '%s_'" % nick)
db = self.get_session() connection.nick(nick + "_")
if not db.query(Url).filter(Url.url == url).count():
theurl = Url(nickname, url)
db.add(theurl)
db.commit()
else:
theurl = db.query(Url).filter(Url.url == url).one()
print(theurl)
title = theurl.title
except Exception as ex:
print("Exception caught saving URL: %s" % ex)
return title
def log_event(self, nick, text): def on_welcome(self, connection, event):
try: debug("Joining channel '%s'" % self.channel)
entry = Log(nick, text) connection.join(self.channel)
db = self.get_session()
db.add(entry)
db.commit()
print(entry)
except Exception as ex:
print("Exception caught logging event: %s" % ex)
def _get_nickname(self): def on_privmsg(self, connection, event):
return self.factory.nickname self.do_command(event, event.arguments[0])
nickname = property(_get_nickname)
def _get_channel(self): def on_pubmsg(self, connection, event):
return self.factory.channel
channel = property(_get_channel)
def _get_config(self):
return self.factory.config
config = property(_get_config)
def signedOn(self):
self.join(self.channel)
print("Signed on as %s." % (self.nickname,))
def joined(self, channel):
print("Joined %s." % (channel,))
def privmsg(self, user, channel, msg):
user = user.split('!')[0]
if channel == self.nickname:
# Private /msg from a user
self.do_command(msg, user)
else:
# log it
self.log_event(user, msg)
# deal with MoxQuizz question
answered = False
if isinstance(self.question, Question) and self.question.answer.lower() in msg.lower():
answered = True
print("%s +1" % user)
self.reply('Correct! %s scores one point.' % user)
if answered:
self.question = None
args = string.split(msg, ":", 1)
if len(args) > 1 and args[0] == self.nickname:
self.do_command(string.strip(args[1]))
else:
# parse it for links, add URLs to the list
words = msg.split(" ")
for w in words:
if w.startswith('http://') or w.startswith('https://'):
title = self.save_url(user, w)
if title is False:
self.say_public("Sorry, I'm useless at UTF-8.")
else:
self.say_public("URL added. %s" % title)
def say_public(self, text):
"Print TEXT into public channel, for all to see."
self.notice(self.channel, text)
self.log_event(self.nickname, text)
def say_private(self, nick, text):
"Send private message of TEXT to NICK."
self.notice(nick, text)
def reply(self, text, to_private=None):
"Send TEXT to either public channel or TO_PRIVATE nick (if defined)."
if to_private is not None:
self.say_private(to_private, text)
else:
self.say_public(text)
def ponder(self):
"Return a random pondering."
return random.choice(ponderings)
def exclaim(self):
"Return a random exclamation string."
return random.choice(exclamations)
def do_command(self, cmd, target=None):
""" """
This is the function called whenever someone sends a public or Handle an event on the channel.
private message addressed to the bot. (e.g. "bot: blah"). Handle commands addressed to the bot.
If there's a question, see if it's been answered.
""" """
try: try:
if cmd == 'help': (nick, message) = event.arguments[0].split(":", 1)
self.reply(self.helptext, target) # handle command, if addressed
if irc.strings.lower(nick) == irc.strings.lower(self.connection.get_nickname()):
self.do_command(event, message.strip())
except ValueError:
message = event.arguments[0]
nick = event.source.nick
elif cmd == 'ask': # deal with MoxQuizz question
self.question = random.choice(self.qb) if self.quiz:
self.reply(str(self.question.question)) self.handle_quiz(nick, message)
elif cmd == 'lol': def start_quiz(self, nick):
self.reply(self.ponder(), target) self.quiz = 0
self.quiz_scores = dict()
self.connection.notice(self.channel, 'Quiz begun by %s.' % nick)
self.quiz_get_next()
elif cmd == 'urls' or cmd == 'list': def stop_quiz(self):
db = self.get_session() self.quiz = 0
for url in db.query(Url).order_by(Url.timestamp.desc())[:10]: self.quiz_scores = None
line = "%s %s" % (url.url, url.title) self.question = None
self.reply(line, target)
time.sleep(1)
elif cmd.startswith('urls ') or cmd.startswith('list '): def quiz_get_next(self):
db = self.get_session() self.quiz += 1
(listcmd, n) = cmd.split(" ", 1) self.question = random.choice(self.qb)
n = n.strip() print(str(self.question.question))
if n == "all": self.connection.notice(self.channel, str(self.question.question))
rows = db.query(Url).order_by(Url.timestamp.desc())
elif n.find("-") > 0:
(x, y) = n.split("-", 1)
try:
x = abs(int(x))
y = abs(int(y))
if y < x:
x, y = y, x
except ValueError as ex:
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
raise ex
rows = db.query(Url).order_by(Url.timestamp.desc())[x-1:y]
else:
try:
n = abs(int(n))
except ValueError as ex:
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
raise ex
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
for url in rows: def quiz_award_points(self, nick):
line = "%s %s" % (url.url, url.title) score = "%s point" % self.question.score
self.reply(line, target) if self.question.score != 1:
time.sleep(1) score += "s"
elif cmd.startswith('http:') or cmd.startswith('https:'): self.connection.notice(self.channel, 'Correct! The answer was %s. %s scores %s.' % (self.question.answer, nick, score))
title = self.save_url(target, cmd) if nick not in self.quiz_scores.keys():
if title is False: self.quiz_scores[nick] = 0
self.say_public("Sorry, I'm useless at UTF-8.") self.quiz_scores[nick] += self.question.score
else:
self.reply('URL added. %s' % title, target)
def quiz_check_win(self, nick):
if self.quiz_scores[nick] == 10:
self.connection.notice(self.channel, '%s wins with 10 points!' % nick)
self.quiz_scoreboard()
self.stop_quiz()
def quiz_scoreboard(self):
self.connection.notice(self.channel, 'Scoreboard:')
for nick in self.quiz_scores.keys():
score = "%s point" % self.quiz_scores[nick]
if self.quiz_scores[nick] != 1:
score += "s"
self.connection.notice(self.channel, '%s has %s.' % (nick, score))
def handle_quiz(self, nick, message):
# bail if there's no quiz or unanswered question.
if not self.quiz or not isinstance(self.question, Question):
return
# see if anyone answered correctly.
if self.question.attempt(message):
self.quiz_award_points(nick)
self.quiz_check_win(nick)
# if nobody has won, carry on
if self.quiz:
self.quiz_get_next()
def do_command(self, e, cmd):
"""
Handle bot commands.
"""
nick = e.source.nick
c = self.connection
if cmd == "die":
self.die()
elif cmd == 'help':
c.notice(nick, self.helptext)
elif cmd == 'status':
c.notice(nick, "I know %s questions." % len(self.qb))
elif cmd == 'halt' or cmd == 'quit':
if self.quiz:
self.quiz_scoreboard()
self.stop_quiz()
c.notice(self.channel, "Quiz halted by %s. Use ask to start a new one." % nick)
else: else:
self.reply(self.exclaim(), target) c.notice(self.channel, "No quiz running.")
except Exception as ex: elif cmd == 'ask':
print("Exception caught processing command: %s" % ex) if self.quiz:
print(" command was '%s' from %s" % (cmd, target)) c.notice(self.channel, "Quiz is running. Use halt or quit to stop.")
self.reply("Sorry, I didn't understand: %s" % cmd, target) c.notice(self.channel, str(self.question.question))
self.reply(self.helptext, target) elif isinstance(self.question, Question):
c.notice(self.channel, "There is an unanswered question.")
c.notice(self.channel, str(self.question.question))
else:
self.start_quiz(nick)
elif cmd == 'revolt':
if isinstance(self.question, Question):
c.notice(self.channel, "Fine, the answer is: %s" % self.question.answer)
self.quiz_get_next()
class LolBotFactory(protocol.ClientFactory): elif cmd.startswith('urls') or cmd.startswith('list'):
protocol = LolBot db = self.db_session()
def __init__(self, config_path):
self.config = get_config(config_path)
self.server = self.config['irc.server']
self.port = self.config['irc.port']
self.nickname = self.config['irc.nickname']
self.channel = self.config['irc.channel']
def clientConnectionLost(self, connector, reason):
print("Lost connection (%s), reconnecting." % (reason,))
connector.connect()
def clientConnectionFailed(self, connector, reason):
print("Could not connect: %s" % (reason,))
def get_options():
try:
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
except getopt.GetoptError as err:
print(str(err))
usage()
sys.exit(2)
config_path = ""
for option, value in options:
if option in ("-h", "--help"):
usage()
sys.exit(2)
if option in ("-c", "--config"):
config_path = value
return {'config_path': config_path}
def get_config(config_path):
"""
This method loads configuration options from a lolbot.conf file. The file
should look like this:
irc.server = irc.freenode.net
irc.port = 6667
irc.channel = #lolbottest
irc.nickname = lolbot
db.url = sqlite:///lolbot.db
"""
if not config_path:
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
if not os.path.exists(config_path):
print("Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option.")
usage()
sys.exit(1)
# open the configuration file and grab all param=value declarations.
config = {}
with open(config_path) as f:
for line in f:
# skip comments
if line.strip().startswith("#") or line.strip() == "":
continue
# collect up param = value
try: try:
(param, value) = line.strip().split("=", 1) (listcmd, n) = cmd.split(" ", 1)
if param.strip() != "":
config[param.strip()] = value.strip()
except ValueError: except ValueError:
continue n = '5'
# validate IRC host n = n.strip()
if "irc.server" not in config.keys(): if n == "all":
print("Error: the IRC server was not specified. Use --help for more information.") rows = db.query(Url).order_by(Url.timestamp.desc())
elif n.find("-") > 0:
(x, y) = n.split("-", 1)
try:
x = abs(int(x))
y = abs(int(y))
if y < x:
x, y = y, x
except ValueError as ex:
c.notice(nick, "Give me a number or a range of numbers, e.g. list 5 or list 11-20")
raise ex
rows = db.query(Url).order_by(Url.timestamp.desc())[x - 1: y]
else:
try:
n = abs(int(n))
except ValueError as ex:
c.notice(nick, "Give me a number or a range of numbers, e.g. list 5 or list 11-20")
raise ex
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
for url in rows:
line = "%s %s" % (url.url, url.title)
c.notice(nick, line)
time.sleep(1)
else:
c.notice(nick, "Not understood: " + cmd)
def main():
import sys
if len(sys.argv) != 5:
print("Usage: lolbot2.py <server[:port]> <channel> <nickname> <db>")
sys.exit(1) sys.exit(1)
# validate IRC port s = sys.argv[1].split(":", 1)
if "irc.port" not in config.keys(): server = s[0]
config["irc.port"] = "6667" if len(s) == 2:
try: try:
config["irc.port"] = int(config["irc.port"]) port = int(s[1])
except ValueError: except ValueError:
print("Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information.") print("Error: Erroneous port.")
sys.exit(1) sys.exit(1)
else:
port = 6667
channel = sys.argv[2]
nickname = sys.argv[3]
database = sys.argv[4]
# validate IRC channel debug("Parameters: server=%s port=%s nickname=%s channel=%s database=%s" % (server, port, nickname, channel, database))
if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"):
print("Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information.")
sys.exit(1)
# validate bot nickname irc.client.ServerConnection.buffer_class = irc.buffer.LenientDecodingLineBuffer
if "irc.nickname" not in config.keys(): bot = LolBot(channel, nickname, server, database, port)
config["irc.nickname"] = "lolbot" bot.start()
return config
def usage():
print("""Run a lolbot.
-h, --help
This message.
-c, --config=<filename>
Specify a configuration file. Defaults to lolbot.conf in the same
directory as the script.
Configuration:
irc.server = <host>
The IRC server, e.g. irc.freenode.net
irc.port = <port>
The IRC server port. Default: 6667
irc.channel = <channel>
The chat channel to join, e.g. #trainspotting
irc.nickname = <nickname>
The nickname for your lolbot. Default: "lolbot"
db.url = <url>
The URL to your lolbot database. This can be any valid URL accepted
by SQL Alchemy. If not specified, lolbot will attempt to use a
SQLite database called lolbot.db in the same directory as the
script.
""")
if __name__ == "__main__": if __name__ == "__main__":
args = get_options() main()
config = get_config(args['config_path'])
try:
reactor.connectTCP(config['irc.server'], config['irc.port'], LolBotFactory(args['config_path']))
reactor.run()
except KeyboardInterrupt:
print("Shutting down.")

View file

@ -1,7 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
"""
A MozQuizz question library for Python.
See http://moxquizz.de/ for the original implementation in TCL.
"""
from __future__ import unicode_literals, print_function from __future__ import unicode_literals, print_function
from io import open from io import open
import re
import sys
class Question: class Question:
@ -10,30 +17,103 @@ class Question:
""" """
category = None category = None
"""
The question category. Arbitrary text; optional.
"""
question = None question = None
"""
The question. Arbitrary text; required.
"""
answer = None answer = None
"""
The answer. Arbitrary text; required. Correct answers can also be covered
by the :attr:`regexp` property.
"""
regexp = None regexp = None
"""
A regular expression that will generate correct answers. Optional. See
also the :attr:`answer` property.
"""
author = None author = None
level = None """
The question author. Arbitrary text; optional.
"""
level = None # Default: NORMAL (constructor)
"""
The difficulty level. Value must be from the :attr:`LEVELS` tuple.
The default value is :attr:`NORMAL`.
"""
comment = None comment = None
score = 0 """
A comment. Arbitrary text; optional.
"""
score = 1
"""
The points scored for the correct answer. Integer value; default is 1.
"""
tip = list() tip = list()
"""
An ordered list of tips (hints) to display to users. Optional.
"""
tipcycle = 0 tipcycle = 0
"""
Indicates which tip is to be displayed next, if any.
"""
TRIVIAL = 1 TRIVIAL = 1
"""
A value for :attr:`level` that indicates a question of trivial difficulty.
"""
EASY = 2 EASY = 2
"""
A value for :attr:`level` that indicates a question of easy difficulty.
"""
NORMAL = 3 NORMAL = 3
"""
A value for :attr:`level` that indicates a question of average or normal
difficulty.
"""
HARD = 4 HARD = 4
"""
A value for :attr:`level` that indicates a question of hard difficulty.
"""
EXTREME = 5 EXTREME = 5
"""
A value for :attr:`level` that indicates a question of extreme difficulty
or obscurity.
"""
LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME) LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME)
"""
The available :attr:`level` difficulty values, :attr:`TRIVIAL`, :attr:`EASY`,
:attr:`NORMAL`, :attr:`HARD` and :attr:`EXTREME`.
"""
def __init__(self, attributes_dict): def __init__(self, attributes_dict):
"""
Constructor that takes a dictionary of MoxQuizz key-value pairs. Usually
called from a :class:`QuestionBank`.
"""
# Set defaults first.
self.level = self.NORMAL
self.parse(attributes_dict) self.parse(attributes_dict)
def parse(self, attributes_dict): def parse(self, attributes_dict):
""" """
Populate fields from a dictionary of attributes (from a question bank). Populate fields from a dictionary of attributes, usually provided by a
:class:`QuestionBank` :attr:`~QuestionBank.parse` call.
""" """
## Valid keys: ## Valid keys:
@ -69,7 +149,9 @@ class Question:
self.category = attributes_dict['Author'] self.category = attributes_dict['Author']
if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS: if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS:
self.level = attributes_dict['level'] self.level = attributes_dict['Level']
elif 'Level' in attributes_dict.keys() and attributes_dict['Level'] in QuestionBank.LEVEL_VALUES.keys():
self.level = QuestionBank.LEVEL_VALUES[attributes_dict['Level']]
if 'Comment' in attributes_dict.keys(): if 'Comment' in attributes_dict.keys():
self.comment = attributes_dict['Comment'] self.comment = attributes_dict['Comment']
@ -83,6 +165,9 @@ class Question:
if 'Tipcycle' in attributes_dict.keys(): if 'Tipcycle' in attributes_dict.keys():
self.tipcycle = attributes_dict['Tipcycle'] self.tipcycle = attributes_dict['Tipcycle']
def attempt(self, answer):
return (self.answer is not None and self.answer.lower() == answer.lower()) or (
self.regexp is not None and re.search(self.regexp, answer, re.IGNORECASE) is not None)
class QuestionBank: class QuestionBank:
""" """
@ -90,7 +175,15 @@ class QuestionBank:
""" """
filename = '' filename = ''
"""
The path or filename of the question bank file.
"""
questions = list() questions = list()
"""
A list of :class:`Question` objects, constituting the questions in the
question bank.
"""
# Case sensitive, to remain backwards-compatible with MoxQuizz. # Case sensitive, to remain backwards-compatible with MoxQuizz.
KEYS = ('Answer', KEYS = ('Answer',
@ -104,17 +197,34 @@ class QuestionBank:
'Tip', 'Tip',
'Tipcycle', 'Tipcycle',
) )
"""
The valid attributes available in a MoxQuizz question bank file.
"""
LEVEL_VALUES = {
'trivial': Question.TRIVIAL,
'baby': Question.TRIVIAL,
'easy': Question.EASY,
'normal': Question.NORMAL,
'hard': Question.HARD,
'difficult': Question.HARD,
'extreme': Question.EXTREME
}
"""
Text labels for the :attr:`Question.level` difficulty values.
"""
def __init__(self, filename): def __init__(self, filename):
""" """
Construct a question bank from a file. Constructor, takes a MozQuizz-formatted question bank filename.
""" """
self.filename = filename self.filename = filename
self.questions = self.parse(filename) self.questions = self.parse(filename)
def parse(self, filename): def parse(self, filename):
""" """
Read a Moxquizz question bank file into a list. Read a MoxQuizz-formatted question bank file. Returns a ``list`` of
:class:`Question` objects found in the file.
""" """
questions = list() questions = list()
@ -149,6 +259,8 @@ class QuestionBank:
# Fetch the next parameter. # Fetch the next parameter.
try: try:
(key, value) = line.split(':', 1) (key, value) = line.split(':', 1)
key = key.strip()
value = value.strip()
except ValueError: except ValueError:
print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i)) print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i))
continue continue
@ -162,6 +274,11 @@ class QuestionBank:
# Enumerate the Tips. # Enumerate the Tips.
if key == 'Tip': if key == 'Tip':
q['Tip'].append(value.strip()) q['Tip'].append(value.strip())
elif key == 'Level':
if value not in self.LEVEL_VALUES:
print("Unexpected Level value '%s' in MoxQuizz questionbank '%s', line '%s'." % (value, self.filename, i))
else:
q['Level'] = self.LEVEL_VALUES[value]
else: else:
q[key] = value.strip() q[key] = value.strip()
@ -170,12 +287,14 @@ class QuestionBank:
# A crappy test. # A crappy test.
if __name__ == '__main__': if __name__ == '__main__':
qb = QuestionBank('questions.doctorlard.en') qb = QuestionBank('questions/questions.doctorlard.en')
for q in qb.questions: for q in qb.questions:
print(q.question) print(q.question)
a = unicode(raw_input('A: '), 'utf8') if sys.version.startswith('2'):
#a = input('A: ') # Python 3 a = unicode(raw_input('A: '), 'utf8')
if a.lower() == q.answer.lower(): else:
a = input('A: ')
if q.attempt(a):
print("Correct!") print("Correct!")
else: else:
print("Incorrect - the answer is '%s'" % q.answer) print("Incorrect - the answer is '%s'" % q.answer)