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