]> jfr.im git - erebus.git/blame_incremental - modules/trivia.py
update todo
[erebus.git] / modules / trivia.py
... / ...
CommitLineData
1# Erebus IRC bot - Author: Erebus Team
2# trivia module
3# This file is released into the public domain; see http://unlicense.org/
4
5HINTTIMER = 10.0 # how long between hints
6HINTNUM = 3 # how many hints to give
7MAXMISSEDQUESTIONS = 25 # how many missed questions before game stops
8
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):
21 state.parent = parent
22 return lib.modstart(parent, *args, **kwargs)
23def modstop(*args, **kwargs):
24 global state
25 stop()
26 state.closeshop()
27 del state
28 return lib.modstop(*args, **kwargs)
29
30# module code
31import json, random, threading, re, time
32
33try:
34 import twitter
35 hastwitter = True
36except ImportError:
37 hastwitter = False
38
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)
44
45class TriviaState(object):
46 def __init__(self, questionfile, parent=None):
47 self.parent = parent
48 self.questionfile = questionfile
49 self.db = json.load(open(questionfile, "r"))
50 self.chan = self.db['chan']
51 self.curq = None
52 self.nextq = None
53 self.steptimer = None
54 self.hintstr = None
55 self.hintanswer = None
56 self.hintsgiven = 0
57 self.revealpossibilities = None
58 self.gameover = False
59 self.missedquestions = 0
60
61 def __del__(self):
62 self.closeshop()
63 def closeshop(self):
64 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
65 self.steptimer.cancel()
66 if json is not None and json.dump is not None:
67 json.dump(self.db, open(self.questionfile, "w"))#, indent=4, separators=(',', ': '))
68
69 def getchan(self):
70 return self.parent.channel(self.chan)
71 def getbot(self):
72 return self.getchan().bot
73
74 def nexthint(self, hintnum):
75 answer = self.hintanswer
76
77 if self.hintstr is None or self.revealpossibilities is None or self.reveal is None:
78 self.hintstr = list(re.sub(r'[a-zA-Z0-9]', '*', answer))
79 self.revealpossibilities = range(''.join(self.hintstr).count('*'))
80 self.reveal = int(''.join(self.hintstr).count('*') * (7/24.0))
81
82 for i in range(self.reveal):
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, "\00304,01Here's a hint: %s" % (''.join(self.hintstr)))
88
89 self.hintsgiven += 1
90
91 if hintnum < HINTNUM:
92 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
93 self.steptimer.start()
94 else:
95 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
96 self.steptimer.start()
97
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']
102 winner = person(0)
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
110 except Exception as e: msg("DERP! %r" % (e))
111
112 self.db['users'] = {}
113 self.db['ranks'] = []
114 stop()
115 self.closeshop()
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
124 self.__init__(self.questionfile, self.parent)
125
126 def nextquestion(self, qskipped=False, iteration=0):
127 if self.gameover == True:
128 return self.doGameOver()
129 if qskipped:
130 self.getbot().msg(self.getchan(), "\00304Fail! The correct answer was: %s" % (self.hintanswer))
131 self.missedquestions += 1
132 else:
133 self.missedquestions = 0
134
135 if isinstance(self.steptimer, threading._Timer):
136 self.steptimer.cancel()
137
138 self.hintstr = None
139 self.hintsgiven = 0
140 self.revealpossibilities = None
141 self.reveal = None
142
143 if self.missedquestions > MAXMISSEDQUESTIONS:
144 stop()
145 self.getbot().msg(self.getchan(), "%d questions unanswered! Stopping the game.")
146
147 if state.nextq is not None:
148 nextq = state.nextq
149 state.nextq = None
150 else:
151 nextq = random.choice(self.db['questions'])
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()
161
162 qtext = "\00304,01Next up: "
163 qary = nextq['question'].split(None)
164 for qword in qary:
165 qtext += "\00304,01"+qword+"\00301,01"+chr(random.randrange(0x61,0x7A)) #a-z
166 self.getbot().msg(self.chan, qtext)
167
168 self.curq = nextq
169
170 if isinstance(self.curq['answer'], basestring): self.hintanswer = self.curq['answer']
171 else: self.hintanswer = random.choice(self.curq['answer'])
172
173 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
174 self.steptimer.start()
175
176 def checkanswer(self, answer):
177 if self.curq is None:
178 return False
179 elif isinstance(self.curq['answer'], basestring):
180 return answer.lower() == self.curq['answer']
181 else: # assume it's a list or something.
182 return answer.lower() in self.curq['answer']
183
184 def addpoint(self, _user, count=1):
185 _user = str(_user)
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'])}
191 self.db['ranks'].append(user)
192
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
197
198 if self.db['users'][user]['points'] >= state.db['target']:
199 self.gameover = True
200
201 return self.db['users'][user]['points']
202
203 def points(self, user):
204 user = str(user).lower()
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):
211 user = str(user).lower()
212 if user in self.db['users']:
213 return self.db['users'][user]['rank']+1
214 else:
215 return len(self.db['users'])+1
216
217 def targetuser(self, user):
218 if len(self.db['ranks']) == 0: return "no one is ranked!"
219
220 user = str(user).lower()
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]
227 else:
228 return self.db['ranks'][-1]
229 def targetpoints(self, user):
230 if len(self.db['ranks']) == 0: return 0
231
232 user = str(user).lower()
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']
239 else:
240 return self.db['users'][self.db['ranks'][-1]]['points']
241
242state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
243
244@lib.hookchan(state.db['chan'])
245def trivia_checkanswer(bot, user, chan, *args):
246 line = ' '.join([str(arg) for arg in args])
247 if state.checkanswer(line):
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)))
249 if state.hintsgiven == 0:
250 bot.msg(chan, "\00312%s\003 got an extra point for getting it before the hints! New score: %d." % (user, state.addpoint(user)))
251 state.nextquestion()
252
253@lib.hook('points', needchan=False)
254def cmd_points(bot, user, chan, realtarget, *args):
255 if chan == realtarget: replyto = chan
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
263@lib.hook('give', clevel=lib.OP, needchan=False)
264@lib.argsGE(1)
265def cmd_give(bot, user, chan, realtarget, *args):
266 whoto = args[0]
267 if len(args) > 1:
268 numpoints = int(args[1])
269 else:
270 numpoints = 1
271 balance = state.addpoint(whoto, numpoints)
272
273 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
274
275@lib.hook('setnext', clevel=lib.OP, needchan=False)
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('*')
280 if len(linepieces) < 2:
281 bot.msg(user, "Error: need <question>*<answer>")
282 return
283 question = linepieces[0].strip()
284 answer = linepieces[1].strip()
285 state.nextq = {'question':question,'answer':answer}
286 bot.msg(user, "Done.")
287
288@lib.hook('skip', clevel=lib.KNOWN, needchan=False)
289def cmd_skip(bot, user, chan, realtarget, *args):
290 state.nextquestion(True)
291
292@lib.hook('start', needchan=False)
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
302#FIXME @lib.hook('stop', clevel=lib.KNOWN, needchan=False)
303@lib.hook('stop', needchan=False) #FIXME
304def cmd_stop(bot, user, chan, realtarget, *args):
305 if stop():
306 bot.msg(state.chan, "Game stopped by %s" % (user))
307 else:
308 bot.msg(user, "Game isn't running.")
309
310def stop():
311 if state.curq is not None:
312 state.curq = None
313 try:
314 state.steptimer.cancel()
315 except Exception as e:
316 print "!!! steptimer.cancel(): e"
317 return True
318 else:
319 return False
320
321@lib.hook('rank', needchan=False)
322def cmd_rank(bot, user, chan, realtarget, *args):
323 if chan == realtarget: replyto = chan
324 else: replyto = user
325
326 if len(args) != 0: who = args[0]
327 else: who = user
328
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)))
330
331@lib.hook('top10', needchan=False)
332def cmd_top10(bot, user, chan, realtarget, *args):
333 if len(state.db['ranks']) == 0:
334 return bot.msg(state.db['chan'], "No one is ranked!")
335
336 replylist = []
337 for nick in state.db['ranks'][0:10]:
338 user = state.db['users'][nick]
339 replylist.append("%s (%s)" % (user['realnick'], user['points']))
340 bot.msg(state.db['chan'], ', '.join(replylist))
341
342@lib.hook('settarget', clevel=lib.MASTER, needchan=False)
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
350@lib.hook('triviahelp', needchan=False)
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)")
365
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
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]