]> jfr.im git - erebus.git/blob - modules/trivia.py
move stuff around
[erebus.git] / modules / trivia.py
1 # Erebus IRC bot - Author: Erebus Team
2 # trivia module
3 # This file is released into the public domain; see http://unlicense.org/
4
5 #TODO:
6 # stop game after X unanswered questions
7 # bonus points
8 # v2
9 # team play
10 # statistics
11
12 HINTTIMER = 15.0 # how long between hints
13 HINTNUM = 3 # how many hints to give
14 MAXMISSEDQUESTIONS = 5 # how many missed questions before game stops
15
16 # module info
17 modinfo = {
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
25 import modlib
26 lib = modlib.modlib(__name__)
27 def modstart(parent, *args, **kwargs):
28 state.parent = parent
29 return lib.modstart(parent, *args, **kwargs)
30 def modstop(*args, **kwargs):
31 global state
32 stop()
33 state.closeshop()
34 del state
35 return lib.modstop(*args, **kwargs)
36
37 # module code
38 import json, random, threading, re, time
39
40 try:
41 import twitter
42 hastwitter = True
43 except ImportError:
44 hastwitter = False
45
46 def 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)
51
52 class TriviaState(object):
53 def __init__(self, questionfile, parent=None):
54 self.parent = parent
55 self.questionfile = questionfile
56 self.db = json.load(open(questionfile, "r"))
57 self.chan = self.db['chan']
58 self.curq = None
59 self.nextq = None
60 self.steptimer = None
61 self.hintstr = None
62 self.hintanswer = None
63 self.hintsgiven = 0
64 self.revealpossibilities = None
65 self.gameover = False
66 self.missedquestions = 0
67
68 def __del__(self):
69 self.closeshop()
70 def closeshop(self):
71 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
72 self.steptimer.cancel()
73 if json is not None and json.dump is not None:
74 json.dump(self.db, open(self.questionfile, "w"), indent=4, separators=(',',': '))
75
76 def getchan(self):
77 return self.parent.channel(self.chan)
78 def getbot(self):
79 return self.getchan().bot
80
81 def nexthint(self, hintnum):
82 answer = self.hintanswer
83
84 if self.hintstr is None or self.revealpossibilities is None or self.reveal is None:
85 self.hintstr = list(re.sub(r'[a-zA-Z0-9]', '*', answer))
86 self.revealpossibilities = range(''.join(self.hintstr).count('*'))
87 self.reveal = int(''.join(self.hintstr).count('*') * (7/24.0))
88
89 for i in range(self.reveal):
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
96 self.hintsgiven += 1
97
98 if hintnum < HINTNUM:
99 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
100 self.steptimer.start()
101 else:
102 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
103 self.steptimer.start()
104
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']
109 winner = person(0)
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
117 except Exception as e: msg("DERP! %r" % (e))
118
119 self.db['users'] = {}
120 self.db['ranks'] = []
121 stop()
122 self.closeshop()
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
131 self.__init__(self.questionfile, self.parent)
132
133 def nextquestion(self, qskipped=False, iteration=0):
134 if self.gameover == True:
135 return self.doGameOver()
136 if qskipped:
137 self.getbot().msg(self.getchan(), "Fail! The correct answer was: %s" % (self.hintanswer))
138 self.missedquestions += 1
139 else:
140 self.missedquestions = 0
141
142 if isinstance(self.steptimer, threading._Timer):
143 self.steptimer.cancel()
144
145 self.hintstr = None
146 self.hintsgiven = 0
147 self.revealpossibilities = None
148 self.reveal = None
149
150 if self.missedquestions > MAXMISSEDQUESTIONS:
151 stop()
152 self.getbot().msg(self.getchan(), "%d questions unanswered! Stopping the game.")
153
154 if state.nextq is not None:
155 nextq = state.nextq
156 state.nextq = None
157 else:
158 nextq = random.choice(self.db['questions'])
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()
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))
173 self.getbot().msg(self.chan, qtext)
174
175 self.curq = nextq
176
177 if isinstance(self.curq['answer'], basestring): self.hintanswer = self.curq['answer']
178 else: self.hintanswer = random.choice(self.curq['answer'])
179
180 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
181 self.steptimer.start()
182
183 def checkanswer(self, answer):
184 if self.curq is None:
185 return False
186 elif isinstance(self.curq['answer'], basestring):
187 return answer.lower() == self.curq['answer']
188 else: # assume it's a list or something.
189 return answer.lower() in self.curq['answer']
190
191 def addpoint(self, _user, count=1):
192 _user = str(_user)
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'])}
198 self.db['ranks'].append(user)
199
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
205 return self.db['users'][user]['points']
206
207 def points(self, user):
208 user = str(user).lower()
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):
215 user = str(user).lower()
216 if user in self.db['users']:
217 return self.db['users'][user]['rank']+1
218 else:
219 return len(self.db['users'])+1
220
221 def targetuser(self, user):
222 if len(self.db['ranks']) == 0: return "no one is ranked!"
223
224 user = str(user).lower()
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]
231 else:
232 return self.db['ranks'][-1]
233 def targetpoints(self, user):
234 if len(self.db['ranks']) == 0: return 0
235
236 user = str(user).lower()
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']
243 else:
244 return self.db['users'][self.db['ranks'][-1]]['points']
245
246 state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
247
248 @lib.hookchan(state.db['chan'])
249 def trivia_checkanswer(bot, user, chan, *args):
250 line = ' '.join([str(arg) for arg in args])
251 if state.checkanswer(line):
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)))
255 state.nextquestion()
256
257 @lib.hook('points', needchan=False)
258 def cmd_points(bot, user, chan, realtarget, *args):
259 if chan == realtarget: replyto = chan
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
267 @lib.hook('give', clevel=lib.OP, needchan=False)
268 @lib.argsGE(1)
269 def cmd_give(bot, user, chan, realtarget, *args):
270 whoto = args[0]
271 if len(args) > 1:
272 numpoints = int(args[1])
273 else:
274 numpoints = 1
275 balance = state.addpoint(whoto, numpoints)
276
277 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
278
279 @lib.hook('setnext', clevel=lib.OP, needchan=False)
280 @lib.argsGE(1)
281 def cmd_setnext(bot, user, chan, realtarget, *args):
282 line = ' '.join([str(arg) for arg in args])
283 linepieces = line.split('*')
284 if len(linepieces) < 2:
285 bot.msg(user, "Error: need <question>*<answer>")
286 return
287 question = linepieces[0].strip()
288 answer = linepieces[1].strip()
289 state.nextq = {'question':question,'answer':answer}
290 bot.msg(user, "Done.")
291
292 @lib.hook('skip', clevel=lib.KNOWN, needchan=False)
293 def cmd_skip(bot, user, chan, realtarget, *args):
294 state.nextquestion(True)
295
296 @lib.hook('start', needchan=False)
297 def 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
306 @lib.hook('stop', clevel=lib.KNOWN, needchan=False)
307 def cmd_stop(bot, user, chan, realtarget, *args):
308 if stop():
309 bot.msg(state.chan, "Game stopped by %s" % (user))
310 else:
311 bot.msg(user, "Game isn't running.")
312
313 def stop():
314 if state.curq is not None:
315 state.curq = None
316 try:
317 state.steptimer.cancel()
318 except Exception as e:
319 print "!!! steptimer.cancel(): e"
320 return True
321 else:
322 return False
323
324 @lib.hook('rank', needchan=False)
325 def cmd_rank(bot, user, chan, realtarget, *args):
326 if chan == realtarget: replyto = chan
327 else: replyto = user
328
329 if len(args) != 0: who = args[0]
330 else: who = user
331
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)))
333
334 @lib.hook('top10', needchan=False)
335 def cmd_top10(bot, user, chan, realtarget, *args):
336 if len(state.db['ranks']) == 0:
337 return bot.msg(state.db['chan'], "No one is ranked!")
338
339 replylist = []
340 for nick in state.db['ranks'][0:10]:
341 user = state.db['users'][nick]
342 replylist.append("%s (%s)" % (user['realnick'], user['points']))
343 bot.msg(state.db['chan'], ', '.join(replylist))
344
345 @lib.hook('settarget', clevel=lib.MASTER, needchan=False)
346 def 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
353 @lib.hook('triviahelp', needchan=False)
354 def 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)")
368
369
370 def 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
384 def 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]