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
12 HINTTIMER
= 15.0 # how long between hints
13 HINTNUM
= 3 # how many hints to give
14 MAXMISSEDQUESTIONS
= 5 # how many missed questions before game stops
18 'author': 'Erebus Team',
19 'license': 'public domain',
20 'compatible': [1], # compatible module API versions
21 'depends': [], # other modules required to work properly?
26 lib
= modlib
.modlib(__name__
)
27 def modstart(parent
, *args
, **kwargs
):
29 return lib
.modstart(parent
, *args
, **kwargs
)
30 def modstop(*args
, **kwargs
):
35 return lib
.modstop(*args
, **kwargs
)
38 import json
, random
, threading
, re
, time
46 def findnth(haystack
, needle
, n
): #http://stackoverflow.com/a/1884151
47 parts
= haystack
.split(needle
, n
+1)
50 return len(haystack
)-len(parts
[-1])-len(needle
)
52 class TriviaState(object):
53 def __init__(self
, questionfile
, parent
=None):
55 self
.questionfile
= questionfile
56 self
.db
= json
.load(open(questionfile
, "r"))
57 self
.chan
= self
.db
['chan']
62 self
.hintanswer
= None
64 self
.revealpossibilities
= None
66 self
.missedquestions
= 0
71 if threading
is not None and threading
._Timer
is not None and isinstance(self
.steptimer
, threading
._Timer
):
72 self
.steptimer
.cancel()
73 if json
is not None and json
.dump
is not None:
74 json
.dump(self
.db
, open(self
.questionfile
, "w"), indent
=4, separators
=(',',': '))
77 return self
.parent
.channel(self
.chan
)
79 return self
.getchan().bot
81 def nexthint(self
, hintnum
):
82 answer
= self
.hintanswer
84 if self
.hintstr
is None or self
.revealpossibilities
is None or self
.reveal
is None:
85 self
.hintstr
= list(re
.sub(r
'[a-zA-Z0-9]', '*', answer
))
86 self
.revealpossibilities
= range(''.join(self
.hintstr
).count('*'))
87 self
.reveal
= int(''.join(self
.hintstr
).count('*') * (7/24.0))
89 for i
in range(self
.reveal
):
90 revealcount
= random
.choice(self
.revealpossibilities
)
91 revealloc
= findnth(''.join(self
.hintstr
), '*', revealcount
)
92 self
.revealpossibilities
.remove(revealcount
)
93 self
.hintstr
[revealloc
] = answer
[revealloc
]
94 self
.parent
.channel(self
.chan
).bot
.msg(self
.chan
, "Here's a hint: %s" % (''.join(self
.hintstr
)))
99 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[hintnum
+1])
100 self
.steptimer
.start()
102 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nextquestion
, args
=[True])
103 self
.steptimer
.start()
105 def doGameOver(self
):
106 def msg(line
): self
.getbot().msg(self
.getchan(), line
)
107 def person(num
): return self
.db
['users'][self
.db
['ranks'][num
]]['realnick']
108 def pts(num
): return self
.db
['users'][self
.db
['ranks'][num
]]['points']
111 msg("\00312THE GAME IS OVER!!!")
112 msg("THE WINNER IS: %s (%s)" % (person(0), pts(0)))
113 msg("2ND PLACE: %s (%s)" % (person(1), pts(1)))
114 msg("3RD PLACE: %s (%s)" % (person(2), pts(2)))
115 [msg("%dth place: %s (%s)" % (i
+1, person(i
), pts(i
))) for i
in range(3,10)]
116 except IndexError: pass
117 except Exception as e
: msg("DERP! %r" % (e
))
119 self
.db
['users'] = {}
120 self
.db
['ranks'] = []
125 t
= twitter
.Twitter(auth
=twitter
.OAuth(self
.getbot().parent
.cfg
.get('trivia', 'token'),
126 self
.getbot().parent
.cfg
.get('trivia', 'token_sec'),
127 self
.getbot().parent
.cfg
.get('trivia', 'con'),
128 self
.getbot().parent
.cfg
.get('trivia', 'con_sec')))
129 t
.statuses
.update(status
="Round is over! The winner was %s" % (winner
))
131 self
.__init
__(self
.questionfile
, self
.parent
)
133 def nextquestion(self
, qskipped
=False, iteration
=0):
134 if self
.gameover
== True:
135 return self
.doGameOver()
137 self
.getbot().msg(self
.getchan(), "Fail! The correct answer was: %s" % (self
.hintanswer
))
138 self
.missedquestions
+= 1
140 self
.missedquestions
= 0
142 if isinstance(self
.steptimer
, threading
._Timer
):
143 self
.steptimer
.cancel()
147 self
.revealpossibilities
= None
150 if self
.missedquestions
> MAXMISSEDQUESTIONS
:
152 self
.getbot().msg(self
.getchan(), "%d questions unanswered! Stopping the game.")
154 if state
.nextq
is not None:
158 nextq
= random
.choice(self
.db
['questions'])
160 if nextq
['question'][0] == "!":
161 nextq
= specialQuestion(nextq
)
163 if iteration
< 10 and 'lastasked' in nextq
and nextq
['lastasked'] - time
.time() < 24*60*60:
164 return self
.nextquestion(iteration
=iteration
+1) #short-circuit to pick another question
165 nextq
['lastasked'] = time
.time()
167 nextq
['answer'] = nextq
['answer'].lower()
169 qtext
= "\00300,01Next up: "
170 qary
= nextq
['question'].split(None)
172 qtext
+= "\00300,01"+qword
+"\00301,01"+chr(random
.randrange(32,126))
173 self
.getbot().msg(self
.chan
, qtext
)
177 if isinstance(self
.curq
['answer'], basestring
): self
.hintanswer
= self
.curq
['answer']
178 else: self
.hintanswer
= random
.choice(self
.curq
['answer'])
180 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[1])
181 self
.steptimer
.start()
183 def checkanswer(self
, answer
):
184 if self
.curq
is None:
186 elif isinstance(self
.curq
['answer'], basestring
):
187 return answer
.lower() == self
.curq
['answer']
188 else: # assume it's a list or something.
189 return answer
.lower() in self
.curq
['answer']
191 def addpoint(self
, _user
, count
=1):
194 if user
in self
.db
['users']:
195 self
.db
['users'][user
]['points'] += count
197 self
.db
['users'][user
] = {'points': count, 'realnick': _user, 'rank': len(self.db['ranks'])}
198 self
.db
['ranks'].append(user
)
200 state
.db
['ranks'].sort(key
=lambda nick
: state
.db
['users'][nick
]['points'], reverse
=True)
202 if self
.db
['users'][user
]['points'] >= state
.db
['target']:
205 return self
.db
['users'][user
]['points']
207 def points(self
, user
):
208 user
= str(user
).lower()
209 if user
in self
.db
['users']:
210 return self
.db
['users'][user
]['points']
214 def rank(self
, user
):
215 user
= str(user
).lower()
216 if user
in self
.db
['users']:
217 return self
.db
['users'][user
]['rank']+1
219 return len(self
.db
['users'])+1
221 def targetuser(self
, user
):
222 if len(self
.db
['ranks']) == 0: return "no one is ranked!"
224 user
= str(user
).lower()
225 if user
in self
.db
['users']:
226 rank
= self
.db
['users'][user
]['rank']
228 return "you're in the lead!"
230 return self
.db
['ranks'][rank
-1]
232 return self
.db
['ranks'][-1]
233 def targetpoints(self
, user
):
234 if len(self
.db
['ranks']) == 0: return 0
236 user
= str(user
).lower()
237 if user
in self
.db
['users']:
238 rank
= self
.db
['users'][user
]['rank']
242 return self
.db
['users'][self
.db
['ranks'][rank
-1]]['points']
244 return self
.db
['users'][self
.db
['ranks'][-1]]['points']
246 state
= TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
248 @lib.hookchan(state
.db
['chan'])
249 def trivia_checkanswer(bot
, user
, chan
, *args
):
250 line
= ' '.join([str(arg
) for arg
in args
])
251 if state
.checkanswer(line
):
252 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
)))
253 if state
.hintsgiven
== 0:
254 bot
.msg(chan
, "\00308%s\003 got an extra point for getting it before the hints! New score: %d." % (user
, state
.addpoint(user
)))
257 @lib.hook('points', needchan
=False)
258 def cmd_points(bot
, user
, chan
, realtarget
, *args
):
259 if chan
== realtarget
: replyto
= chan
262 if len(args
) != 0: who
= args
[0]
265 bot
.msg(replyto
, "%s has %d points." % (who
, state
.points(who
)))
267 @lib.hook('give', clevel
=lib
.OP
, needchan
=False)
269 def cmd_give(bot
, user
, chan
, realtarget
, *args
):
272 numpoints
= int(args
[1])
275 balance
= state
.addpoint(whoto
, numpoints
)
277 bot
.msg(chan
, "%s gave %s %d points. New balance: %d" % (user
, whoto
, numpoints
, balance
))
279 @lib.hook('setnext', clevel
=lib
.OP
, needchan
=False)
281 def cmd_setnext(bot
, user
, chan
, realtarget
, *args
):
282 line
= ' '.join([str(arg
) for arg
in args
])
283 linepieces
= line
.split('*')
284 if len(linepieces
) < 2:
285 bot
.msg(user
, "Error: need <question>*<answer>")
287 question
= linepieces
[0].strip()
288 answer
= linepieces
[1].strip()
289 state
.nextq
= {'question':question,'answer':answer}
290 bot
.msg(user
, "Done.")
292 @lib.hook('skip', clevel
=lib
.KNOWN
, needchan
=False)
293 def cmd_skip(bot
, user
, chan
, realtarget
, *args
):
294 state
.nextquestion(True)
296 @lib.hook('start', needchan
=False)
297 def cmd_start(bot
, user
, chan
, realtarget
, *args
):
298 if chan
== realtarget
: replyto
= chan
301 if state
.curq
is None:
304 bot
.msg(replyto
, "Game is already started!")
306 @lib.hook('stop', clevel
=lib
.KNOWN
, needchan
=False)
307 def cmd_stop(bot
, user
, chan
, realtarget
, *args
):
309 bot
.msg(state
.chan
, "Game stopped by %s" % (user
))
311 bot
.msg(user
, "Game isn't running.")
314 if state
.curq
is not None:
317 state
.steptimer
.cancel()
318 except Exception as e
:
319 print "!!! steptimer.cancel(): e"
324 @lib.hook('rank', needchan
=False)
325 def cmd_rank(bot
, user
, chan
, realtarget
, *args
):
326 if chan
== realtarget
: replyto
= chan
329 if len(args
) != 0: who
= args
[0]
332 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
)))
334 @lib.hook('top10', needchan
=False)
335 def cmd_top10(bot
, user
, chan
, realtarget
, *args
):
336 if len(state
.db
['ranks']) == 0:
337 return bot
.msg(state
.db
['chan'], "No one is ranked!")
340 for nick
in state
.db
['ranks'][0:10]:
341 user
= state
.db
['users'][nick
]
342 replylist
.append("%s (%s)" % (user
['realnick'], user
['points']))
343 bot
.msg(state
.db
['chan'], ', '.join(replylist
))
345 @lib.hook('settarget', clevel
=lib
.MASTER
, needchan
=False)
346 def cmd_settarget(bot
, user
, chan
, realtarget
, *args
):
348 state
.db
['target'] = int(args
[0])
349 bot
.msg(state
.db
['chan'], "Target has been changed to %s points!" % (state
.db
['target']))
351 bot
.msg(user
, "Failed to set target.")
353 @lib.hook('triviahelp', needchan
=False)
354 def cmd_triviahelp(bot
, user
, chan
, realtarget
, *args
):
355 bot
.msg(user
, "POINTS [<user>]")
356 bot
.msg(user
, "START")
357 bot
.msg(user
, "RANK [<user>]")
358 bot
.msg(user
, "TOP10")
360 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.KNOWN
:
361 bot
.msg(user
, "SKIP (>=KNOWN )")
362 bot
.msg(user
, "STOP (>=KNOWN )")
363 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.OP
:
364 bot
.msg(user
, "GIVE <user> [<points>] (>=OP )")
365 bot
.msg(user
, "SETNEXT <q>*<a> (>=OP )")
366 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.MASTER
:
367 bot
.msg(user
, "SETTARGET <points> (>=MASTER)")
370 def specialQuestion(oldq
):
371 newq
= {'question': oldq['question'], 'answer': oldq['answer']}
372 qtype
= oldq
['question'].upper()
374 if qtype
== "!MONTH":
375 newq
['question'] = "What month is it currently (in UTC)?"
376 newq
['answer'] = time
.strftime("%B").lower()
377 elif qtype
== "!MATH+":
378 randnum1
= random
.randrange(0, 11)
379 randnum2
= random
.randrange(0, 11)
380 newq
['question'] = "What is %d + %d?" % (randnum1
, randnum2
)
381 newq
['answer'] = spellout(randnum1
+randnum2
)
386 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
387 "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
388 "sixteen", "seventeen", "eighteen", "nineteen", "twenty"