]> jfr.im git - erebus.git/blame - modules/trivia.py
added helper function to eval module
[erebus.git] / modules / trivia.py
CommitLineData
80d02bd8 1# Erebus IRC bot - Author: Erebus Team
c695f740 2# trivia module
80d02bd8 3# This file is released into the public domain; see http://unlicense.org/
4
c0eee1b4 5HINTTIMER = 15.0 # how long between hints
6HINTNUM = 3 # how many hints to give
7MAXMISSEDQUESTIONS = 5 # how many missed questions before game stops
fadbf980 8
80d02bd8 9# module info
10modinfo = {
11 'author': 'Erebus Team',
12 'license': 'public domain',
13 'compatible': [1], # compatible module API versions
14 'depends': [], # other modules required to work properly?
15}
16
17# preamble
18import modlib
19lib = modlib.modlib(__name__)
20def modstart(parent, *args, **kwargs):
9557ee54 21 state.parent = parent
80d02bd8 22 return lib.modstart(parent, *args, **kwargs)
23def modstop(*args, **kwargs):
c0eee1b4 24 global state
fadbf980 25 stop()
77c61775 26 state.closeshop()
80d02bd8 27 del state
28 return lib.modstop(*args, **kwargs)
29
30# module code
b5c89dfb 31import json, random, threading, re, time
b16b8c05 32
c0eee1b4 33try:
34 import twitter
35 hastwitter = True
36except ImportError:
37 hastwitter = False
38
b16b8c05 39def findnth(haystack, needle, n): #http://stackoverflow.com/a/1884151
40 parts = haystack.split(needle, n+1)
41 if len(parts)<=n+1:
42 return -1
43 return len(haystack)-len(parts[-1])-len(needle)
80d02bd8 44
45class TriviaState(object):
77c61775 46 def __init__(self, questionfile, parent=None):
47 self.parent = parent
80d02bd8 48 self.questionfile = questionfile
49 self.db = json.load(open(questionfile, "r"))
9557ee54 50 self.chan = self.db['chan']
51 self.curq = None
c695f740 52 self.nextq = None
b16b8c05 53 self.steptimer = None
54 self.hintstr = None
55 self.hintanswer = None
77c61775 56 self.hintsgiven = 0
b16b8c05 57 self.revealpossibilities = None
77c61775 58 self.gameover = False
c0eee1b4 59 self.missedquestions = 0
80d02bd8 60
61 def __del__(self):
77c61775 62 self.closeshop()
63 def closeshop(self):
b16b8c05 64 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
65 self.steptimer.cancel()
c695f740 66 if json is not None and json.dump is not None:
67 json.dump(self.db, open(self.questionfile, "w"), indent=4, separators=(',',': '))
80d02bd8 68
fadbf980 69 def getchan(self):
70 return self.parent.channel(self.chan)
71 def getbot(self):
72 return self.getchan().bot
73
b16b8c05 74 def nexthint(self, hintnum):
b16b8c05 75 answer = self.hintanswer
76
b5c89dfb 77 if self.hintstr is None or self.revealpossibilities is None or self.reveal is None:
b16b8c05 78 self.hintstr = list(re.sub(r'[a-zA-Z0-9]', '*', answer))
79 self.revealpossibilities = range(''.join(self.hintstr).count('*'))
b5c89dfb 80 self.reveal = int(''.join(self.hintstr).count('*') * (7/24.0))
b16b8c05 81
b5c89dfb 82 for i in range(self.reveal):
b16b8c05 83 revealcount = random.choice(self.revealpossibilities)
84 revealloc = findnth(''.join(self.hintstr), '*', revealcount)
85 self.revealpossibilities.remove(revealcount)
86 self.hintstr[revealloc] = answer[revealloc]
87 self.parent.channel(self.chan).bot.msg(self.chan, "Here's a hint: %s" % (''.join(self.hintstr)))
88
77c61775 89 self.hintsgiven += 1
90
fadbf980 91 if hintnum < HINTNUM:
92 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
b16b8c05 93 self.steptimer.start()
94 else:
fadbf980 95 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
b16b8c05 96 self.steptimer.start()
97
77c61775 98 def doGameOver(self):
99 def msg(line): self.getbot().msg(self.getchan(), line)
100 def person(num): return self.db['users'][self.db['ranks'][num]]['realnick']
101 def pts(num): return self.db['users'][self.db['ranks'][num]]['points']
c0eee1b4 102 winner = person(0)
77c61775 103 try:
104 msg("\00312THE GAME IS OVER!!!")
105 msg("THE WINNER IS: %s (%s)" % (person(0), pts(0)))
106 msg("2ND PLACE: %s (%s)" % (person(1), pts(1)))
107 msg("3RD PLACE: %s (%s)" % (person(2), pts(2)))
108 [msg("%dth place: %s (%s)" % (i+1, person(i), pts(i))) for i in range(3,10)]
109 except IndexError: pass
c0eee1b4 110 except Exception as e: msg("DERP! %r" % (e))
111
77c61775 112 self.db['users'] = {}
113 self.db['ranks'] = []
114 stop()
115 self.closeshop()
c0eee1b4 116
117 if hastwitter:
118 t = twitter.Twitter(auth=twitter.OAuth(self.getbot().parent.cfg.get('trivia', 'token'),
119 self.getbot().parent.cfg.get('trivia', 'token_sec'),
120 self.getbot().parent.cfg.get('trivia', 'con'),
121 self.getbot().parent.cfg.get('trivia', 'con_sec')))
122 t.statuses.update(status="Round is over! The winner was %s" % (winner))
123
77c61775 124 self.__init__(self.questionfile, self.parent)
125
b5c89dfb 126 def nextquestion(self, qskipped=False, iteration=0):
77c61775 127 if self.gameover == True:
128 return self.doGameOver()
fadbf980 129 if qskipped:
130 self.getbot().msg(self.getchan(), "Fail! The correct answer was: %s" % (self.hintanswer))
c0eee1b4 131 self.missedquestions += 1
132 else:
133 self.missedquestions = 0
fadbf980 134
b16b8c05 135 if isinstance(self.steptimer, threading._Timer):
136 self.steptimer.cancel()
c0eee1b4 137
b16b8c05 138 self.hintstr = None
77c61775 139 self.hintsgiven = 0
b16b8c05 140 self.revealpossibilities = None
b5c89dfb 141 self.reveal = None
b16b8c05 142
c0eee1b4 143 if self.missedquestions > MAXMISSEDQUESTIONS:
144 stop()
145 self.getbot().msg(self.getchan(), "%d questions unanswered! Stopping the game.")
b16b8c05 146
c695f740 147 if state.nextq is not None:
148 nextq = state.nextq
c695f740 149 state.nextq = None
150 else:
151 nextq = random.choice(self.db['questions'])
b5c89dfb 152
153 if nextq['question'][0] == "!":
154 nextq = specialQuestion(nextq)
155
156 if iteration < 10 and 'lastasked' in nextq and nextq['lastasked'] - time.time() < 24*60*60:
157 return self.nextquestion(iteration=iteration+1) #short-circuit to pick another question
158 nextq['lastasked'] = time.time()
159
160 nextq['answer'] = nextq['answer'].lower()
c695f740 161
162 qtext = "\00300,01Next up: "
163 qary = nextq['question'].split(None)
164 for qword in qary:
165 qtext += "\00300,01"+qword+"\00301,01"+chr(random.randrange(32,126))
fadbf980 166 self.getbot().msg(self.chan, qtext)
80d02bd8 167
b5c89dfb 168 self.curq = nextq
169
c0eee1b4 170 if isinstance(self.curq['answer'], basestring): self.hintanswer = self.curq['answer']
171 else: self.hintanswer = random.choice(self.curq['answer'])
172
fadbf980 173 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
b16b8c05 174 self.steptimer.start()
175
80d02bd8 176 def checkanswer(self, answer):
9557ee54 177 if self.curq is None:
178 return False
179 elif isinstance(self.curq['answer'], basestring):
80d02bd8 180 return answer.lower() == self.curq['answer']
181 else: # assume it's a list or something.
182 return answer.lower() in self.curq['answer']
77c61775 183
80d02bd8 184 def addpoint(self, _user, count=1):
9557ee54 185 _user = str(_user)
80d02bd8 186 user = _user.lower()
187 if user in self.db['users']:
188 self.db['users'][user]['points'] += count
189 else:
190 self.db['users'][user] = {'points': count, 'realnick': _user, 'rank': len(self.db['ranks'])}
9557ee54 191 self.db['ranks'].append(user)
80d02bd8 192
77c61775 193 state.db['ranks'].sort(key=lambda nick: state.db['users'][nick]['points'], reverse=True)
194
195 if self.db['users'][user]['points'] >= state.db['target']:
196 self.gameover = True
197
80d02bd8 198 return self.db['users'][user]['points']
199
200 def points(self, user):
9557ee54 201 user = str(user).lower()
80d02bd8 202 if user in self.db['users']:
203 return self.db['users'][user]['points']
204 else:
205 return 0
206
207 def rank(self, user):
c695f740 208 user = str(user).lower()
fadbf980 209 if user in self.db['users']:
210 return self.db['users'][user]['rank']+1
211 else:
212 return len(self.db['users'])+1
77c61775 213
c695f740 214 def targetuser(self, user):
77c61775 215 if len(self.db['ranks']) == 0: return "no one is ranked!"
216
c695f740 217 user = str(user).lower()
fadbf980 218 if user in self.db['users']:
219 rank = self.db['users'][user]['rank']
220 if rank == 0:
221 return "you're in the lead!"
222 else:
223 return self.db['ranks'][rank-1]
c695f740 224 else:
fadbf980 225 return self.db['ranks'][-1]
c695f740 226 def targetpoints(self, user):
77c61775 227 if len(self.db['ranks']) == 0: return 0
228
c695f740 229 user = str(user).lower()
fadbf980 230 if user in self.db['users']:
231 rank = self.db['users'][user]['rank']
232 if rank == 0:
233 return "N/A"
234 else:
235 return self.db['users'][self.db['ranks'][rank-1]]['points']
c695f740 236 else:
fadbf980 237 return self.db['users'][self.db['ranks'][-1]]['points']
80d02bd8 238
c695f740 239state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
9557ee54 240
80d02bd8 241@lib.hookchan(state.db['chan'])
242def trivia_checkanswer(bot, user, chan, *args):
80d02bd8 243 line = ' '.join([str(arg) for arg in args])
244 if state.checkanswer(line):
77c61775 245 bot.msg(chan, "\00308%s\003 has it! The answer was \00308%s\003. New score: %d. Rank: %d. Target: %s (%s)." % (user, line, state.addpoint(user), state.rank(user), state.targetuser(user), state.targetpoints(user)))
246 if state.hintsgiven == 0:
247 bot.msg(chan, "\00308%s\003 got an extra point for getting it before the hints! New score: %d." % (user, state.addpoint(user)))
9557ee54 248 state.nextquestion()
80d02bd8 249
f6252f1c 250@lib.hook('points', needchan=False)
80d02bd8 251def cmd_points(bot, user, chan, realtarget, *args):
c695f740 252 if chan == realtarget: replyto = chan
80d02bd8 253 else: replyto = user
254
255 if len(args) != 0: who = args[0]
256 else: who = user
257
258 bot.msg(replyto, "%s has %d points." % (who, state.points(who)))
259
f6252f1c 260@lib.hook('give', clevel=lib.OP, needchan=False)
80d02bd8 261@lib.argsGE(1)
262def cmd_give(bot, user, chan, realtarget, *args):
80d02bd8 263 whoto = args[0]
c695f740 264 if len(args) > 1:
265 numpoints = int(args[1])
266 else:
267 numpoints = 1
268 balance = state.addpoint(whoto, numpoints)
fadbf980 269
c695f740 270 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
271
f6252f1c 272@lib.hook('setnext', clevel=lib.OP, needchan=False)
c695f740 273@lib.argsGE(1)
274def cmd_setnext(bot, user, chan, realtarget, *args):
275 line = ' '.join([str(arg) for arg in args])
276 linepieces = line.split('*')
b5c89dfb 277 if len(linepieces) < 2:
278 bot.msg(user, "Error: need <question>*<answer>")
279 return
c695f740 280 question = linepieces[0].strip()
281 answer = linepieces[1].strip()
282 state.nextq = {'question':question,'answer':answer}
283 bot.msg(user, "Done.")
284
f6252f1c 285@lib.hook('skip', clevel=lib.KNOWN, needchan=False)
c695f740 286def cmd_skip(bot, user, chan, realtarget, *args):
c0eee1b4 287 state.nextquestion(True)
c695f740 288
f6252f1c 289@lib.hook('start', needchan=False)
c695f740 290def cmd_start(bot, user, chan, realtarget, *args):
291 if chan == realtarget: replyto = chan
292 else: replyto = user
293
294 if state.curq is None:
295 state.nextquestion()
296 else:
297 bot.msg(replyto, "Game is already started!")
298
f6252f1c 299@lib.hook('stop', clevel=lib.KNOWN, needchan=False)
c695f740 300def cmd_stop(bot, user, chan, realtarget, *args):
fadbf980 301 if stop():
302 bot.msg(state.chan, "Game stopped by %s" % (user))
303 else:
304 bot.msg(user, "Game isn't running.")
c695f740 305
fadbf980 306def stop():
c695f740 307 if state.curq is not None:
308 state.curq = None
c0eee1b4 309 try:
b16b8c05 310 state.steptimer.cancel()
c0eee1b4 311 except Exception as e:
312 print "!!! steptimer.cancel(): e"
fadbf980 313 return True
c695f740 314 else:
fadbf980 315 return False
80d02bd8 316
f6252f1c 317@lib.hook('rank', needchan=False)
80d02bd8 318def cmd_rank(bot, user, chan, realtarget, *args):
c695f740 319 if chan == realtarget: replyto = chan
80d02bd8 320 else: replyto = user
321
c695f740 322 if len(args) != 0: who = args[0]
323 else: who = user
324
77c61775 325 bot.msg(replyto, "%s is in %d place (%s points). Target is: %s (%s points)." % (who, state.rank(who), state.points(who), state.targetuser(who), state.targetpoints(who)))
fadbf980 326
f6252f1c 327@lib.hook('top10', needchan=False)
fadbf980 328def cmd_top10(bot, user, chan, realtarget, *args):
77c61775 329 if len(state.db['ranks']) == 0:
330 return bot.msg(state.db['chan'], "No one is ranked!")
fadbf980 331
332 replylist = []
333 for nick in state.db['ranks'][0:10]:
334 user = state.db['users'][nick]
77c61775 335 replylist.append("%s (%s)" % (user['realnick'], user['points']))
336 bot.msg(state.db['chan'], ', '.join(replylist))
337
f6252f1c 338@lib.hook('settarget', clevel=lib.MASTER, needchan=False)
77c61775 339def cmd_settarget(bot, user, chan, realtarget, *args):
340 try:
341 state.db['target'] = int(args[0])
342 bot.msg(state.db['chan'], "Target has been changed to %s points!" % (state.db['target']))
343 except:
344 bot.msg(user, "Failed to set target.")
345
f6252f1c 346@lib.hook('triviahelp', needchan=False)
77c61775 347def cmd_triviahelp(bot, user, chan, realtarget, *args):
348 bot.msg(user, "POINTS [<user>]")
349 bot.msg(user, "START")
350 bot.msg(user, "RANK [<user>]")
351 bot.msg(user, "TOP10")
352
353 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.KNOWN:
354 bot.msg(user, "SKIP (>=KNOWN )")
355 bot.msg(user, "STOP (>=KNOWN )")
356 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.OP:
357 bot.msg(user, "GIVE <user> [<points>] (>=OP )")
358 bot.msg(user, "SETNEXT <q>*<a> (>=OP )")
359 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.MASTER:
360 bot.msg(user, "SETTARGET <points> (>=MASTER)")
b5c89dfb 361
362
363def specialQuestion(oldq):
364 newq = {'question': oldq['question'], 'answer': oldq['answer']}
365 qtype = oldq['question'].upper()
366
367 if qtype == "!MONTH":
368 newq['question'] = "What month is it currently (in UTC)?"
369 newq['answer'] = time.strftime("%B").lower()
370 elif qtype == "!MATH+":
371 randnum1 = random.randrange(0, 11)
372 randnum2 = random.randrange(0, 11)
373 newq['question'] = "What is %d + %d?" % (randnum1, randnum2)
374 newq['answer'] = spellout(randnum1+randnum2)
375 return newq
376
377def spellout(num):
378 return [
379 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
380 "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
381 "sixteen", "seventeen", "eighteen", "nineteen", "twenty"
382 ][num]