### # Copyright (c) 2012, Matthias Meusburger # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### from supybot.commands import * import supybot.plugins as plugins import supybot.callbacks as callbacks import threading, random, pickle, os, time import supybot.ircdb as ircdb class DuckHunt(callbacks.Plugin): """ A DuckHunt game for supybot. Use the "start" command to start a game. The bot will randomly launch ducks. Whenever a duck is launched, the first person to use the "bang" command wins a point. Using the "bang" command when there is no duck launched costs a point. """ # threaded = True # Those parameters are per-channel parameters started = {} # Has the hunt started? duck = {} # Is there currently a duck to shoot? shoots = {} # Number of successfull shoots in a hunt scores = {} # Scores for the current hunt times = {} # Elapsed time since the last duck was launched channelscores = {} # Saved scores for the channel toptimes = {} # Times for the current hunt channeltimes = {} # Saved times for the channel # Where to save scores? path = "supybot/data/DuckHunt/" # Does a duck needs to be launched? probability = 1 lastSpoke = time.time() minthrottle = 15 maxthrottle = 45 throttle = random.randint(minthrottle, maxthrottle) debug = 0 # Adds new scores and times to the already saved ones # and saves them back to the disk def _write_scores(self, msg): currentChannel = msg.args[0] # scores # Adding current scores to the channel scores for player in self.scores[currentChannel].keys(): if not player in self.channelscores[currentChannel]: # It's a new player self.channelscores[currentChannel][player] = self.scores[currentChannel][player] else: # It's a player that already has a saved score self.channelscores[currentChannel][player] += self.scores[currentChannel][player] outputfile = open(self.path + msg.args[0] + ".scores", "wb") pickle.dump(self.channelscores[currentChannel], outputfile) outputfile.close() # times # Adding times scores to the channel scores for player in self.toptimes[currentChannel].keys(): if not player in self.channeltimes[currentChannel]: # It's a new player self.channeltimes[currentChannel][player] = self.toptimes[currentChannel][player] else: # It's a player that already has a saved score # And we save the time of the current hunt if it's better than it's previous time if(self.toptimes[currentChannel][player] < self.channeltimes[currentChannel][player]): self.channeltimes[currentChannel][player] = self.toptimes[currentChannel][player] outputfile = open(self.path + msg.args[0] + ".times", "wb") pickle.dump(self.channeltimes[currentChannel], outputfile) outputfile.close() # Reads scores and times from disk def _read_scores(self, msg): # scores if os.path.isfile(self.path + msg.args[0] + ".scores"): inputfile = open(self.path + msg.args[0] + ".scores", "rb") self.channelscores[msg.args[0]] = pickle.load(inputfile) inputfile.close() # times if os.path.isfile(self.path + msg.args[0] + ".times"): inputfile = open(self.path + msg.args[0] + ".times", "rb") self.channeltimes[msg.args[0]] = pickle.load(inputfile) inputfile.close() # Starts a hunt def start(self, irc, msg, args): """ Starts the hunt """ currentChannel = msg.args[0] if irc.isChannel(currentChannel): if(self.started.get(currentChannel) == True): irc.reply("There is already a hunt right now!") else: # Init frequency if self.registryValue('frequency', currentChannel): self.probability = self.registryValue('frequency', currentChannel) else: self.probability = 0.3 # Init saved scores try: self.channelscores[currentChannel] except: self.channelscores[currentChannel] = {} # Init saved times try: self.channeltimes[currentChannel] except: self.channeltimes[currentChannel] = {} # Init times self.toptimes[currentChannel] = {} # Init bangdelay self.times[currentChannel] = False if not self.channelscores[currentChannel] or not self.channeltimes[currentChannel]: self._read_scores(msg) # Reinit current hunt scores if self.scores.get(currentChannel): self.scores[currentChannel] = {} # Reinit current hunt times self.toptimes[currentChannel] = {} # No duck launched self.duck[currentChannel] = False # Hunt started self.started[currentChannel] = True irc.reply("The hunt starts now!") else: irc.error('You have to be on a channel') start = wrap(start) # Stops the current hunt def stop(self, irc, msg, args): """ Stops the hunt """ currentChannel = msg.args[0] if irc.isChannel(currentChannel): self._end(irc, msg, args) else: irc.error('You have to be on a channel') stop = wrap(stop) # Tells if there is currently a duck def launched(self, irc, msg, args): """ Is there a duck right now? """ currentChannel = msg.args[0] if irc.isChannel(currentChannel): if(self.started.get(currentChannel) == True): if(self.duck[currentChannel] == True): irc.reply("There is currently a duck! You can shoot it with the 'bang' command") else: irc.reply("There is no duck right now! Wait for one to be launched!") else: irc.reply("There is no hunt right now! You can start a hunt with the 'start' command") else: irc.error('You have to be on a channel') launched = wrap(launched) # Shows the score for a given nick def score(self, irc, msg, args, nick): """: Shows the score for a given nick """ currentChannel = msg.args[0] if irc.isChannel(currentChannel): try: self.channelscores[currentChannel] except: self.channelscores[currentChannel] = {} if not self.channelscores[currentChannel]: self._read_scores(msg) try: irc.reply(self.channelscores[currentChannel][nick]) except: irc.reply("There is no score for %s on %s" % (nick, currentChannel)) else: irc.error('You have to be on a channel') score = wrap(score, ['anything']) # Merge scores # nickto gets the points of nickfrom and nickfrom is removed from the scorelist def merge(self, irc, msg, args, nickto, nickfrom): """ : nickto gets the points of nickfrom and nickfrom is removed from the scorelist """ if self._capability(msg, 'owner'): currentChannel = msg.args[0] if irc.isChannel(currentChannel): try: self.channelscores[currentChannel][nickto] += self.channelscores[currentChannel][nickfrom] del self.channelscores[currentChannel][nickfrom] irc.reply("Okay! (will be effective at the end of the hunt)") except: irc.error("Something went wrong") else: irc.error('You have to be on a channel') else: irc.error("Who are you again?") merge = wrap(merge, ['anything', 'anything']) # Shows all scores for the channel def listscores(self, irc, msg, args): """ Shows the score list for the current channel """ currentChannel = msg.args[0] try: self.channelscores[currentChannel] except: self.channelscores[currentChannel] = {} if not self.channelscores[currentChannel]: self._read_scores(msg) # Sort the scores (reversed: the higher the better) scores = sorted(self.channelscores[currentChannel].iteritems(), key=lambda (k,v):(v,k), reverse=True) msgstring = "" for item in scores: # Why do we show the nicks as xnickx? # Just to prevent everyone that has ever played a hunt in the channel to be pinged every time anyone asks for the score list msgstring += "x" + item[0] + "x: "+ str(item[1]) + ", " if msgstring != "": irc.reply("\_o< ~ DuckHunt scores for " + msg.args[0] + " ~ >o_/") irc.reply(msgstring) else: irc.reply("There aren't any scores for this channel yet.") listscores = wrap(listscores) # Shows all times for the channel def listtimes(self, irc, msg, args): """ Shows the times list for the current channel """ currentChannel = msg.args[0] try: self.channeltimes[currentChannel] except: self.channeltimes[currentChannel] = {} if not self.channeltimes[currentChannel]: self._read_scores(msg) # Sort the times (not reversed: the lower the better) times = sorted(self.channeltimes[currentChannel].iteritems(), key=lambda (k,v):(v,k), reverse=False) msgstring = "" for item in times: # Same as in listscores for the xnickx msgstring += "x" + item[0] + "x: "+ str(round(item[1],2)) + ", " if msgstring != "": irc.reply("\_o< ~ DuckHunt times for " + msg.args[0] + " ~ >o_/") irc.reply(msgstring) else: irc.reply("There aren't any times for this channel yet.") listscores = wrap(listscores) # This is the callback when someones speaks in the channel # We use this to determine if a duck has to be launched def doPrivmsg(self, irc, msg): currentChannel = msg.args[0] now = time.time() if irc.isChannel(currentChannel): if(self.started.get(currentChannel) == True): if (self.duck[currentChannel] == False): if now > self.lastSpoke + self.throttle: if random.random() < self.probability: self._launch(irc, msg, '') self.lastSpoke = now # This is the debug function: when debug is enabled, # it launches a duck when called def dbg(self, irc, msg, args): """ This is a debug command. If debug mode is not enabled, it won't do anything """ currentChannel = msg.args[0] if (self.debug): if irc.isChannel(currentChannel): self._launch(irc, msg, '') dbg = wrap(dbg) # Shoots the duck! def bang(self, irc, msg, args): """ Shoots the duck! """ currentChannel = msg.args[0] if self.registryValue('ducks', currentChannel): maxShoots = self.registryValue('ducks', currentChannel) else: maxShoots = 10 if irc.isChannel(currentChannel): if(self.started.get(currentChannel) == True): # bangdelay: how much time between the duck and launched and this shot? if self.times[currentChannel]: bangdelay = time.time() - self.times[currentChannel] else: bangdelay = False # There was a duck if (self.duck[currentChannel] == True): # Adds one point for the nick that shot the duck try: self.scores[currentChannel][msg.nick] += 1 except: try: self.scores[currentChannel][msg.nick] = 1 except: self.scores[currentChannel] = {} self.scores[currentChannel][msg.nick] = 1 irc.reply("\_x< %s: %i (%.2f seconds)" % (msg.nick, self.scores[currentChannel][msg.nick], bangdelay)) # Now save the bang delay for the player (if it's quicker than it's previous bangdelay) try: previoustime = self.toptimes[currentChannel][msg.nick] if(bangdelay < previoustime): self.toptimes[currentChannel][msg.nick] = bangdelay except: self.toptimes[currentChannel][msg.nick] = bangdelay self.duck[currentChannel] = False # End of Hunt if (self.shoots[currentChannel] == maxShoots): self._end(irc, msg, args) # If autorestart is enabled, we restart a hunt automatically! if self.registryValue('autoRestart', currentChannel): self.started[currentChannel] = True if self.scores.get(currentChannel): self.scores[currentChannel] = {} irc.reply("The hunt starts now!") # There was no duck or the duck has already been shot else: # Removes one point for the nick that shot try: self.scores[currentChannel][msg.nick] -= 1 except: try: self.scores[currentChannel][msg.nick] = -1 except: self.scores[currentChannel] = {} self.scores[currentChannel][msg.nick] = -1 # If we were able to have a bangdelay (ie: a duck was launched before someone did bang) if (bangdelay): irc.reply("There was no duck! %s: %i (%.2f seconds) " % (msg.nick, self.scores[currentChannel][msg.nick], bangdelay)) else: irc.reply("There was no duck! %s: %i" % (msg.nick, self.scores[currentChannel][msg.nick])) else: irc.reply("The hunt has not started yet!") else: irc.error('You have to be on a channel') bang = wrap(bang) # End of the hunt (is called when the hunts stop "naturally" or when someone uses the !stop command) def _end(self, irc, msg, args): currentChannel = msg.args[0] try: self.channelscores[currentChannel] except: self.channelscores[currentChannel] = {} irc.reply("The hunt stops now!") # Showing scores irc.reply(self.scores.get(currentChannel)) # Getting channel best time (to see if the best time of this hunt is better) channelbestnick = None channelbesttime = None if self.channeltimes.get(currentChannel): channelbestnick, channelbesttime = min(self.channeltimes.get(currentChannel).iteritems(), key=lambda (k,v):(v,k)) # Showing best time recordmsg = '' if (self.toptimes.get(currentChannel)): key,value = min(self.toptimes.get(currentChannel).iteritems(), key=lambda (k,v):(v,k)) if (channelbesttime and value < channelbesttime): recordmsg = '. This is the new record for this channel! (previous record was held by ' + channelbestnick + ' with ' + str(round(channelbesttime,2)) + ' seconds)' else: try: if(value < self.channeltimes[currentChannel][key]): recordmsg = ' (this is your new record in this channel! Your previous record was ' + str(round(self.channeltimes[currentChannel][key],2)) + ')' except: recordmsg = '' irc.reply("Best time: %s with %.2f seconds%s" % (key, value, recordmsg)) # Write the scores and times to disk self._write_scores(msg) # Reinit current hunt scores if self.scores.get(currentChannel): self.scores[currentChannel] = {} # Reinit current hunt times if self.toptimes.get(currentChannel): self.toptimes[currentChannel] = {} # No duck lauched self.duck[currentChannel] = False # Hunt not started self.started[currentChannel] = False # Reinit number of shoots self.shoots[currentChannel] = 0 # Launch a duck def _launch(self, irc, msg, args): """ Launch a duck """ currentChannel = msg.args[0] if irc.isChannel(currentChannel): if(self.started[currentChannel] == True): if (self.duck[currentChannel] == False): self.times[currentChannel] = time.time() self.throttle = random.randint(self.minthrottle, self.maxthrottle) irc.reply("\_o< quack!") self.duck[currentChannel] = True # Store time here try: self.shoots[currentChannel] += 1 except: self.shoots[currentChannel] = 1 else: irc.reply("Already a duck") else: irc.reply("The hunting has not started yet!") else: irc.error('You have to be on a channel') def _capability(self, msg, c): try: u = ircdb.users.getUser(msg.prefix) if u._checkCapability(c): return True except: return False Class = DuckHunt # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: