1 # Erebus IRC bot - Author: Erebus Team
3 # This file is released into the public domain; see http://unlicense.org/
6 # stop game after X unanswered questions
17 'author': 'Erebus Team',
18 'license': 'public domain',
19 'compatible': [1], # compatible module API versions
20 'depends': [], # other modules required to work properly?
25 lib
= modlib
.modlib(__name__
)
26 def modstart(parent
, *args
, **kwargs
):
28 return lib
.modstart(parent
, *args
, **kwargs
)
29 def modstop(*args
, **kwargs
):
34 return lib
.modstop(*args
, **kwargs
)
37 import json
, random
, threading
, re
, time
39 def findnth(haystack
, needle
, n
): #http://stackoverflow.com/a/1884151
40 parts
= haystack
.split(needle
, n
+1)
43 return len(haystack
)-len(parts
[-1])-len(needle
)
45 class TriviaState(object):
46 def __init__(self
, questionfile
, parent
=None):
48 self
.questionfile
= questionfile
49 self
.db
= json
.load(open(questionfile
, "r"))
50 self
.chan
= self
.db
['chan']
55 self
.hintanswer
= None
57 self
.revealpossibilities
= None
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
=(',',': '))
69 return self
.parent
.channel(self
.chan
)
71 return self
.getchan().bot
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
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))
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
)))
94 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[hintnum
+1])
95 self
.steptimer
.start()
97 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nextquestion
, args
=[True])
98 self
.steptimer
.start()
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']
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'] = []
117 self
.__init
__(self
.questionfile
, self
.parent
)
119 def nextquestion(self
, qskipped
=False, iteration
=0):
120 if self
.gameover
== True:
121 return self
.doGameOver()
123 self
.getbot().msg(self
.getchan(), "Fail! The correct answer was: %s" % (self
.hintanswer
))
125 if isinstance(self
.steptimer
, threading
._Timer
):
126 self
.steptimer
.cancel()
128 self
.hintanswer
= None
130 self
.revealpossibilities
= None
134 if state
.nextq
is not None:
138 nextq
= random
.choice(self
.db
['questions'])
140 if nextq
['question'][0] == "!":
141 nextq
= specialQuestion(nextq
)
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()
147 nextq
['answer'] = nextq
['answer'].lower()
149 qtext
= "\00300,01Next up: "
150 qary
= nextq
['question'].split(None)
152 qtext
+= "\00300,01"+qword
+"\00301,01"+chr(random
.randrange(32,126))
153 self
.getbot().msg(self
.chan
, qtext
)
157 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[1])
158 self
.steptimer
.start()
160 def checkanswer(self
, answer
):
161 if self
.curq
is None:
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']
168 def addpoint(self
, _user
, count
=1):
171 if user
in self
.db
['users']:
172 self
.db
['users'][user
]['points'] += count
174 self
.db
['users'][user
] = {'points': count, 'realnick': _user, 'rank': len(self.db['ranks'])}
175 self
.db
['ranks'].append(user
)
177 state
.db
['ranks'].sort(key
=lambda nick
: state
.db
['users'][nick
]['points'], reverse
=True)
179 if self
.db
['users'][user
]['points'] >= state
.db
['target']:
182 return self
.db
['users'][user
]['points']
184 def points(self
, user
):
185 user
= str(user
).lower()
186 if user
in self
.db
['users']:
187 return self
.db
['users'][user
]['points']
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
196 return len(self
.db
['users'])+1
198 def targetuser(self
, user
):
199 if len(self
.db
['ranks']) == 0: return "no one is ranked!"
201 user
= str(user
).lower()
202 if user
in self
.db
['users']:
203 rank
= self
.db
['users'][user
]['rank']
205 return "you're in the lead!"
207 return self
.db
['ranks'][rank
-1]
209 return self
.db
['ranks'][-1]
210 def targetpoints(self
, user
):
211 if len(self
.db
['ranks']) == 0: return 0
213 user
= str(user
).lower()
214 if user
in self
.db
['users']:
215 rank
= self
.db
['users'][user
]['rank']
219 return self
.db
['users'][self
.db
['ranks'][rank
-1]]['points']
221 return self
.db
['users'][self
.db
['ranks'][-1]]['points']
223 state
= TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
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
)))
234 @lib.hook('points', needchan
=False)
235 def cmd_points(bot
, user
, chan
, realtarget
, *args
):
236 if chan
== realtarget
: replyto
= chan
239 if len(args
) != 0: who
= args
[0]
242 bot
.msg(replyto
, "%s has %d points." % (who
, state
.points(who
)))
244 @lib.hook('give', clevel
=lib
.OP
, needchan
=False)
246 def cmd_give(bot
, user
, chan
, realtarget
, *args
):
249 numpoints
= int(args
[1])
252 balance
= state
.addpoint(whoto
, numpoints
)
254 bot
.msg(chan
, "%s gave %s %d points. New balance: %d" % (user
, whoto
, numpoints
, balance
))
256 @lib.hook('setnext', clevel
=lib
.OP
, needchan
=False)
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>")
264 question
= linepieces
[0].strip()
265 answer
= linepieces
[1].strip()
266 state
.nextq
= {'question':question,'answer':answer}
267 bot
.msg(user
, "Done.")
269 @lib.hook('skip', clevel
=lib
.KNOWN
, needchan
=False)
270 def cmd_skip(bot
, user
, chan
, realtarget
, *args
):
273 @lib.hook('start', needchan
=False)
274 def cmd_start(bot
, user
, chan
, realtarget
, *args
):
275 if chan
== realtarget
: replyto
= chan
278 if state
.curq
is None:
281 bot
.msg(replyto
, "Game is already started!")
283 @lib.hook('stop', clevel
=lib
.KNOWN
, needchan
=False)
284 def cmd_stop(bot
, user
, chan
, realtarget
, *args
):
286 bot
.msg(state
.chan
, "Game stopped by %s" % (user
))
288 bot
.msg(user
, "Game isn't running.")
291 if state
.curq
is not None:
293 if isinstance(state
.steptimer
, threading
._Timer
):
294 state
.steptimer
.cancel()
299 @lib.hook('rank', needchan
=False)
300 def cmd_rank(bot
, user
, chan
, realtarget
, *args
):
301 if chan
== realtarget
: replyto
= chan
304 if len(args
) != 0: who
= args
[0]
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
)))
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!")
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
))
320 @lib.hook('settarget', clevel
=lib
.MASTER
, needchan
=False)
321 def cmd_settarget(bot
, user
, chan
, realtarget
, *args
):
323 state
.db
['target'] = int(args
[0])
324 bot
.msg(state
.db
['chan'], "Target has been changed to %s points!" % (state
.db
['target']))
326 bot
.msg(user
, "Failed to set target.")
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")
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)")
345 def specialQuestion(oldq
):
346 newq
= {'question': oldq['question'], 'answer': oldq['answer']}
347 qtype
= oldq
['question'].upper()
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
)
361 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
362 "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
363 "sixteen", "seventeen", "eighteen", "nineteen", "twenty"