]> jfr.im git - erebus.git/blob - modules/trivia.py
trivia
[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 # ability to REDUCE users points
9 # dynamic questions
10 # v2
11 # team play
12 # statistics
13
14 HINTTIMER = 2.0
15 HINTNUM = 3
16
17 # module info
18 modinfo = {
19 'author': 'Erebus Team',
20 'license': 'public domain',
21 'compatible': [1], # compatible module API versions
22 'depends': [], # other modules required to work properly?
23 }
24
25 # preamble
26 import modlib
27 lib = modlib.modlib(__name__)
28 def modstart(parent, *args, **kwargs):
29 state.parent = parent
30 return lib.modstart(parent, *args, **kwargs)
31 def modstop(*args, **kwargs):
32 stop()
33 global state
34 del state
35 return lib.modstop(*args, **kwargs)
36
37 # module code
38 import json, random, threading, re
39
40 def findnth(haystack, needle, n): #http://stackoverflow.com/a/1884151
41 parts = haystack.split(needle, n+1)
42 if len(parts)<=n+1:
43 return -1
44 return len(haystack)-len(parts[-1])-len(needle)
45
46 class TriviaState(object):
47 def __init__(self, questionfile):
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.revealpossibilities = None
57
58 def __del__(self):
59 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
60 self.steptimer.cancel()
61 if json is not None and json.dump is not None:
62 json.dump(self.db, open(self.questionfile, "w"), indent=4, separators=(',',': '))
63
64 def getchan(self):
65 return self.parent.channel(self.chan)
66 def getbot(self):
67 return self.getchan().bot
68
69 def nexthint(self, hintnum):
70 if self.hintanswer is None:
71 if isinstance(self.curq['answer'], basestring): self.hintanswer = self.curq['answer']
72 else: self.hintanswer = random.choice(self.curq['answer'])
73 answer = self.hintanswer
74
75 if self.hintstr is None or self.revealpossibilities is None:
76 self.hintstr = list(re.sub(r'[a-zA-Z0-9]', '*', answer))
77 self.revealpossibilities = range(''.join(self.hintstr).count('*'))
78
79 reveal = int(len(self.hintstr) * (7/24.0))
80 for i in range(reveal):
81 revealcount = random.choice(self.revealpossibilities)
82 revealloc = findnth(''.join(self.hintstr), '*', revealcount)
83 self.revealpossibilities.remove(revealcount)
84 self.hintstr[revealloc] = answer[revealloc]
85 self.parent.channel(self.chan).bot.msg(self.chan, "Here's a hint: %s" % (''.join(self.hintstr)))
86
87 if hintnum < HINTNUM:
88 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
89 self.steptimer.start()
90 else:
91 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
92 self.steptimer.start()
93
94 def nextquestion(self, qskipped=False):
95 if qskipped:
96 self.getbot().msg(self.getchan(), "Fail! The correct answer was: %s" % (self.hintanswer))
97
98 if isinstance(self.steptimer, threading._Timer):
99 self.steptimer.cancel()
100 self.hintstr = None
101 self.hintanswer = None
102 self.revealpossibilities = None
103
104
105 if state.nextq is not None:
106 nextq = state.nextq
107 self.curq = nextq
108 state.nextq = None
109 else:
110 nextq = random.choice(self.db['questions'])
111 self.curq = nextq
112
113 qtext = "\00300,01Next up: "
114 qary = nextq['question'].split(None)
115 for qword in qary:
116 qtext += "\00300,01"+qword+"\00301,01"+chr(random.randrange(32,126))
117 self.getbot().msg(self.chan, qtext)
118
119 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
120 self.steptimer.start()
121
122 def checkanswer(self, answer):
123 if self.curq is None:
124 return False
125 elif isinstance(self.curq['answer'], basestring):
126 return answer.lower() == self.curq['answer']
127 else: # assume it's a list or something.
128 return answer.lower() in self.curq['answer']
129
130 def addpoint(self, _user, count=1):
131 _user = str(_user)
132 user = _user.lower()
133 if user in self.db['users']:
134 self.db['users'][user]['points'] += count
135 else:
136 self.db['users'][user] = {'points': count, 'realnick': _user, 'rank': len(self.db['ranks'])}
137 self.db['ranks'].append(user)
138
139 oldrank = self.db['users'][user]['rank']
140 while oldrank != 0:
141 nextperson = self.db['ranks'][oldrank-1]
142 if self.db['users'][user]['points'] > self.db['users'][nextperson]['points']:
143 self.db['ranks'][oldrank-1] = user
144 self.db['users'][user]['rank'] = oldrank-1
145 self.db['ranks'][oldrank] = nextperson
146 self.db['users'][nextperson]['rank'] = oldrank
147 oldrank = oldrank-1
148 else:
149 break
150 return self.db['users'][user]['points']
151
152 def points(self, user):
153 user = str(user).lower()
154 if user in self.db['users']:
155 return self.db['users'][user]['points']
156 else:
157 return 0
158
159 def rank(self, user):
160 user = str(user).lower()
161 if user in self.db['users']:
162 return self.db['users'][user]['rank']+1
163 else:
164 return len(self.db['users'])+1
165
166 def targetuser(self, user):
167 user = str(user).lower()
168 if user in self.db['users']:
169 rank = self.db['users'][user]['rank']
170 if rank == 0:
171 return "you're in the lead!"
172 else:
173 return self.db['ranks'][rank-1]
174 else:
175 return self.db['ranks'][-1]
176 def targetpoints(self, user):
177 user = str(user).lower()
178 if user in self.db['users']:
179 rank = self.db['users'][user]['rank']
180 if rank == 0:
181 return "N/A"
182 else:
183 return self.db['users'][self.db['ranks'][rank-1]]['points']
184 else:
185 return self.db['users'][self.db['ranks'][-1]]['points']
186
187 state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
188
189 @lib.hookchan(state.db['chan'])
190 def trivia_checkanswer(bot, user, chan, *args):
191 line = ' '.join([str(arg) for arg in args])
192 if state.checkanswer(line):
193 bot.msg(chan, "\00308%s\003 has it! The answer was \00308%s\003. Current points: %d. Rank: %d. Target: %s (%s)." % (user, line, state.addpoint(user), state.rank(user), state.targetuser(user), state.targetpoints(user)))
194 state.nextquestion()
195
196 @lib.hook('points')
197 def cmd_points(bot, user, chan, realtarget, *args):
198 if chan == realtarget: replyto = chan
199 else: replyto = user
200
201 if len(args) != 0: who = args[0]
202 else: who = user
203
204 bot.msg(replyto, "%s has %d points." % (who, state.points(who)))
205
206 @lib.hook('give', clevel=lib.OP)
207 @lib.argsGE(1)
208 def cmd_give(bot, user, chan, realtarget, *args):
209 whoto = args[0]
210 if len(args) > 1:
211 numpoints = int(args[1])
212 else:
213 numpoints = 1
214 balance = state.addpoint(whoto, numpoints)
215
216 if numpoints < 0:
217 state.db['ranks'].sort(key=lambda nick: state.db['users'][nick]['points'], reverse=True)
218 bot.msg(chan, "%s gave %s %d points. New balance: %d" % (user, whoto, numpoints, balance))
219
220 @lib.hook('setnext', clevel=lib.OP)
221 @lib.argsGE(1)
222 def cmd_setnext(bot, user, chan, realtarget, *args):
223 line = ' '.join([str(arg) for arg in args])
224 linepieces = line.split('*')
225 question = linepieces[0].strip()
226 answer = linepieces[1].strip()
227 state.nextq = {'question':question,'answer':answer}
228 bot.msg(user, "Done.")
229
230 @lib.hook('skip', clevel=lib.KNOWN)
231 def cmd_skip(bot, user, chan, realtarget, *args):
232 state.nextquestion()
233
234 @lib.hook('start')
235 def cmd_start(bot, user, chan, realtarget, *args):
236 if chan == realtarget: replyto = chan
237 else: replyto = user
238
239 if state.curq is None:
240 state.nextquestion()
241 else:
242 bot.msg(replyto, "Game is already started!")
243
244 @lib.hook('stop', clevel=lib.KNOWN)
245 def cmd_stop(bot, user, chan, realtarget, *args):
246 if stop():
247 bot.msg(state.chan, "Game stopped by %s" % (user))
248 else:
249 bot.msg(user, "Game isn't running.")
250
251 def stop():
252 if state.curq is not None:
253 state.curq = None
254 if isinstance(state.steptimer, threading._Timer):
255 state.steptimer.cancel()
256 return True
257 else:
258 return False
259
260 @lib.hook('rank')
261 def cmd_rank(bot, user, chan, realtarget, *args):
262 if chan == realtarget: replyto = chan
263 else: replyto = user
264
265 if len(args) != 0: who = args[0]
266 else: who = user
267
268 bot.msg(replyto, "%s is in %d place. Target is: %s (%d points)." % (who, state.rank(who), state.targetuser(who), state.targetpoints(who)))
269
270 @lib.hook('top10')
271 def cmd_top10(bot, user, chan, realtarget, *args):
272 if chan == realtarget: replyto = chan
273 else: replyto = user
274
275 replylist = []
276 for nick in state.db['ranks'][0:10]:
277 user = state.db['users'][nick]
278 replylist.append("%s (%d)" % (user['realnick'], user['points']))
279 bot.msg(replyto, ', '.join(replylist))