]> jfr.im git - erebus.git/blame - modules/trivia.py
add ctcp version reply
[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
77c61775 12HINTTIMER = 15.0
fadbf980 13HINTNUM = 3
14
80d02bd8 15# module info
16modinfo = {
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
24import modlib
25lib = modlib.modlib(__name__)
26def modstart(parent, *args, **kwargs):
9557ee54 27 state.parent = parent
80d02bd8 28 return lib.modstart(parent, *args, **kwargs)
29def modstop(*args, **kwargs):
fadbf980 30 stop()
77c61775 31 state.closeshop()
80d02bd8 32 global state
33 del state
34 return lib.modstop(*args, **kwargs)
35
36# module code
b5c89dfb 37import json, random, threading, re, time
b16b8c05 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)
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
80d02bd8 59
60 def __del__(self):
77c61775 61 self.closeshop()
62 def closeshop(self):
b16b8c05 63 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
64 self.steptimer.cancel()
c695f740 65 if json is not None and json.dump is not None:
66 json.dump(self.db, open(self.questionfile, "w"), indent=4, separators=(',',': '))
80d02bd8 67
fadbf980 68 def getchan(self):
69 return self.parent.channel(self.chan)
70 def getbot(self):
71 return self.getchan().bot
72
b16b8c05 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
b5c89dfb 79 if self.hintstr is None or self.revealpossibilities is None or self.reveal is None:
b16b8c05 80 self.hintstr = list(re.sub(r'[a-zA-Z0-9]', '*', answer))
81 self.revealpossibilities = range(''.join(self.hintstr).count('*'))
b5c89dfb 82 self.reveal = int(''.join(self.hintstr).count('*') * (7/24.0))
b16b8c05 83
b5c89dfb 84 for i in range(self.reveal):
b16b8c05 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
77c61775 91 self.hintsgiven += 1
92
fadbf980 93 if hintnum < HINTNUM:
94 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
b16b8c05 95 self.steptimer.start()
96 else:
fadbf980 97 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
b16b8c05 98 self.steptimer.start()
99
77c61775 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
b5c89dfb 119 def nextquestion(self, qskipped=False, iteration=0):
77c61775 120 if self.gameover == True:
121 return self.doGameOver()
fadbf980 122 if qskipped:
123 self.getbot().msg(self.getchan(), "Fail! The correct answer was: %s" % (self.hintanswer))
124
b16b8c05 125 if isinstance(self.steptimer, threading._Timer):
126 self.steptimer.cancel()
127 self.hintstr = None
128 self.hintanswer = None
77c61775 129 self.hintsgiven = 0
b16b8c05 130 self.revealpossibilities = None
b5c89dfb 131 self.reveal = None
b16b8c05 132
133
c695f740 134 if state.nextq is not None:
135 nextq = state.nextq
c695f740 136 state.nextq = None
137 else:
138 nextq = random.choice(self.db['questions'])
b5c89dfb 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()
c695f740 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))
fadbf980 153 self.getbot().msg(self.chan, qtext)
80d02bd8 154
b5c89dfb 155 self.curq = nextq
156
fadbf980 157 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
b16b8c05 158 self.steptimer.start()
159
80d02bd8 160 def checkanswer(self, answer):
9557ee54 161 if self.curq is None:
162 return False
163 elif isinstance(self.curq['answer'], basestring):
80d02bd8 164 return answer.lower() == self.curq['answer']
165 else: # assume it's a list or something.
166 return answer.lower() in self.curq['answer']
77c61775 167
80d02bd8 168 def addpoint(self, _user, count=1):
9557ee54 169 _user = str(_user)
80d02bd8 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'])}
9557ee54 175 self.db['ranks'].append(user)
80d02bd8 176
77c61775 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
80d02bd8 182 return self.db['users'][user]['points']
183
184 def points(self, user):
9557ee54 185 user = str(user).lower()
80d02bd8 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):
c695f740 192 user = str(user).lower()
fadbf980 193 if user in self.db['users']:
194 return self.db['users'][user]['rank']+1
195 else:
196 return len(self.db['users'])+1
77c61775 197
c695f740 198 def targetuser(self, user):
77c61775 199 if len(self.db['ranks']) == 0: return "no one is ranked!"
200
c695f740 201 user = str(user).lower()
fadbf980 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]
c695f740 208 else:
fadbf980 209 return self.db['ranks'][-1]
c695f740 210 def targetpoints(self, user):
77c61775 211 if len(self.db['ranks']) == 0: return 0
212
c695f740 213 user = str(user).lower()
fadbf980 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']
c695f740 220 else:
fadbf980 221 return self.db['users'][self.db['ranks'][-1]]['points']
80d02bd8 222
c695f740 223state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
9557ee54 224
80d02bd8 225@lib.hookchan(state.db['chan'])
226def trivia_checkanswer(bot, user, chan, *args):
80d02bd8 227 line = ' '.join([str(arg) for arg in args])
228 if state.checkanswer(line):
77c61775 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)))
9557ee54 232 state.nextquestion()
80d02bd8 233
f6252f1c 234@lib.hook('points', needchan=False)
80d02bd8 235def cmd_points(bot, user, chan, realtarget, *args):
c695f740 236 if chan == realtarget: replyto = chan
80d02bd8 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
f6252f1c 244@lib.hook('give', clevel=lib.OP, needchan=False)
80d02bd8 245@lib.argsGE(1)
246def cmd_give(bot, user, chan, realtarget, *args):
80d02bd8 247 whoto = args[0]
c695f740 248 if len(args) > 1:
249 numpoints = int(args[1])
250 else:
251 numpoints = 1
252 balance = state.addpoint(whoto, numpoints)
fadbf980 253
c695f740 254 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
255
f6252f1c 256@lib.hook('setnext', clevel=lib.OP, needchan=False)
c695f740 257@lib.argsGE(1)
258def cmd_setnext(bot, user, chan, realtarget, *args):
259 line = ' '.join([str(arg) for arg in args])
260 linepieces = line.split('*')
b5c89dfb 261 if len(linepieces) < 2:
262 bot.msg(user, "Error: need <question>*<answer>")
263 return
c695f740 264 question = linepieces[0].strip()
265 answer = linepieces[1].strip()
266 state.nextq = {'question':question,'answer':answer}
267 bot.msg(user, "Done.")
268
f6252f1c 269@lib.hook('skip', clevel=lib.KNOWN, needchan=False)
c695f740 270def cmd_skip(bot, user, chan, realtarget, *args):
271 state.nextquestion()
272
f6252f1c 273@lib.hook('start', needchan=False)
c695f740 274def 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
f6252f1c 283@lib.hook('stop', clevel=lib.KNOWN, needchan=False)
c695f740 284def cmd_stop(bot, user, chan, realtarget, *args):
fadbf980 285 if stop():
286 bot.msg(state.chan, "Game stopped by %s" % (user))
287 else:
288 bot.msg(user, "Game isn't running.")
c695f740 289
fadbf980 290def stop():
c695f740 291 if state.curq is not None:
292 state.curq = None
b16b8c05 293 if isinstance(state.steptimer, threading._Timer):
294 state.steptimer.cancel()
fadbf980 295 return True
c695f740 296 else:
fadbf980 297 return False
80d02bd8 298
f6252f1c 299@lib.hook('rank', needchan=False)
80d02bd8 300def cmd_rank(bot, user, chan, realtarget, *args):
c695f740 301 if chan == realtarget: replyto = chan
80d02bd8 302 else: replyto = user
303
c695f740 304 if len(args) != 0: who = args[0]
305 else: who = user
306
77c61775 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)))
fadbf980 308
f6252f1c 309@lib.hook('top10', needchan=False)
fadbf980 310def cmd_top10(bot, user, chan, realtarget, *args):
77c61775 311 if len(state.db['ranks']) == 0:
312 return bot.msg(state.db['chan'], "No one is ranked!")
fadbf980 313
314 replylist = []
315 for nick in state.db['ranks'][0:10]:
316 user = state.db['users'][nick]
77c61775 317 replylist.append("%s (%s)" % (user['realnick'], user['points']))
318 bot.msg(state.db['chan'], ', '.join(replylist))
319
f6252f1c 320@lib.hook('settarget', clevel=lib.MASTER, needchan=False)
77c61775 321def 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
f6252f1c 328@lib.hook('triviahelp', needchan=False)
77c61775 329def 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)")
b5c89dfb 343
344
345def 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
359def 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]