]> jfr.im git - erebus.git/blame - modules/trivia.py
trivia v1 done
[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
8# ability to REDUCE users points
9# dynamic questions
10# v2
11# team play
12# statistics
13
fadbf980 14HINTTIMER = 2.0
15HINTNUM = 3
16
80d02bd8 17# module info
18modinfo = {
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
26import modlib
27lib = modlib.modlib(__name__)
28def modstart(parent, *args, **kwargs):
9557ee54 29 state.parent = parent
80d02bd8 30 return lib.modstart(parent, *args, **kwargs)
31def modstop(*args, **kwargs):
fadbf980 32 stop()
80d02bd8 33 global state
34 del state
35 return lib.modstop(*args, **kwargs)
36
37# module code
b16b8c05 38import json, random, threading, re
39
40def 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)
80d02bd8 45
46class TriviaState(object):
9557ee54 47 def __init__(self, questionfile):
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
56 self.revealpossibilities = None
80d02bd8 57
58 def __del__(self):
b16b8c05 59 if threading is not None and threading._Timer is not None and isinstance(self.steptimer, threading._Timer):
60 self.steptimer.cancel()
c695f740 61 if json is not None and json.dump is not None:
62 json.dump(self.db, open(self.questionfile, "w"), indent=4, separators=(',',': '))
80d02bd8 63
fadbf980 64 def getchan(self):
65 return self.parent.channel(self.chan)
66 def getbot(self):
67 return self.getchan().bot
68
b16b8c05 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
fadbf980 87 if hintnum < HINTNUM:
88 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[hintnum+1])
b16b8c05 89 self.steptimer.start()
90 else:
fadbf980 91 self.steptimer = threading.Timer(HINTTIMER, self.nextquestion, args=[True])
b16b8c05 92 self.steptimer.start()
93
fadbf980 94 def nextquestion(self, qskipped=False):
95 if qskipped:
96 self.getbot().msg(self.getchan(), "Fail! The correct answer was: %s" % (self.hintanswer))
97
b16b8c05 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
c695f740 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))
fadbf980 117 self.getbot().msg(self.chan, qtext)
80d02bd8 118
fadbf980 119 self.steptimer = threading.Timer(HINTTIMER, self.nexthint, args=[1])
b16b8c05 120 self.steptimer.start()
121
80d02bd8 122 def checkanswer(self, answer):
9557ee54 123 if self.curq is None:
124 return False
125 elif isinstance(self.curq['answer'], basestring):
80d02bd8 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):
9557ee54 131 _user = str(_user)
80d02bd8 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'])}
9557ee54 137 self.db['ranks'].append(user)
80d02bd8 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
c695f740 144 self.db['users'][user]['rank'] = oldrank-1
80d02bd8 145 self.db['ranks'][oldrank] = nextperson
c695f740 146 self.db['users'][nextperson]['rank'] = oldrank
80d02bd8 147 oldrank = oldrank-1
148 else:
149 break
150 return self.db['users'][user]['points']
151
152 def points(self, user):
9557ee54 153 user = str(user).lower()
80d02bd8 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):
c695f740 160 user = str(user).lower()
fadbf980 161 if user in self.db['users']:
162 return self.db['users'][user]['rank']+1
163 else:
164 return len(self.db['users'])+1
80d02bd8 165
c695f740 166 def targetuser(self, user):
167 user = str(user).lower()
fadbf980 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]
c695f740 174 else:
fadbf980 175 return self.db['ranks'][-1]
c695f740 176 def targetpoints(self, user):
177 user = str(user).lower()
fadbf980 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']
c695f740 184 else:
fadbf980 185 return self.db['users'][self.db['ranks'][-1]]['points']
80d02bd8 186
c695f740 187state = TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
9557ee54 188
80d02bd8 189@lib.hookchan(state.db['chan'])
190def trivia_checkanswer(bot, user, chan, *args):
80d02bd8 191 line = ' '.join([str(arg) for arg in args])
192 if state.checkanswer(line):
c695f740 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)))
9557ee54 194 state.nextquestion()
80d02bd8 195
196@lib.hook('points')
197def cmd_points(bot, user, chan, realtarget, *args):
c695f740 198 if chan == realtarget: replyto = chan
80d02bd8 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)
208def cmd_give(bot, user, chan, realtarget, *args):
80d02bd8 209 whoto = args[0]
c695f740 210 if len(args) > 1:
211 numpoints = int(args[1])
212 else:
213 numpoints = 1
214 balance = state.addpoint(whoto, numpoints)
fadbf980 215
216 if numpoints < 0:
217 state.db['ranks'].sort(key=lambda nick: state.db['users'][nick]['points'], reverse=True)
c695f740 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)
222def 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)
231def cmd_skip(bot, user, chan, realtarget, *args):
232 state.nextquestion()
233
234@lib.hook('start')
235def 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)
245def cmd_stop(bot, user, chan, realtarget, *args):
fadbf980 246 if stop():
247 bot.msg(state.chan, "Game stopped by %s" % (user))
248 else:
249 bot.msg(user, "Game isn't running.")
c695f740 250
fadbf980 251def stop():
c695f740 252 if state.curq is not None:
253 state.curq = None
b16b8c05 254 if isinstance(state.steptimer, threading._Timer):
255 state.steptimer.cancel()
fadbf980 256 return True
c695f740 257 else:
fadbf980 258 return False
80d02bd8 259
260@lib.hook('rank')
80d02bd8 261def cmd_rank(bot, user, chan, realtarget, *args):
c695f740 262 if chan == realtarget: replyto = chan
80d02bd8 263 else: replyto = user
264
c695f740 265 if len(args) != 0: who = args[0]
266 else: who = user
267
fadbf980 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')
271def 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))