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