# OTriviaBot an irc triviabot # Copyright (C) 2002 Oskar Flordal # Oskar Flordal can be reached at oskar@flordal.net # or WWW at http://www.lysator.liu.se/~bobby # # This bot is a moddified version of the A2K bot by Mark Cornick # available at http://accutron2000.sourceforge.net/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA # # Things to change if you were to add a new questionmode # Questionfile check in the beginning # savequestions() should add your new question-list # on_question() needs a new mode # start -> question -> init a new questionobject -> hints -> test answers with # checkAnswer in the questionobject ############# #TODO # #kan itne skriva uit alla multisvar om man har för mångha max på en rad liksom #ircnät och kanal i commandline #svara på halva eller fler svarsalternativ #MODE andra än 1 2 3 4 ger ingen fråga fixa #STATISTIK!!! #mindre poäng om många försök? #winningstreak borde stå blir broken #spara vem som förökt addera en fråga 3 #ta bort alla från en person 2 #score mod över perioder (tidsstamp) #!help utöka #vote för att få igång #connection indication #Förbättra Levenshtein # allowedErrorAmount = 0.2 allowedErrorLength = 2 # ################# # QUESTION modules # #describing various forms of questions #should probably inherit from each other in some way and be placed in separate #files class singleQuestion: "A normal question with a single answer" #important vairables are answer with answer defined in init #also uses dead if it was fully answered at checkAnswer dead = False def timeUp(self): self.instance.connection.privmsg(trivconf.channel,color(6)+ 'Time is up the answer was: ' +self.answer) def next(self,whonick): self.instance.connection.privmsg(trivconf.channel,'!next called by '+whonick+ '(answer was: '+self.answer+')') #put up 3 delayed hints on delayed_commands def hints(self,connection): "Set up a series of hints and a force answer in 40 sec" #send out a few delayed hints connection.execute_delayed(0,self.hint, (connection,0)) connection.execute_delayed(10,self.hint, (connection,1)) connection.execute_delayed(20,self.hint, (connection,2)) connection.execute_delayed(30,self.hint, (connection,3)) def hint(self,connection,hintNr): # "make a hint whit letter written as underscore # more letter will be revealed with a higher hintNr" hintRand = Random(543) # gives the same pattern every time (543 seems good) text = 'Hint ('+repr(hintNr)+'/3): ' #3 hints hint = '' for x in self.answer: nr=hintRand.randrange(1,10) if x == ' ' or x == ',' or x == "'" or x == ".": hint = hint + x else: if nr < hintNr*2: hint = hint + x if nr >= hintNr*2: hint = hint + '_' connection.privmsg(trivconf.channel,color(4)+text+hint) # connection.execute_delayed(delay,connection.privmsg, # (trivconf.channel,color(4)+text+hint)) #true if exact answer or if fussy answer is on it will calculate a bit def checkAnswer(self,sugestion): #run levenstein percent = levenshtein(self.answer.lower(),sugestion) #if 80% were correct it is deeamed correct if percent= hintNr*2: hint = hint + '_' connection.privmsg(trivconf.channel,color(4)+text+hint) # connection.execute_delayed(delay,connection.privmsg, # (trivconf.channel,color(4)+text+hint)) #true if exact answer or if fussy answer is on it will calculate a bit def checkAnswer(self,sugestion): #run levenstein percent = levenshtein(self.answer,sugestion) #if 80% were correct it is deeamed correct if percent0: rand = randNr.randrange(0,len(posLeft)) self.question = self.question + trivia.wordquestions[nr][posLeft[rand]] + ' ' #remove the used index from posLeft posLeft[rand:rand+1]=[] #perhaps questionnr isnät necessary here... instance.connection.privmsg(trivconf.channel,color(5)+'What word can be made from these letters (M:4): ' +self.question) self.answer=trivia.wordquestions[nr] self.hints(instance.connection) # ############################################################ # Global Configuration # # Configuration not nescessarily for the user #CONSTANTS QUESTIONDELAY=4 # Version string. VERSION="OTriviaBot 0.9" #keeps the winning streak shouldn't be here winStreakWho ='' winStreakLength = 0 # Pull in necessary python bits import formatter,ircbot,irclib,os,re,string,sys,urllib,time import trivia,score from random import Random # Read the configuration and op list. try: import trivconf except ImportError: print "Couldn't read configuration from trivconf.py" sys.exit(1) #check the question file is ok (check that there is a question and an answer #for every post in the list #Add a new loop for new questionmodes here try: # If available use the server specified in the commandline # should extend with normal options print sys.argv if len(sys.argv)>=3 : trivconf.nick = sys.argv[1] trivconf.channel = '#'+sys.argv[2] print "Testing the question file" for x in trivia.questions: x[0]+"test"#use checkstring or something instead x[1]+"test" print "SingleQuestions are OK" for x in trivia.multiquestions: x[0]+"test" for y in x[1]: y[0]+"test" print "MultiQuestions are OK" for x in trivia.hintquestions: for y in x[0]: y[0]+"test" x[1]+"test" print "HintQuestions are OK" for x in trivia.wordquestions: x+"test" print "WordQuestions are OK" except IndexError: print "Something is wrong with the questionfile perhaps at: " print x sys.exit(1) except TypeError: print "A question/answer is not a string at:" print x sys.exit(1) randNr = Random(time.time()) ############################################################ # Miscellaneous utility subroutines # # #Levenshtein algorithm returns % correct when comparing who strings #borrowed without permission from #Magnus Lie Hetland http://mail.python.org/pipermail/python-list/2002-March/090371.html def levenshtein(a,b): "Calculates the Levenshtein distance between a and b." n, m = len(a), len(b) if n > m: # Make sure n <= m, to use O(min(n,m)) space a,b = b,a n,m = m,n current = range(n+1) for i in range(1,m+1): previous, current = current, [i]+[0]*m for j in range(1,n+1): add, delete = previous[j]+1, current[j-1]+1 change = previous[j-1] if a[j-1] != b[i-1]: change = change + 1 current[j] = min(add, delete, change) return float(current[n])/float(len(a)) #return procent correct def help(connection,whonick): "help comes when someone does !help give some commands." connection.notice(whonick,'Available commands are(not complete): !next, '+whonick+'.') return def rightnow(): "Return a string representing the current time and date. Swedish format." return time.strftime('%X %y%m%d',time.localtime(time.time())) def validateprefs(): "Check to make sure all of the preferences have been defined." try: trivconf.server trivconf.nick trivconf.name trivconf.channel trivconf.password trivconf.fussyans if ('change.this',6667) in trivconf.server: raise AttributeError,'server' if trivconf.password == 'changethis': raise AttributeError,'password' except AttributeError,pref: print '%s is not properly set in trivconf.py.' % pref sys.exit(1) #throw in a color (ctcp i belive) the defeacto standard is used #http://www.mircscripts.com/docs/color.txt #2c shoudl'give the background color aswell def color(color): "send in a string for color" return '\x03' + repr(color) def backcolor(color): "send in a string for color" return '\x2c' + repr(color) def process_clear(connection): "clear the list of delayed events" connection.irclibobj.delayed_commands=[] ####################################################### # Trivia part and som print functions # #probably should use all these flags def add_point(name,time): flag=0 for x in score.score: if x[1]==name: x[0]=x[0]+(40-time)/4+4 #more point if you do it fast flag=1 if flag==0: score.score[len(score.score):]=[[(40-time)/4+4,name]] #appendlen score.score.sort() score.score.reverse() #prints top5 scores + the score of the person requesting the scoreboard def print_score(connection,destination,whonick): placeiter = 0 nickFound = False connection.privmsg(destination,color(5)+'SCOREBOARD') for x in score.score: #make sure the person who asked for score get's to know his #position even if it is not top10 placeiter=placeiter+1 if x[1] == whonick: nickFound = True #only show top 5 if placeiter > 5: if nickFound: #can't avoid this can I? if x[1] == whonick: connection.privmsg(destination,repr(placeiter)+'| ' + x[1] + ' '+ repr(x[0])+'p') break else: connection.privmsg(destination,repr(placeiter)+'| ' + x[1] + ' '+ repr(x[0])+'p') #Save scores to the scorefile score.py def savescore(): try: savescore=open("score.py","w") savescore.write("""#genrated score file score=%s""" % (score.score)) savescore.close() print "Score saved" return 1 except IOError: print "savescore() failed" return 0 #save so any new questions submitted during the session are properly saved def savequestions(): try: savequestions=open("trivia.py","w") savequestions.write("""#generated question file questions=[""") for x in trivia.questions: savequestions.write("%s,\n" % (x)) savequestions.write("]\nmultiquestions=[") for x in trivia.multiquestions: savequestions.write("%s,\n" % (x)) savequestions.write("]\nhintquestions=[") for x in trivia.hintquestions: savequestions.write("%s,\n" % (x)) savequestions.write("]\nwordquestions=[") for x in trivia.wordquestions: savequestions.write("'%s'," % (x)) savequestions.write("]") savequestions.close() print "Questions saved" return 1 except IOError: print "savequestions() failed" return 0 #print score for somebody probably after a correct answer def print_score_personal(connection,whonick): placeiter=0 for x in score.score: placeiter=placeiter+1 if x[1]==whonick: persScore=x[0] place=placeiter connection.privmsg(trivconf.channel,'Your score ('+whonick+') is ' +repr(persScore)+', placed ' + repr(place) + ' of ' + repr(len(score.score))) def delayed_answer(connection,answer,instance): "what happens if noone has answerd in 40 sec" instance.winStreakWho='' instance.question.timeUp() instance.ansWaiting=0 connection.execute_delayed(QUESTIONDELAY,trivbot.on_question, (instance,connection)) #a break before next q ############################################################ # The bot itself, and its event handlers # ############################################################ class trivbot(ircbot.SingleServerIRCBot): "The TRIV bot object." question='' uptime = 0 mode = [1, 2, 3, 4] #on_pubmsg will look for answer after a question ansWaiting=0 #the answer on_pubmsg is looking for answer= '' #starttime for a question to calculate time and score questionTime=0 #a container for new not yet validated questions #an element looks like [[question,answer],author] newQuestions = [] #unaswered questions with user who sent it waitAns = {} started = False winStreakWho='' winStreakLength=0 def resetWinStreak(self): self.winStreakWho= ' ' self.winStreakLength = 0 def __init__(self): "Constructor." #reference for !uptime self.uptime = time.time() # Check to make sure all our preferences are there. validateprefs() # Create a new object. ircbot.SingleServerIRCBot.__init__(self, trivconf.server, trivconf.nick, trivconf.name,10) # Set the channel and go. self.channel = trivconf.channel self.start() def get_version(self): "Returns the bot version." return VERSION+' (Based on http://accutron2000.sourceforge.net/)' def is_chanop(self,whonick): "Return 1 if the user has ops, 0 otherwise." # maybe this could be made simpler, dunno for channelname,channelobject in self.channels.items(): if channelobject.has_user(whonick): if channelobject.is_oper(whonick): return 1 else: return 0 def on_welcome(self, connection, event): "Run after connecting to the IRC server." connection.join(trivconf.channel) def on_nicknameinuse(self, connection, event): "Handles nickname-in-use errors." trivconf.nick=trivconf.nick+'_' self._nickname=trivconf.nick connection.nick(trivconf.nick) def on_join(self, connection, event): "Handles new clients joining the channel." def on_ctcp(self,connection,event): "Handles CTCP requests." whonick=irclib.nm_to_n(event.source()) userhost=irclib.nm_to_uh(event.source()) message=string.lower(event.arguments()[0]) if message == 'version': connection.notice(whonick,self.get_version()) if message == 'ping': connection.pong(whonick) def on_kick(self,connection,event): "Handles clients being kicked from the channel." whonick=irclib.nm_to_n(event.source()) userhost=irclib.nm_to_uh(event.source()) message=event.arguments()[0] channel=event.target() victim=event.arguments()[0] reason=event.arguments()[1] # Try to rejoin... if channel == trivconf.channel and victim == trivconf.nick: connection.join(trivconf.channel) def on_question(self, connection): "Set up a question with hints" #pick one of the modes available in mode rand = self.mode[randNr.randrange(0,len(self.mode))] #Add new modes here! if rand == 1: self.question = singleQuestion(self) elif rand == 2: self.question = multiQuestion(self) elif rand == 3: self.question = hintQuestion(self) else: self.question = wordQuestion(self) connection.execute_delayed(40,delayed_answer, (connection,self.answer,self)) self.ansWaiting=1 self.questionTime=time.time() def on_pubmsg(self, connection, event): "Handles public messages on the channel." whonick=irclib.nm_to_n(event.source()) userhost=irclib.nm_to_uh(event.source()) message=event.arguments()[0] #make sure it's this way as self.question isn't initialized #from the beginning if self.ansWaiting and self.question.checkAnswer(message.lower()): qtime=time.time()-self.questionTime #check if the nick won before and if so raise his credit if whonick == self.winStreakWho: self.winStreakLength = self.winStreakLength+1 else: self.winStreakWho = whonick self.winStreakLength=1 #skicka whonick och int(qtime) till typ belöningsfunktionen och avbryt om den vill då borde den veta om den har fler svar att ge för den kan flgga internt # add_point(whonick,int(qtime)) if self.question.score(whonick,qtime,self.winStreakLength): #print something apropriate if this was the last question #that is is self.question.score is TRUE that also means #we do not want any more answers process_clear(connection) self.ansWaiting=0 connection.execute_delayed(QUESTIONDELAY,self.on_question, (connection,)) # an answer can possibly be a command to if message=='!help': help(connection,whonick) elif message=='!score': if self.is_chanop(whonick): print_score(connection,trivconf.channel,whonick) elif message=='!next' and self.ansWaiting: if self.is_chanop(whonick): process_clear(connection) #get rid of the old question self.question.next(whonick) connection.execute_delayed(0,self.on_question, (connection,)) #ask new(instantly) elif message=='!uptime': connection.privmsg(trivconf.channel,trivconf.nick + ' uptime: '+repr(int(time.time()-self.uptime))+ "s") #catch error reports. Usage !error elif message[0:7]=='!error ': try: error = open("error.log",'a') error.write(message[7:]) error.write("(" + str(rightnow()) +") ") error.write('\n') error.close() except IOError: connection.notice(whonick,'!error failed please notify the bot operator!') def on_privmsg(self, connection, event): "Handles private messages to the bot." whonick=irclib.nm_to_n(event.source()) userhost=irclib.nm_to_uh(event.source()) message=event.arguments()[0] #all input in your console print whonick + ': ' +message+ "(" + str(rightnow()) +") " # print message if message == "hello": connection.notice(whonick,color(4)+'Hello, '+whonick+'.') # send a scoreboard elif message == 'score': print_score(connection,whonick,whonick) #add a new question (it won't save it yet...) elif message[0:10] == 'question: ': connection.privmsg(whonick, "And what is the answer to " + message[10:] +"(reply with answer: ") self.waitAns[whonick]=message[10:] elif message[0:8] == 'answer: ': if self.waitAns.has_key(whonick) : #ÅÄÖ won't go lowercase (how do i fix this?) connection.privmsg(whonick, "Your questions has been registered (Q: "+self.waitAns[whonick] + " A: "+message[8:].lower() +")") self.newQuestions.append([[self.waitAns[whonick],message[8:].lower()],whonick]) else: connection.privmsg(whonick, "No question given! (try question: )") #look at the last question among the new elif message == 'nextnew': if len(self.newQuestions) > 0: connection.privmsg(whonick,"Nr: "+repr(len(self.newQuestions))+ " Q: " + self.newQuestions[len(self.newQuestions)-1][0][0] + " A: "+color(4)+">|" + self.newQuestions[len(self.newQuestions)-1][0][1]+"|<") else: connection.privmsg(whonick, "No new questions waiting!") else: # Check for password. # authmessage is what remains after stripping the p/w. try: (password,authmessage) = string.split(message,' ',1) authmessage=authmessage except ValueError: password = None authmessage = message # If password is OK: if password == trivconf.password: # The first word of authmessage will be a command - # split it off from any args. try: (authcmd, authargs) = string.split(authmessage,' ',1) except ValueError: authcmd = authmessage authargs = None # /msg bot die: kill the bot if authcmd == "die": savescore() savequestions() self.die(VERSION+" Killed, ordered by "+whonick) # start the trivia elif authcmd == 'start' and self.started == False: # do i ! self.started = True connection.privmsg(trivconf.channel, "Trivia started by "+whonick) self.on_question(connection) # stop the trivia elif authcmd == 'stop' and self.started: self.started = False self.ansWaiting=0 connection.privmsg(trivconf.channel, "Trivia stopped by "+whonick) process_clear(connection) # save the score to score.py elif authcmd == "savescore": if savescore(): connection.privmsg(whonick, "score saved") else: connection.privmsg(whonick, "Error: score not saved") #validate the latest question elif authcmd == 'queuevalidate': if len(self.newQuestions) > 0: trivia.questions.append(self.newQuestions.pop()[0]) connection.privmsg(whonick, "Question Validated") else: connection.privmsg(whonick, "No questions to validate! (try question: )") #throw away the last question elif authcmd == 'queueremove': if len(self.newQuestions) > 0: self.newQuestions.pop() connection.privmsg(whonick, "Question removed!") else: connection.privmsg(whonick, "No questions to remove!") elif authcmd == 'checkquestion': if authargs != None: try: connection.privmsg(whonick,trivia.questions[int(authargs)][0] +" "+color(4) +trivia.questions[int(authargs)][1]) except ValueError: connection.privmsg(whonick,"That is not a number") elif authcmd == 'changeanswer': if authargs != None: try: (questionNr, answer) = string.split(authargs,' ',1) except ValueError: questionNr = 'NaN' try: trivia.questions[int(questionNr)][1]=answer.lower() connection.privmsg(whonick, "answer changed") connection.privmsg(whonick,trivia.questions[int(questionNr)]) except ValueError: connection.privmsg(whonick, "Syntax Error!") elif authcmd == 'changequestion': if authargs != None: try: (questionNr, question) = string.split(authargs,' ',1) except ValueError: questionNr = 'NaN' try: trivia.questions[int(questionNr)][0]=question connection.privmsg(whonick, "question changed") connection.privmsg(whonick,trivia.questions[int(questionNr)]) except ValueError: connection.privmsg(whonick, "Syntax Error!") elif authcmd == 'mode': if authargs != None: try: mode = string.split(authargs,' ') for x in range(len(mode)): mode[x] = int(mode[x]) #if we made it change self.mode = mode #shows everyone in the channel that something #happened connection.privmsg(trivconf.channel,color(4) +whonick+" changed mode to " +repr(self.mode)) #and a confirmation connection.privmsg(whonick,"mode changed to " +repr(self.mode)) except ValueError: connection.privmsg(whonick,"That is not a number") else: connection.privmsg(whonick, "Error: no argument try a number") elif authcmd == 'remove': try: trivia.questions[int(authargs):int(authargs)+1]=[] connection.privmsg(whonick,"Question removed") except ValueError: connection.privmsg(whonick,"That is not a number") #save new questions elif authcmd == 'savequestions': if savequestions(): connection.privmsg(whonick, "Questions saved") else: connection.privmsg(whonick, "Error: score not saved") # reset all scores elif authcmd == "resetscore": score.score=[] connection.privmsg(whonick, "score resetted") # Change the bot's nick. elif authcmd == "nick": if authargs != None: trivconf.nick = authargs self._nickname=trivconf.nick connection.nick(trivconf.nick) else: connection.noti(whonick, 'Not enough arguments given to nick!') # Change the password. elif authcmd == "passwd": if authargs != None: trivconf.password = authargs connection.notice(whonick,'Password changed. Do a writeconf to make permanent.') else: connection.notice(whonick,'Not enough arguments given to passwd!') # Write the current conf file. elif authcmd == "writeconf": try: writeconf = open("trivconf.py","w") writeconf.write("""# Otrivbot config # This file created by %s using writeconf at %s # The IRC server(s) (host and port) to connect to. This is a list, # and can contain one or more sets of hosts or ports, e.g. # server = [('irc.foo.com',6667),('irc.bar.com',6667)] server = %s # The nickname to use: nick = '%s' # The name to use: name = '%s' # The channel to join: channel = '%s' # A password for remote commands via /msg: password = '%s' # Turn this on if you want to allow small spellingerror in the answer fuzzyans = '%s' """ % (whonick,rightnow(),trivconf.server,trivconf.nick,trivconf.name, trivconf.channel,trivconf.password,trivconf.fuzzyans)) writeconf.close() connection.notice(whonick,'writeconf successful.') except IOError: connection.notice(whonick,'writeconf failed!')