]> jfr.im git - erebus.git/blame - modules/trivia.py
update todo
[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
6374d61f 5HINTTIMER = 10.0 # how long between hints
c0eee1b4 6HINTNUM = 3 # how many hints to give
6374d61f 7MAXMISSEDQUESTIONS = 25 # 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:
6374d61f 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]
6374d61f 87 self.parent.channel(self.chan).bot.msg(self.chan, "\00304,01Here's a hint: %s" % (''.join(self.hintstr)))
b16b8c05 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:
6374d61f 130 self.getbot().msg(self.getchan(), "\00304Fail! 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
6374d61f 162 qtext = "\00304,01Next up: "
c695f740 163 qary = nextq['question'].split(None)
164 for qword in qary:
6374d61f 165 qtext += "\00304,01"+qword+"\00301,01"+chr(random.randrange(0x61,0x7A)) #a-z
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
6374d61f 193 self.db['ranks'].sort(key=lambda nick: state.db['users'][nick]['points'], reverse=True)
194 for i in range(0, len(self.db['ranks'])):
195 nick = self.db['ranks'][i]
196 self.db['users'][nick]['rank'] = i
77c61775 197
198 if self.db['users'][user]['points'] >= state.db['target']:
199 self.gameover = True
200
80d02bd8 201 return self.db['users'][user]['points']
202
203 def points(self, user):
9557ee54 204 user = str(user).lower()
80d02bd8 205 if user in self.db['users']:
206 return self.db['users'][user]['points']
207 else:
208 return 0
209
210 def rank(self, user):
c695f740 211 user = str(user).lower()
fadbf980 212 if user in self.db['users']:
213 return self.db['users'][user]['rank']+1
214 else:
215 return len(self.db['users'])+1
77c61775 216
c695f740 217 def targetuser(self, user):
77c61775 218 if len(self.db['ranks']) == 0: return "no one is ranked!"
219
c695f740 220 user = str(user).lower()
fadbf980 221 if user in self.db['users']:
222 rank = self.db['users'][user]['rank']
223 if rank == 0:
224 return "you're in the lead!"
225 else:
226 return self.db['ranks'][rank-1]
c695f740 227 else:
fadbf980 228 return self.db['ranks'][-1]
c695f740 229 def targetpoints(self, user):
77c61775 230 if len(self.db['ranks']) == 0: return 0
231
c695f740 232 user = str(user).lower()
fadbf980 233 if user in self.db['users']:
234 rank = self.db['users'][user]['rank']
235 if rank == 0:
236 return "N/A"
237 else:
238 return self.db['users'][self.db['ranks'][rank-1]]['points']
c695f740 239 else:
fadbf980 240 return self.db['users'][self.db['ranks'][-1]]['points']
80d02bd8 241
c695f740 242state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
9557ee54 243
80d02bd8 244@lib.hookchan(state.db['chan'])
245def trivia_checkanswer(bot, user, chan, *args):
80d02bd8 246 line = ' '.join([str(arg) for arg in args])
247 if state.checkanswer(line):
6374d61f 248 bot.msg(chan, "\00312%s\003 has it! The answer was \00312%s\003. New score: %d. Rank: %d. Target: %s (%s)." % (user, line, state.addpoint(user), state.rank(user), state.targetuser(user), state.targetpoints(user)))
77c61775 249 if state.hintsgiven == 0:
6374d61f 250 bot.msg(chan, "\00312%s\003 got an extra point for getting it before the hints! New score: %d." % (user, state.addpoint(user)))
9557ee54 251 state.nextquestion()
80d02bd8 252
f6252f1c 253@lib.hook('points', needchan=False)
80d02bd8 254def cmd_points(bot, user, chan, realtarget, *args):
c695f740 255 if chan == realtarget: replyto = chan
80d02bd8 256 else: replyto = user
257
258 if len(args) != 0: who = args[0]
259 else: who = user
260
261 bot.msg(replyto, "%s has %d points." % (who, state.points(who)))
262
f6252f1c 263@lib.hook('give', clevel=lib.OP, needchan=False)
80d02bd8 264@lib.argsGE(1)
265def cmd_give(bot, user, chan, realtarget, *args):
80d02bd8 266 whoto = args[0]
c695f740 267 if len(args) > 1:
268 numpoints = int(args[1])
269 else:
270 numpoints = 1
271 balance = state.addpoint(whoto, numpoints)
fadbf980 272
c695f740 273 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
274
f6252f1c 275@lib.hook('setnext', clevel=lib.OP, needchan=False)
c695f740 276@lib.argsGE(1)
277def cmd_setnext(bot, user, chan, realtarget, *args):
278 line = ' '.join([str(arg) for arg in args])
279 linepieces = line.split('*')
b5c89dfb 280 if len(linepieces) < 2:
281 bot.msg(user, "Error: need <question>*<answer>")
282 return
c695f740 283 question = linepieces[0].strip()
284 answer = linepieces[1].strip()
285 state.nextq = {'question':question,'answer':answer}
286 bot.msg(user, "Done.")
287
f6252f1c 288@lib.hook('skip', clevel=lib.KNOWN, needchan=False)
c695f740 289def cmd_skip(bot, user, chan, realtarget, *args):
c0eee1b4 290 state.nextquestion(True)
c695f740 291
f6252f1c 292@lib.hook('start', needchan=False)
c695f740 293def cmd_start(bot, user, chan, realtarget, *args):
294 if chan == realtarget: replyto = chan
295 else: replyto = user
296
297 if state.curq is None:
298 state.nextquestion()
299 else:
300 bot.msg(replyto, "Game is already started!")
301
6374d61f 302#FIXME @lib.hook('stop', clevel=lib.KNOWN, needchan=False)
303@lib.hook('stop', needchan=False) #FIXME
c695f740 304def cmd_stop(bot, user, chan, realtarget, *args):
fadbf980 305 if stop():
306 bot.msg(state.chan, "Game stopped by %s" % (user))
307 else:
308 bot.msg(user, "Game isn't running.")
c695f740 309
fadbf980 310def stop():
c695f740 311 if state.curq is not None:
312 state.curq = None
c0eee1b4 313 try:
b16b8c05 314 state.steptimer.cancel()
c0eee1b4 315 except Exception as e:
316 print "!!! steptimer.cancel(): e"
fadbf980 317 return True
c695f740 318 else:
fadbf980 319 return False
80d02bd8 320
f6252f1c 321@lib.hook('rank', needchan=False)
80d02bd8 322def cmd_rank(bot, user, chan, realtarget, *args):
c695f740 323 if chan == realtarget: replyto = chan
80d02bd8 324 else: replyto = user
325
c695f740 326 if len(args) != 0: who = args[0]
327 else: who = user
328
77c61775 329 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 330
f6252f1c 331@lib.hook('top10', needchan=False)
fadbf980 332def cmd_top10(bot, user, chan, realtarget, *args):
77c61775 333 if len(state.db['ranks']) == 0:
334 return bot.msg(state.db['chan'], "No one is ranked!")
fadbf980 335
336 replylist = []
337 for nick in state.db['ranks'][0:10]:
338 user = state.db['users'][nick]
77c61775 339 replylist.append("%s (%s)" % (user['realnick'], user['points']))
340 bot.msg(state.db['chan'], ', '.join(replylist))
341
f6252f1c 342@lib.hook('settarget', clevel=lib.MASTER, needchan=False)
77c61775 343def cmd_settarget(bot, user, chan, realtarget, *args):
344 try:
345 state.db['target'] = int(args[0])
346 bot.msg(state.db['chan'], "Target has been changed to %s points!" % (state.db['target']))
347 except:
348 bot.msg(user, "Failed to set target.")
349
f6252f1c 350@lib.hook('triviahelp', needchan=False)
77c61775 351def cmd_triviahelp(bot, user, chan, realtarget, *args):
352 bot.msg(user, "POINTS [<user>]")
353 bot.msg(user, "START")
354 bot.msg(user, "RANK [<user>]")
355 bot.msg(user, "TOP10")
356
357 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.KNOWN:
358 bot.msg(user, "SKIP (>=KNOWN )")
359 bot.msg(user, "STOP (>=KNOWN )")
360 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.OP:
361 bot.msg(user, "GIVE <user> [<points>] (>=OP )")
362 bot.msg(user, "SETNEXT <q>*<a> (>=OP )")
363 if bot.parent.channel(state.db['chan']).levelof(user.auth) >= lib.MASTER:
364 bot.msg(user, "SETTARGET <points> (>=MASTER)")
b5c89dfb 365
6374d61f 366@lib.hooknum(417)
367def num_417(bot, textline):
368 bot.msg(state.db['chan'], "Whoops, it looks like that question didn't quite go through! (E:417). Let's try another...")
369 state.nextquestion(False)
370
b5c89dfb 371
372def specialQuestion(oldq):
373 newq = {'question': oldq['question'], 'answer': oldq['answer']}
374 qtype = oldq['question'].upper()
375
376 if qtype == "!MONTH":
377 newq['question'] = "What month is it currently (in UTC)?"
378 newq['answer'] = time.strftime("%B").lower()
379 elif qtype == "!MATH+":
380 randnum1 = random.randrange(0, 11)
381 randnum2 = random.randrange(0, 11)
382 newq['question'] = "What is %d + %d?" % (randnum1, randnum2)
383 newq['answer'] = spellout(randnum1+randnum2)
384 return newq
385
386def spellout(num):
387 return [
388 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
389 "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
390 "sixteen", "seventeen", "eighteen", "nineteen", "twenty"
391 ][num]