1 # Erebus IRC bot - Author: Erebus Team
3 # This file is released into the public domain; see http://unlicense.org/
5 HINTTIMER
= 10.0 # how long between hints
6 HINTNUM
= 3 # how many hints to give
7 MAXMISSEDQUESTIONS
= 25 # how many missed questions before game stops
11 'author': 'Erebus Team',
12 'license': 'public domain',
13 'compatible': [1], # compatible module API versions
14 'depends': [], # other modules required to work properly?
19 lib
= modlib
.modlib(__name__
)
20 def modstart(parent
, *args
, **kwargs
):
22 return lib
.modstart(parent
, *args
, **kwargs
)
23 def modstop(*args
, **kwargs
):
28 return lib
.modstop(*args
, **kwargs
)
31 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
59 self
.missedquestions
= 0
64 if threading
is not None and threading
._Timer
is not None and isinstance(self
.steptimer
, threading
._Timer
):
65 self
.steptimer
.cancel()
66 if json
is not None and json
.dump
is not None:
67 json
.dump(self
.db
, open(self
.questionfile
, "w"))#, indent=4, separators=(',', ': '))
70 return self
.parent
.channel(self
.chan
)
72 return self
.getchan().bot
74 def nexthint(self
, hintnum
):
75 answer
= self
.hintanswer
77 if self
.hintstr
is None or self
.revealpossibilities
is None or self
.reveal
is None:
78 self
.hintstr
= list(re
.sub(r
'[a-zA-Z0-9]', '*', answer
))
79 self
.revealpossibilities
= range(''.join(self
.hintstr
).count('*'))
80 self
.reveal
= int(''.join(self
.hintstr
).count('*') * (7/24.0))
82 for i
in range(self
.reveal
):
83 revealcount
= random
.choice(self
.revealpossibilities
)
84 revealloc
= findnth(''.join(self
.hintstr
), '*', revealcount
)
85 self
.revealpossibilities
.remove(revealcount
)
86 self
.hintstr
[revealloc
] = answer
[revealloc
]
87 self
.parent
.channel(self
.chan
).bot
.msg(self
.chan
, "\00304,01Here's a hint: %s" % (''.join(self
.hintstr
)))
92 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[hintnum
+1])
93 self
.steptimer
.start()
95 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nextquestion
, args
=[True])
96 self
.steptimer
.start()
99 def msg(line
): self
.getbot().msg(self
.getchan(), line
)
100 def person(num
): return self
.db
['users'][self
.db
['ranks'][num
]]['realnick']
101 def pts(num
): return self
.db
['users'][self
.db
['ranks'][num
]]['points']
104 msg("\00312THE GAME IS OVER!!!")
105 msg("THE WINNER IS: %s (%s)" % (person(0), pts(0)))
106 msg("2ND PLACE: %s (%s)" % (person(1), pts(1)))
107 msg("3RD PLACE: %s (%s)" % (person(2), pts(2)))
108 [msg("%dth place: %s (%s)" % (i
+1, person(i
), pts(i
))) for i
in range(3,10)]
109 except IndexError: pass
110 except Exception as e
: msg("DERP! %r" % (e
))
112 self
.db
['users'] = {}
113 self
.db
['ranks'] = []
118 t
= twitter
.Twitter(auth
=twitter
.OAuth(self
.getbot().parent
.cfg
.get('trivia', 'token'),
119 self
.getbot().parent
.cfg
.get('trivia', 'token_sec'),
120 self
.getbot().parent
.cfg
.get('trivia', 'con'),
121 self
.getbot().parent
.cfg
.get('trivia', 'con_sec')))
122 t
.statuses
.update(status
="Round is over! The winner was %s" % (winner
))
124 self
.__init
__(self
.questionfile
, self
.parent
)
126 def nextquestion(self
, qskipped
=False, iteration
=0):
127 if self
.gameover
== True:
128 return self
.doGameOver()
130 self
.getbot().msg(self
.getchan(), "\00304Fail! The correct answer was: %s" % (self
.hintanswer
))
131 self
.missedquestions
+= 1
133 self
.missedquestions
= 0
135 if isinstance(self
.steptimer
, threading
._Timer
):
136 self
.steptimer
.cancel()
140 self
.revealpossibilities
= None
143 if self
.missedquestions
> MAXMISSEDQUESTIONS
:
145 self
.getbot().msg(self
.getchan(), "%d questions unanswered! Stopping the game.")
147 if state
.nextq
is not None:
151 nextq
= random
.choice(self
.db
['questions'])
153 if nextq
['question'][0] == "!":
154 nextq
= specialQuestion(nextq
)
156 if iteration
< 10 and 'lastasked' in nextq
and nextq
['lastasked'] - time
.time() < 24*60*60:
157 return self
.nextquestion(iteration
=iteration
+1) #short-circuit to pick another question
158 nextq
['lastasked'] = time
.time()
160 nextq
['answer'] = nextq
['answer'].lower()
162 qtext
= "\00304,01Next up: "
163 qary
= nextq
['question'].split(None)
165 qtext
+= "\00304,01"+qword
+"\00301,01"+chr(random
.randrange(0x61,0x7A)) #a-z
166 self
.getbot().msg(self
.chan
, qtext
)
170 if isinstance(self
.curq
['answer'], basestring
): self
.hintanswer
= self
.curq
['answer']
171 else: self
.hintanswer
= random
.choice(self
.curq
['answer'])
173 self
.steptimer
= threading
.Timer(HINTTIMER
, self
.nexthint
, args
=[1])
174 self
.steptimer
.start()
176 def checkanswer(self
, answer
):
177 if self
.curq
is None:
179 elif isinstance(self
.curq
['answer'], basestring
):
180 return answer
.lower() == self
.curq
['answer']
181 else: # assume it's a list or something.
182 return answer
.lower() in self
.curq
['answer']
184 def addpoint(self
, _user
, count
=1):
187 if user
in self
.db
['users']:
188 self
.db
['users'][user
]['points'] += count
190 self
.db
['users'][user
] = {'points': count, 'realnick': _user, 'rank': len(self.db['ranks'])}
191 self
.db
['ranks'].append(user
)
193 self
.db
['ranks'].sort(key
=lambda nick
: state
.db
['users'][nick
]['points'], reverse
=True)
194 for i
in range(0, len(self
.db
['ranks'])):
195 nick
= self
.db
['ranks'][i
]
196 self
.db
['users'][nick
]['rank'] = i
198 if self
.db
['users'][user
]['points'] >= state
.db
['target']:
201 return self
.db
['users'][user
]['points']
203 def points(self
, user
):
204 user
= str(user
).lower()
205 if user
in self
.db
['users']:
206 return self
.db
['users'][user
]['points']
210 def rank(self
, user
):
211 user
= str(user
).lower()
212 if user
in self
.db
['users']:
213 return self
.db
['users'][user
]['rank']+1
215 return len(self
.db
['users'])+1
217 def targetuser(self
, user
):
218 if len(self
.db
['ranks']) == 0: return "no one is ranked!"
220 user
= str(user
).lower()
221 if user
in self
.db
['users']:
222 rank
= self
.db
['users'][user
]['rank']
224 return "you're in the lead!"
226 return self
.db
['ranks'][rank
-1]
228 return self
.db
['ranks'][-1]
229 def targetpoints(self
, user
):
230 if len(self
.db
['ranks']) == 0: return 0
232 user
= str(user
).lower()
233 if user
in self
.db
['users']:
234 rank
= self
.db
['users'][user
]['rank']
238 return self
.db
['users'][self
.db
['ranks'][rank
-1]]['points']
240 return self
.db
['users'][self
.db
['ranks'][-1]]['points']
242 state
= TriviaState("/home/jrunyon/erebus/modules/trivia.json") #TODO get path from config
244 @lib.hookchan(state
.db
['chan'])
245 def trivia_checkanswer(bot
, user
, chan
, *args
):
246 line
= ' '.join([str(arg
) for arg
in args
])
247 if state
.checkanswer(line
):
248 bot
.msg(chan
, "\00312%s\003 has it! The answer was \00312%s\003. New score: %d. Rank: %d. Target: %s (%s)." % (user
, line
, state
.addpoint(user
), state
.rank(user
), state
.targetuser(user
), state
.targetpoints(user
)))
249 if state
.hintsgiven
== 0:
250 bot
.msg(chan
, "\00312%s\003 got an extra point for getting it before the hints! New score: %d." % (user
, state
.addpoint(user
)))
253 @lib.hook('points', needchan
=False)
254 def cmd_points(bot
, user
, chan
, realtarget
, *args
):
255 if chan
== realtarget
: replyto
= chan
258 if len(args
) != 0: who
= args
[0]
261 bot
.msg(replyto
, "%s has %d points." % (who
, state
.points(who
)))
263 @lib.hook('give', clevel
=lib
.OP
, needchan
=False)
265 def cmd_give(bot
, user
, chan
, realtarget
, *args
):
268 numpoints
= int(args
[1])
271 balance
= state
.addpoint(whoto
, numpoints
)
273 bot
.msg(chan
, "%s gave %s %d points. New balance: %d" % (user
, whoto
, numpoints
, balance
))
275 @lib.hook('setnext', clevel
=lib
.OP
, needchan
=False)
277 def cmd_setnext(bot
, user
, chan
, realtarget
, *args
):
278 line
= ' '.join([str(arg
) for arg
in args
])
279 linepieces
= line
.split('*')
280 if len(linepieces
) < 2:
281 bot
.msg(user
, "Error: need <question>*<answer>")
283 question
= linepieces
[0].strip()
284 answer
= linepieces
[1].strip()
285 state
.nextq
= {'question':question,'answer':answer}
286 bot
.msg(user
, "Done.")
288 @lib.hook('skip', clevel
=lib
.KNOWN
, needchan
=False)
289 def cmd_skip(bot
, user
, chan
, realtarget
, *args
):
290 state
.nextquestion(True)
292 @lib.hook('start', needchan
=False)
293 def cmd_start(bot
, user
, chan
, realtarget
, *args
):
294 if chan
== realtarget
: replyto
= chan
297 if state
.curq
is None:
300 bot
.msg(replyto
, "Game is already started!")
302 #FIXME @lib.hook('stop', clevel=lib.KNOWN, needchan=False)
303 @lib.hook('stop', needchan
=False) #FIXME
304 def cmd_stop(bot
, user
, chan
, realtarget
, *args
):
306 bot
.msg(state
.chan
, "Game stopped by %s" % (user
))
308 bot
.msg(user
, "Game isn't running.")
311 if state
.curq
is not None:
314 state
.steptimer
.cancel()
315 except Exception as e
:
316 print "!!! steptimer.cancel(): e"
321 @lib.hook('rank', needchan
=False)
322 def cmd_rank(bot
, user
, chan
, realtarget
, *args
):
323 if chan
== realtarget
: replyto
= chan
326 if len(args
) != 0: who
= args
[0]
329 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
)))
331 @lib.hook('top10', needchan
=False)
332 def cmd_top10(bot
, user
, chan
, realtarget
, *args
):
333 if len(state
.db
['ranks']) == 0:
334 return bot
.msg(state
.db
['chan'], "No one is ranked!")
337 for nick
in state
.db
['ranks'][0:10]:
338 user
= state
.db
['users'][nick
]
339 replylist
.append("%s (%s)" % (user
['realnick'], user
['points']))
340 bot
.msg(state
.db
['chan'], ', '.join(replylist
))
342 @lib.hook('settarget', clevel
=lib
.MASTER
, needchan
=False)
343 def cmd_settarget(bot
, user
, chan
, realtarget
, *args
):
345 state
.db
['target'] = int(args
[0])
346 bot
.msg(state
.db
['chan'], "Target has been changed to %s points!" % (state
.db
['target']))
348 bot
.msg(user
, "Failed to set target.")
350 @lib.hook('triviahelp', needchan
=False)
351 def cmd_triviahelp(bot
, user
, chan
, realtarget
, *args
):
352 bot
.msg(user
, "POINTS [<user>]")
353 bot
.msg(user
, "START")
354 bot
.msg(user
, "RANK [<user>]")
355 bot
.msg(user
, "TOP10")
357 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.KNOWN
:
358 bot
.msg(user
, "SKIP (>=KNOWN )")
359 bot
.msg(user
, "STOP (>=KNOWN )")
360 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.OP
:
361 bot
.msg(user
, "GIVE <user> [<points>] (>=OP )")
362 bot
.msg(user
, "SETNEXT <q>*<a> (>=OP )")
363 if bot
.parent
.channel(state
.db
['chan']).levelof(user
.auth
) >= lib
.MASTER
:
364 bot
.msg(user
, "SETTARGET <points> (>=MASTER)")
367 def num_417(bot
, textline
):
368 bot
.msg(state
.db
['chan'], "Whoops, it looks like that question didn't quite go through! (E:417). Let's try another...")
369 state
.nextquestion(False)
372 def specialQuestion(oldq
):
373 newq
= {'question': oldq['question'], 'answer': oldq['answer']}
374 qtype
= oldq
['question'].upper()
376 if qtype
== "!MONTH":
377 newq
['question'] = "What month is it currently (in UTC)?"
378 newq
['answer'] = time
.strftime("%B").lower()
379 elif qtype
== "!MATH+":
380 randnum1
= random
.randrange(0, 11)
381 randnum2
= random
.randrange(0, 11)
382 newq
['question'] = "What is %d + %d?" % (randnum1
, randnum2
)
383 newq
['answer'] = spellout(randnum1
+randnum2
)
388 "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
389 "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
390 "sixteen", "seventeen", "eighteen", "nineteen", "twenty"