diff --git a/DuckHunt/README.md b/DuckHunt/README.md
new file mode 100644
index 0000000..0e96cad
--- /dev/null
+++ b/DuckHunt/README.md
@@ -0,0 +1,36 @@
+Requires Python3, Limnoria.
+
+Python 3 and score fixes. Plugin working.
+
+\_o< ~ DuckHunt game for supybot ~ >o_/
+=======================================
+
+How to play
+-----------
+ * 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.
+ * If a player shoots all the ducks during a hunt, it's a perfect! This player gets extra bonus points.
+ * The best scores for a channel are recorded and can be displayed with the "listscores" command.
+ * The quickest and longest shoots are also recorded and can be displayed with the "listtimes" command.
+ * The "launched" command tells if there is currently a duck to shoot.
+
+How to install
+--------------
+Just place the DuckHunt plugin in the plugins directory of your supybot installation and load the module.
+
+How to configure
+----------------
+Several per-channel configuration variables are available (look at the "channel" command to learn more on how to configure per-channel configuration variables):
+ * autoRestart: Does a new hunt automatically start when the previous one is over?
+ * ducks: Number of ducks during a hunt?
+ * minthrottle: The minimum amount of time before a new duck may be launched (in seconds)
+ * maxthrottle: The maximum amount of time before a new duck may be launched (in seconds)
+ * reloadTime: The time it takes to reload your rifle once you have shot (in seconds)
+ * kickMode: If someone shoots when there is no duck, should he be kicked from the channel? (this requires the bot to be op on the channel)
+ * autoFriday: Do we need to automatically launch more ducks on friday?
+ * missProbability: The probability to miss the duck
+
+Update
+------
+Get latest version at : https://github.com/veggiematts/supybot-duckhunt
diff --git a/DuckHunt/__init__.py b/DuckHunt/__init__.py
new file mode 100644
index 0000000..6589e91
--- /dev/null
+++ b/DuckHunt/__init__.py
@@ -0,0 +1,67 @@
+###
+# 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.
+
+###
+
+"""
+This is a DuckHunt game for supybot
+"""
+
+import supybot
+import supybot.world as world
+from imp import reload
+import importlib
+
+# Use this for the version of this plugin. You may wish to put a CVS keyword
+# in here if you're keeping the plugin in CVS or some similar system.
+__version__ = ""
+
+# XXX Replace this with an appropriate author or supybot.Author instance.
+__author__ = supybot.Author('Matthias Meusburger', 'veggiematts', '')
+
+# This is a dictionary mapping supybot.Author instances to lists of
+# contributions.
+__contributors__ = {}
+
+# This is a url where the most recent plugin package can be downloaded.
+__url__ = 'https://github.com/veggiematts/supybot-duckhunt'
+
+from . import config
+from . import plugin
+importlib.reload(plugin) # In case we're being reloaded.
+# Add more reloads here if you add third-party modules and want them to be
+# reloaded when this plugin is reloaded. Don't forget to import them as well!
+
+if world.testing:
+ from . import test
+
+Class = plugin.Class
+configure = config.configure
+
+
+# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
diff --git a/DuckHunt/config.py b/DuckHunt/config.py
new file mode 100644
index 0000000..8260b0e
--- /dev/null
+++ b/DuckHunt/config.py
@@ -0,0 +1,72 @@
+###
+# 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.
+###
+
+import supybot.conf as conf
+import supybot.registry as registry
+
+def configure(advanced):
+ # This will be called by supybot to configure this module. advanced is
+ # a bool that specifies whether the user identified himself as an advanced
+ # user or not. You should effect your configuration by manipulating the
+ # registry as appropriate.
+ from supybot.questions import expect, anything, something, yn
+ conf.registerPlugin('DuckHunt', True)
+
+
+DuckHunt = conf.registerPlugin('DuckHunt')
+# This is where your configuration variables (if any) should go. For example:
+# conf.registerGlobalValue(Quote, 'someConfigVariableName',
+# registry.Boolean(False, """Help for someConfigVariableName."""))
+conf.registerChannelValue(DuckHunt, 'autoRestart',
+ registry.Boolean(False, """Does a new hunt automatically start when the previous one is over?"""))
+
+conf.registerChannelValue(DuckHunt, 'ducks',
+ registry.Integer(5, """Number of ducks during a hunt?"""))
+
+conf.registerChannelValue(DuckHunt, 'minthrottle',
+ registry.Integer(30, """The minimum amount of time before a new duck may be launched (in seconds)"""))
+
+conf.registerChannelValue(DuckHunt, 'maxthrottle',
+ registry.Integer(300, """The maximum amount of time before a new duck may be launched (in seconds)"""))
+
+conf.registerChannelValue(DuckHunt, 'reloadTime',
+ registry.Integer(5, """The time it takes to reload your rifle once you have shot (in seconds)"""))
+
+conf.registerChannelValue(DuckHunt, 'missProbability',
+ registry.Probability(0.2, """The probability to miss the duck"""))
+
+conf.registerChannelValue(DuckHunt, 'kickMode',
+ registry.Boolean(True, """If someone shoots when there is no duck, should he be kicked from the channel? (this requires the bot to be op on the channel)"""))
+
+conf.registerChannelValue(DuckHunt, 'autoFriday',
+ registry.Boolean(True, """ Do we need to automatically launch more ducks on friday? """))
+
+
+
+# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff --git a/DuckHunt/doc/DuckHunt.html b/DuckHunt/doc/DuckHunt.html
new file mode 100644
index 0000000..950f756
--- /dev/null
+++ b/DuckHunt/doc/DuckHunt.html
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+Documentation for the DuckHunt plugin for Supybot
+
+
+
+
+
Documentation for the DuckHunt plugin for Supybot
+
+
+
Purpose
+
This is a DuckHunt game for supybot
+
+
+
Usage
+
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.
+
+
+
Commands
+
+- bang
+- Shoots the duck!
+- dayscores
+- [<channel>] Shows the score list of the day for <channel>.
+- dbg
+- This is a debug command. If debug mode is not enabled, it won't do anything
+- fridaymode
+- [<status>] Enable/disable friday mode! (there are lots of ducks on friday :))
+- launched
+- Is there a duck right now?
+- listscores
+- [<size>] [<channel>] Shows the <size>-sized score list for <channel> (or for
+the current channel if no channel is given)
+- listtimes
+- [<size>] [<channel>] Shows the <size>-sized time list for <channel> (or for
+the current channel if no channel is given)
+- mergescores
+- [<channel>] <nickto> <nickfrom> nickto gets the points of nickfrom and
+nickfrom is removed from the scorelist
+- mergetimes
+- [<channel>] <nickto> <nickfrom> nickto gets the best time of nickfrom if
+nickfrom time is better than nickto time, and nickfrom is removed from the
+timelist. Also works with worst times.
+- rmscore
+- [<channel>] <nick> Remove <nick>'s score
+- rmtime
+- [<channel>] <nick> Remove <nick>'s best time
+- score
+- <nick> Shows the score for a given nick
+- start
+- Starts the hunt
+- stop
+- Stops the current hunt
+- total
+- Shows the total amount of ducks shot in <channel> (or in the current channel
+if no channel is given)
+- weekscores
+- [<week>] [<nick>] [<channel>] Shows the score list of the week for <channel>.
+If <nick> is provided, it will only show <nick>'s scores.
+
+
+
+
Configuration
+
+- supybot.plugins.DuckHunt.public
+This config variable defaults to True and is not channel specific.
+Determines whether this plugin is publicly visible.
+
+- supybot.plugins.DuckHunt.autoRestart
+This config variable defaults to False and is channel specific.
+Does a new hunt automatically start when the previous one is over?
+
+- supybot.plugins.DuckHunt.ducks
+This config variable defaults to 5 and is channel specific.
+Number of ducks during a hunt?
+
+- supybot.plugins.DuckHunt.minthrottle
+This config variable defaults to 30 and is channel specific.
+The minimum amount of time before a new duck may be launched (in seconds)
+
+- supybot.plugins.DuckHunt.maxthrottle
+This config variable defaults to 300 and is channel specific.
+The maximum amount of time before a new duck may be launched (in seconds)
+
+- supybot.plugins.DuckHunt.reloadTime
+This config variable defaults to 5 and is channel specific.
+The time it takes to reload your rifle once you have shot (in seconds)
+
+- supybot.plugins.DuckHunt.missProbability
+This config variable defaults to 0.20000000000000001 and is channel specific.
+The probability to miss the duck
+
+- supybot.plugins.DuckHunt.kickMode
+This config variable defaults to True and is channel specific.
+If someone shoots when there is no duck, should he be kicked from the
+channel? (this requires the bot to be op on the channel)
+
+- supybot.plugins.DuckHunt.autoFriday
+This config variable defaults to True and is channel specific.
+Do we need to automatically launch more ducks on friday?
+
+
+
+
+
+
diff --git a/DuckHunt/doc/DuckHunt.rst b/DuckHunt/doc/DuckHunt.rst
new file mode 100644
index 0000000..7ea6c69
--- /dev/null
+++ b/DuckHunt/doc/DuckHunt.rst
@@ -0,0 +1,119 @@
+Documentation for the DuckHunt plugin for Supybot
+=================================================
+
+Purpose
+-------
+This is a DuckHunt game for supybot
+
+Usage
+-----
+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.
+
+Commands
+--------
+bang
+ Shoots the duck!
+
+dayscores
+ [] Shows the score list of the day for .
+
+dbg
+ This is a debug command. If debug mode is not enabled, it won't do anything
+
+fridaymode
+ [] Enable/disable friday mode! (there are lots of ducks on friday :))
+
+launched
+ Is there a duck right now?
+
+listscores
+ [] [] Shows the -sized score list for (or for
+ the current channel if no channel is given)
+
+listtimes
+ [] [] Shows the -sized time list for (or for
+ the current channel if no channel is given)
+
+mergescores
+ [] nickto gets the points of nickfrom and
+ nickfrom is removed from the scorelist
+
+mergetimes
+ [] nickto gets the best time of nickfrom if
+ nickfrom time is better than nickto time, and nickfrom is removed from the
+ timelist. Also works with worst times.
+
+rmscore
+ [] Remove 's score
+
+rmtime
+ [] Remove 's best time
+
+score
+ Shows the score for a given nick
+
+start
+ Starts the hunt
+
+stop
+ Stops the current hunt
+
+total
+ Shows the total amount of ducks shot in (or in the current channel
+ if no channel is given)
+
+weekscores
+ [] [] [] Shows the score list of the week for .
+ If is provided, it will only show 's scores.
+
+Configuration
+-------------
+supybot.plugins.DuckHunt.public
+ This config variable defaults to True and is not channel specific.
+
+ Determines whether this plugin is publicly visible.
+
+supybot.plugins.DuckHunt.autoRestart
+ This config variable defaults to False and is channel specific.
+
+ Does a new hunt automatically start when the previous one is over?
+
+supybot.plugins.DuckHunt.ducks
+ This config variable defaults to 5 and is channel specific.
+
+ Number of ducks during a hunt?
+
+supybot.plugins.DuckHunt.minthrottle
+ This config variable defaults to 30 and is channel specific.
+
+ The minimum amount of time before a new duck may be launched (in seconds)
+
+supybot.plugins.DuckHunt.maxthrottle
+ This config variable defaults to 300 and is channel specific.
+
+ The maximum amount of time before a new duck may be launched (in seconds)
+
+supybot.plugins.DuckHunt.reloadTime
+ This config variable defaults to 5 and is channel specific.
+
+ The time it takes to reload your rifle once you have shot (in seconds)
+
+supybot.plugins.DuckHunt.missProbability
+ This config variable defaults to 0.20000000000000001 and is channel specific.
+
+ The probability to miss the duck
+
+supybot.plugins.DuckHunt.kickMode
+ This config variable defaults to True and is channel specific.
+
+ If someone shoots when there is no duck, should he be kicked from the
+ channel? (this requires the bot to be op on the channel)
+
+supybot.plugins.DuckHunt.autoFriday
+ This config variable defaults to True and is channel specific.
+
+ Do we need to automatically launch more ducks on friday?
+
diff --git a/DuckHunt/doc/DuckHunt.stx b/DuckHunt/doc/DuckHunt.stx
new file mode 100644
index 0000000..9f69f11
--- /dev/null
+++ b/DuckHunt/doc/DuckHunt.stx
@@ -0,0 +1,146 @@
+Documentation for the DuckHunt plugin for Supybot
+
+ Purpose
+
+ This is a DuckHunt game for supybot
+
+ Usage
+
+ 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.
+
+ Commands
+
+ * bang
+
+ Shoots the duck!
+
+ * dayscores
+
+ [] Shows the score list of the day for .
+
+ * dbg
+
+ This is a debug command. If debug mode is not enabled, it won't do
+ anything
+
+ * fridaymode
+
+ [] Enable/disable friday mode! (there are lots of ducks on friday
+ :))
+
+ * launched
+
+ Is there a duck right now?
+
+ * listscores
+
+ [] [] Shows the -sized score list for (or
+ for the current channel if no channel is given)
+
+ * listtimes
+
+ [] [] Shows the -sized time list for (or
+ for the current channel if no channel is given)
+
+ * mergescores
+
+ [] nickto gets the points of nickfrom and
+ nickfrom is removed from the scorelist
+
+ * mergetimes
+
+ [] nickto gets the best time of nickfrom if
+ nickfrom time is better than nickto time, and nickfrom is removed from
+ the timelist. Also works with worst times.
+
+ * rmscore
+
+ [] Remove 's score
+
+ * rmtime
+
+ [] Remove 's best time
+
+ * score
+
+ Shows the score for a given nick
+
+ * start
+
+ Starts the hunt
+
+ * stop
+
+ Stops the current hunt
+
+ * total
+
+ Shows the total amount of ducks shot in (or in the current
+ channel if no channel is given)
+
+ * weekscores
+
+ [] [] [] Shows the score list of the week for
+ . If is provided, it will only show 's scores.
+
+ Configuration
+
+ * supybot.plugins.DuckHunt.public
+
+ This config variable defaults to True and is not channel specific.
+
+ Determines whether this plugin is publicly visible.
+
+ * supybot.plugins.DuckHunt.autoRestart
+
+ This config variable defaults to False and is channel specific.
+
+ Does a new hunt automatically start when the previous one is over?
+
+ * supybot.plugins.DuckHunt.ducks
+
+ This config variable defaults to 5 and is channel specific.
+
+ Number of ducks during a hunt?
+
+ * supybot.plugins.DuckHunt.minthrottle
+
+ This config variable defaults to 30 and is channel specific.
+
+ The minimum amount of time before a new duck may be launched (in seconds)
+
+ * supybot.plugins.DuckHunt.maxthrottle
+
+ This config variable defaults to 300 and is channel specific.
+
+ The maximum amount of time before a new duck may be launched (in seconds)
+
+ * supybot.plugins.DuckHunt.reloadTime
+
+ This config variable defaults to 5 and is channel specific.
+
+ The time it takes to reload your rifle once you have shot (in seconds)
+
+ * supybot.plugins.DuckHunt.missProbability
+
+ This config variable defaults to 0.20000000000000001 and is channel
+ specific.
+
+ The probability to miss the duck
+
+ * supybot.plugins.DuckHunt.kickMode
+
+ This config variable defaults to True and is channel specific.
+
+ If someone shoots when there is no duck, should he be kicked from the
+ channel? (this requires the bot to be op on the channel)
+
+ * supybot.plugins.DuckHunt.autoFriday
+
+ This config variable defaults to True and is channel specific.
+
+ Do we need to automatically launch more ducks on friday?
+
diff --git a/DuckHunt/plugin.py b/DuckHunt/plugin.py
new file mode 100644
index 0000000..85cd3bf
--- /dev/null
+++ b/DuckHunt/plugin.py
@@ -0,0 +1,1175 @@
+###
+# 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 supybot.schedule as schedule
+import supybot.ircdb as ircdb
+import supybot.ircmsgs as ircmsgs
+import supybot.log as log
+import supybot.conf as conf
+
+
+import threading, random, pickle, os, time, datetime
+
+
+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
+ worsttimes = {} # Worst times for the current hunt
+ channelworsttimes = {} # Saved worst times for the channel
+ averagetime = {} # Average shooting time for the current hunt
+ fridayMode = {} # Are we on friday mode? (automatic)
+ manualFriday = {} # Are we on friday mode? (manual)
+ missprobability = {} # Probability to miss a duck when shooting
+ week = {} # Scores for the week
+ channelweek = {} # Saved scores for the week
+ leader = {} # Who is the leader for the week?
+ reloading = {} # Who is currently reloading?
+ reloadtime = {} # Time to reload after shooting (in seconds)
+
+ # Does a duck needs to be launched?
+ lastSpoke = {}
+ minthrottle = {}
+ maxthrottle = {}
+ throttle = {}
+
+ # Where to save scores?
+ fileprefix = "DuckHunt_"
+ path = conf.supybot.directories.data
+
+ # Enable the 'dbg' command, which launch a duck, if true
+ debug = 0
+
+ # Other params
+ perfectbonus = 5 # How many extra-points are given when someones does a perfect hunt?
+ toplist = 5 # How many high{scores|times} are displayed by default?
+ dow = int(time.strftime("%u")) # Day of week
+ woy = int(time.strftime("%V")) # Week of year
+ year = time.strftime("%Y")
+ dayname = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+
+
+ def _calc_scores(self, channel):
+ """
+ Adds new scores and times to the already saved ones
+ """
+
+ # scores
+ # Adding current scores to the channel scores
+ for player, value in self.scores[channel].items():
+ if not player in self.channelscores[channel]:
+ # It's a new player
+ self.channelscores[channel][player] = value
+ else:
+ # It's a player that already has a saved score
+ self.channelscores[channel][player] += value
+
+ # times
+ # Adding times scores to the channel scores
+ for player, value in self.toptimes[channel].items():
+ if not player in self.channeltimes[channel]:
+ # It's a new player
+ self.channeltimes[channel][player] = value
+ 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[channel][player] < self.channeltimes[channel][player]):
+ self.channeltimes[channel][player] = self.toptimes[channel][player]
+
+ # worst times
+ # Adding worst times scores to the channel scores
+ for player, value in self.worsttimes[channel].items():
+ if not player in self.channelworsttimes[channel]:
+ # It's a new player
+ self.channelworsttimes[channel][player] = value
+ else:
+ # It's a player that already has a saved score
+ # And we save the time of the current hunt if it's worst than it's previous time
+ if(self.worsttimes[channel][player] > value):
+ self.channelworsttimes[channel][player] = value
+
+ # week scores
+ for player, value in self.scores[channel].items():
+ #FIXME: If the hunt starts a day and ends the day after, this will produce an error:
+ if not player in self.channelweek[channel][self.woy][self.dow]:
+ # It's a new player
+ self.channelweek[channel][self.woy][self.dow][player] = value
+ else:
+ # It's a player that already has a saved score
+ self.channelweek[channel][self.woy][self.dow][player] += value
+
+
+
+
+ def _write_scores(self, channel):
+ """
+ Write scores and times to the disk
+ """
+
+ # scores
+ outputfile = open(self.path.dirize(self.fileprefix + channel + ".scores"), "wb")
+ pickle.dump(self.channelscores[channel], outputfile)
+ outputfile.close()
+
+ # times
+ outputfile = open(self.path.dirize(self.fileprefix + channel + ".times"), "wb")
+ pickle.dump(self.channeltimes[channel], outputfile)
+ outputfile.close()
+
+ # worst times
+ outputfile = open(self.path.dirize(self.fileprefix + channel + ".worsttimes"), "wb")
+ pickle.dump(self.channelworsttimes[channel], outputfile)
+ outputfile.close()
+
+ # week scores
+ outputfile = open(self.path.dirize(self.fileprefix + channel + self.year + ".weekscores"), "wb")
+ pickle.dump(self.channelweek[channel], outputfile)
+ outputfile.close()
+
+
+
+
+
+
+
+ def _read_scores(self, channel):
+ """
+ Reads scores and times from disk
+ """
+ filename = self.path.dirize(self.fileprefix + channel)
+ # scores
+ if not self.channelscores.get(channel):
+ if os.path.isfile(filename + ".scores"):
+ inputfile = open(filename + ".scores", "rb")
+ self.channelscores[channel] = pickle.load(inputfile)
+ inputfile.close()
+
+ # times
+ if not self.channeltimes.get(channel):
+ if os.path.isfile(filename + ".times"):
+ inputfile = open(filename + ".times", "rb")
+ self.channeltimes[channel] = pickle.load(inputfile)
+ inputfile.close()
+
+ # worst times
+ if not self.channelworsttimes.get(channel):
+ if os.path.isfile(filename + ".worsttimes"):
+ inputfile = open(filename + ".worsttimes", "rb")
+ self.channelworsttimes[channel] = pickle.load(inputfile)
+ inputfile.close()
+
+ # week scores
+ if not self.channelweek.get(channel):
+ if os.path.isfile(filename + self.year + ".weekscores"):
+ inputfile = open(filename + self.year + ".weekscores", "rb")
+ self.channelweek[channel] = pickle.load(inputfile)
+ inputfile.close()
+
+
+
+ def _initdayweekyear(self, channel):
+ self.dow = int(time.strftime("%u")) # Day of week
+ self.woy = int(time.strftime("%V")) # Week of year
+ year = time.strftime("%Y")
+
+ # Init week scores
+ try:
+ self.channelweek[channel]
+ except:
+ self.channelweek[channel] = {}
+ try:
+ self.channelweek[channel][self.woy]
+ except:
+ self.channelweek[channel][self.woy] = {}
+ try:
+ self.channelweek[channel][self.woy][self.dow]
+ except:
+ self.channelweek[channel][self.woy][self.dow] = {}
+
+
+
+ def _initthrottle(self, irc, msg, args, channel):
+
+ self._initdayweekyear(channel)
+
+ if not self.leader.get(channel):
+ self.leader[channel] = None
+
+ # autoFriday?
+ if (not self.fridayMode.get(channel)):
+ self.fridayMode[channel] = False
+
+ if (not self.manualFriday.get(channel)):
+ self.manualFriday[channel] = False
+
+
+ if self.registryValue('autoFriday', channel) == True:
+ if int(time.strftime("%w")) == 5 and int(time.strftime("%H")) > 8 and int(time.strftime("%H")) < 17:
+ self.fridayMode[channel] = True
+ else:
+ self.fridayMode[channel] = False
+
+ # Miss probability
+ if self.registryValue('missProbability', channel):
+ self.missprobability[channel] = self.registryValue('missProbability', channel)
+ else:
+ self.missprobability[channel] = 0.2
+
+ # Reload time
+ if self.registryValue('reloadTime', channel):
+ self.reloadtime[channel] = self.registryValue('reloadTime', channel)
+ else:
+ self.reloadtime[channel] = 5
+
+ if self.fridayMode[channel] == False and self.manualFriday[channel] == False:
+ # Init min throttle[currentChannel] and max throttle[currentChannel]
+ if self.registryValue('minthrottle', channel):
+ self.minthrottle[channel] = self.registryValue('minthrottle', channel)
+ else:
+ self.minthrottle[channel] = 30
+
+ if self.registryValue('maxthrottle', channel):
+ self.maxthrottle[channel] = self.registryValue('maxthrottle', channel)
+ else:
+ self.maxthrottle[channel] = 300
+
+ else:
+ self.minthrottle[channel] = 3
+ self.maxthrottle[channel] = 60
+
+ self.throttle[channel] = random.randint(self.minthrottle[channel], self.maxthrottle[channel])
+
+
+ def starthunt(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:
+
+ # First of all, let's read the score if needed
+ self._read_scores(currentChannel)
+
+ self._initthrottle(irc, msg, args, currentChannel)
+
+ # Init saved scores
+ try:
+ self.channelscores[currentChannel]
+ except:
+ self.channelscores[currentChannel] = {}
+
+ # Init saved times
+ try:
+ self.channeltimes[currentChannel]
+ except:
+ self.channeltimes[currentChannel] = {}
+
+ # Init saved times
+ try:
+ self.channelworsttimes[currentChannel]
+ except:
+ self.channelworsttimes[currentChannel] = {}
+
+ # Init times
+ self.toptimes[currentChannel] = {}
+ self.worsttimes[currentChannel] = {}
+
+ # Init bangdelay
+ self.times[currentChannel] = False
+
+ # Init lastSpoke
+ self.lastSpoke[currentChannel] = time.time()
+
+ # Reinit current hunt scores
+ if self.scores.get(currentChannel):
+ self.scores[currentChannel] = {}
+
+ # Reinit reloading
+ self.reloading[currentChannel] = {}
+
+ # No duck launched
+ self.duck[currentChannel] = False
+
+ # Hunt started
+ self.started[currentChannel] = True
+
+ # Init shoots
+ self.shoots[currentChannel] = 0
+
+ # Init averagetime
+ self.averagetime[currentChannel] = 0;
+
+ # Init schedule
+
+ # First of all, stop the scheduler if it was still running
+ try:
+ schedule.removeEvent('DuckHunt_' + currentChannel)
+ except KeyError:
+ pass
+
+ # Then restart it
+ def myEventCaller():
+ self._launchEvent(irc, msg)
+ try:
+ schedule.addPeriodicEvent(myEventCaller, 5, 'DuckHunt_' + currentChannel, False)
+ except AssertionError:
+ pass
+
+ irc.reply("The hunt starts now!")
+ else:
+ irc.error('You have to be on a channel')
+ starthunt = wrap(starthunt)
+
+
+ def _launchEvent(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[currentChannel] + self.throttle[currentChannel]:
+ self._launch(irc, msg, '')
+
+
+
+ def stophunt(self, irc, msg, args):
+ """
+ Stops the current hunt
+ """
+
+ currentChannel = msg.args[0]
+ if irc.isChannel(currentChannel):
+ if (self.started.get(currentChannel) == True):
+ self._end(irc, msg, args)
+
+ # If someone uses the stop command,
+ # we stop the scheduler, even if autoRestart is enabled
+ try:
+ schedule.removeEvent('DuckHunt_' + currentChannel)
+ except KeyError:
+ irc.reply('Error: the spammer wasn\'t running! This is a bug.')
+ except:
+ irc.reply('Nothing to stop: there\'s no hunt right now.')
+ else:
+ irc.error('You have to be on a channel')
+ stophunt = wrap(stophunt)
+
+ def fridaymode(self, irc, msg, args, channel, status):
+ """
+ []
+ Enable/disable friday mode! (there are lots of ducks on friday :))
+ """
+ if irc.isChannel(channel):
+
+ if (status == 'status'):
+ irc.reply('Manual friday mode for ' + channel + ' is ' + str(self.manualFriday.get(channel)));
+ irc.reply('Auto friday mode for ' + channel + ' is ' + str(self.fridayMode.get(channel)));
+ else:
+ if (self.manualFriday.get(channel) == None or self.manualFriday[channel] == False):
+ self.manualFriday[channel] = True
+ irc.reply("Friday mode is now enabled! Shoot alllllllllllll the ducks!")
+ else:
+ self.manualFriday[channel] = False
+ irc.reply("Friday mode is now disabled.")
+
+ self._initthrottle(irc, msg, args, channel)
+ else:
+ irc.error('You have to be on a channel')
+
+
+ fridaymode = wrap(fridaymode, ['channel', 'admin', optional('anything')])
+
+ 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)
+
+
+
+ def score(self, irc, msg, args, nick):
+ """
+
+
+ Shows the score for a given nick
+ """
+ currentChannel = msg.args[0]
+ if irc.isChannel(currentChannel):
+ self._read_scores(currentChannel)
+ try:
+ self.channelscores[currentChannel]
+ except:
+ self.channelscores[currentChannel] = {}
+
+
+ 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, ['nick'])
+
+
+
+ def mergescores(self, irc, msg, args, channel, nickto, nickfrom):
+ """
+ []
+
+ nickto gets the points of nickfrom and nickfrom is removed from the scorelist
+ """
+ if irc.isChannel(channel):
+ self._read_scores(channel)
+
+ # Total scores
+ try:
+ self.channelscores[channel][nickto] += self.channelscores[channel][nickfrom]
+ del self.channelscores[channel][nickfrom]
+ self._write_scores(channel)
+ irc.reply("Total scores merged")
+
+ except:
+ irc.error("Can't merge total scores")
+
+ # Day scores
+ try:
+ self._initdayweekyear(channel)
+ day = self.dow
+ week = self.woy
+
+ try:
+ self.channelweek[channel][week][day][nickto] += self.channelweek[channel][week][day][nickfrom]
+ except:
+ self.channelweek[channel][week][day][nickto] = self.channelweek[channel][week][day][nickfrom]
+
+ del self.channelweek[channel][week][day][nickfrom]
+ self._write_scores(channel)
+ irc.reply("Day scores merged")
+
+ except:
+ irc.error("Can't merge day scores")
+
+
+
+ else:
+ irc.error('You have to be on a channel')
+
+
+ mergescores = wrap(mergescores, ['channel', 'nick', 'nick', 'admin'])
+
+
+
+ def mergetimes(self, irc, msg, args, channel, nickto, nickfrom):
+ """
+ []
+
+ nickto gets the best time of nickfrom if nickfrom time is better than nickto time, and nickfrom is removed from the timelist. Also works with worst times.
+ """
+ if irc.isChannel(channel):
+ try:
+ self._read_scores(channel)
+
+ # Merge best times
+ if self.channeltimes[channel][nickfrom] < self.channeltimes[channel][nickto]:
+ self.channeltimes[channel][nickto] = self.channeltimes[channel][nickfrom]
+ del self.channeltimes[channel][nickfrom]
+
+ # Merge worst times
+ if self.channelworsttimes[channel][nickfrom] > self.channelworsttimes[channel][nickto]:
+ self.channelworsttimes[channel][nickto] = self.channelworsttimes[channel][nickfrom]
+ del self.channelworsttimes[channel][nickfrom]
+
+ self._write_scores(channel)
+
+ irc.replySuccess()
+
+ except:
+ irc.replyError()
+
+
+ else:
+ irc.error('You have to be on a channel')
+
+
+ mergetimes = wrap(mergetimes, ['channel', 'nick', 'nick', 'admin'])
+
+
+
+ def rmtime(self, irc, msg, args, channel, nick):
+ """
+ []
+
+ Remove 's best time
+ """
+ if irc.isChannel(channel):
+ self._read_scores(channel)
+ del self.channeltimes[channel][nick]
+ self._write_scores(channel)
+ irc.replySuccess()
+
+ else:
+ irc.error('Are you sure ' + str(channel) + ' is a channel?')
+
+ rmtime = wrap(rmtime, ['channel', 'nick', 'admin'])
+
+
+
+ def rmscore(self, irc, msg, args, channel, nick):
+ """
+ []
+
+ Remove 's score
+ """
+ if irc.isChannel(channel):
+ try:
+ self._read_scores(channel)
+ del self.channelscores[channel][nick]
+ self._write_scores(channel)
+ irc.replySuccess()
+
+ except:
+ irc.replyError()
+
+ else:
+ irc.error('Are you sure this is a channel?')
+
+ rmscore = wrap(rmscore, ['channel', 'nick', 'admin'])
+
+
+
+
+ def dayscores(self, irc, msg, args, channel):
+ """
+ []
+
+ Shows the score list of the day for .
+ """
+
+ if irc.isChannel(channel):
+
+ self._read_scores(channel)
+ self._initdayweekyear(channel)
+ day = self.dow
+ week = self.woy
+
+ if self.channelweek.get(channel):
+ if self.channelweek[channel].get(week):
+ if self.channelweek[channel][week].get(day):
+ # Getting all scores, to get the winner of the week
+ msgstring = ''
+ scores = sorted(iter(self.channelweek[channel][week][day].items()), key=lambda k_v2:(k_v2[1],k_v2[0]), reverse=True)
+ for item in scores:
+ msgstring += "x" + item[0] + "x: "+ str(item[1]) + " | "
+
+ if msgstring != "":
+ irc.reply("Scores for today: " + msgstring)
+ else:
+ irc.reply("There aren't any day scores for today yet.")
+ else:
+ irc.reply("There aren't any day scores for today yet.")
+ else:
+ irc.reply("There aren't any day scores for today yet.")
+ else:
+ irc.reply("There aren't any day scores for this channel yet.")
+ else:
+ irc.reply("Are you sure this is a channel?")
+ dayscores = wrap(dayscores, ['channel'])
+
+
+
+ def weekscores(self, irc, msg, args, week, nick, channel):
+ """
+ [] [] []
+
+ Shows the score list of the week for . If is provided, it will only show 's scores.
+ """
+
+ if irc.isChannel(channel):
+
+ self._read_scores(channel)
+ weekscores = {}
+
+ if (not week):
+ week = self.woy
+
+ if self.channelweek.get(channel):
+ if self.channelweek[channel].get(week):
+ # Showing the winner for each day
+ if not nick:
+ msgstring = ''
+ # for each day of week
+ for i in (1,2,3,4,5,6,7):
+ if self.channelweek[channel][week].get(i):
+ # Getting winner of the day
+ winnernick, winnerscore = max(iter(self.channelweek[channel][week][i].items()), key=lambda k_v:(k_v[1],k_v[0]))
+ msgstring += self.dayname[i - 1] + ": x" + winnernick + "x ("+ str(winnerscore) + ") | "
+
+ # Getting all scores, to get the winner of the week
+ for i, players in self.channelweek[channel][week].items():
+ for player, value in players.items():
+ weekscores.setdefault(player, 0)
+ weekscores[player] += value
+
+ if msgstring != "":
+ irc.reply("Scores for week " + str(week) + ": " + msgstring)
+ # Who's the winner at this point?
+ winnernick, winnerscore = max(iter(weekscores.items()), key=lambda k_v1:(k_v1[1],k_v1[0]))
+ irc.reply("Leader: x%sx with %i points." % (winnernick, winnerscore))
+
+ else:
+ irc.reply("There aren't any week scores for this week yet.")
+ else:
+ # Showing the scores of
+ msgstring = ''
+ total = 0
+ for i in (1,2,3,4,5,6,7):
+ if self.channelweek[channel][week].get(i):
+ if self.channelweek[channel][week][i].get(nick):
+ msgstring += self.dayname[i - 1] + ": "+ str(self.channelweek[channel][week][i].get(nick)) + " | "
+ total += self.channelweek[channel][week][i].get(nick)
+
+ if msgstring != "":
+ irc.reply(nick + " scores for week " + str(self.woy) + ": " + msgstring)
+ irc.reply("Total: " + str(total) + " points.")
+ else:
+ irc.reply("There aren't any week scores for this nick.")
+
+
+ else:
+ irc.reply("There aren't any week scores for this week yet.")
+ else:
+ irc.reply("There aren't any week scores for this channel yet.")
+ else:
+ irc.reply("Are you sure this is a channel?")
+ weekscores = wrap(weekscores, [optional('int'), optional('nick'), 'channel'])
+
+
+
+ def listscores(self, irc, msg, args, size, channel):
+ """
+ [] []
+
+ Shows the -sized score list for (or for the current channel if no channel is given)
+ """
+
+ if irc.isChannel(channel):
+ try:
+ self.channelscores[channel]
+ except:
+ self.channelscores[channel] = {}
+
+ self._read_scores(channel)
+
+ # How many results do we display?
+ if (not size):
+ listsize = self.toplist
+ else:
+ listsize = size
+
+ # Sort the scores (reversed: the higher the better)
+ scores = sorted(iter(self.channelscores[channel].items()), key=lambda k_v9:(k_v9[1],k_v9[0]), reverse=True)
+ del scores[listsize:]
+
+ 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 top-" + str(listsize) + " scores for " + channel + " ~ >o_/")
+ irc.reply(msgstring)
+ else:
+ irc.reply("There aren't any scores for this channel yet.")
+ else:
+ irc.reply("Are you sure this is a channel?")
+ listscores = wrap(listscores, [optional('int'), 'channel'])
+
+
+ def total(self, irc, msg, args, channel):
+ """
+ Shows the total amount of ducks shot in (or in the current channel if no channel is given)
+ """
+
+ if irc.isChannel(channel):
+ self._read_scores(channel)
+ if (self.channelscores.get(channel)):
+ scores = self.channelscores[channel]
+ total = 0
+ for player, value in scores.items():
+ total += value
+ irc.reply(str(total) + " ducks have been shot in " + channel + "!")
+ else:
+ irc.reply("There are no scores for this channel yet")
+
+ else:
+ irc.reply("Are you sure this is a channel?")
+ total = wrap(total, ['channel'])
+
+
+ def listtimes(self, irc, msg, args, size, channel):
+ """
+ [] []
+
+ Shows the -sized time list for (or for the current channel if no channel is given)
+ """
+
+ if irc.isChannel(channel):
+ self._read_scores(channel)
+
+ try:
+ self.channeltimes[channel]
+ except:
+ self.channeltimes[channel] = {}
+
+ try:
+ self.channelworsttimes[channel]
+ except:
+ self.channelworsttimes[channel] = {}
+
+ # How many results do we display?
+ if (not size):
+ listsize = self.toplist
+ else:
+ listsize = size
+
+ # Sort the times (not reversed: the lower the better)
+ times = sorted(iter(self.channeltimes[channel].items()), key=lambda k_v10:(k_v10[1],k_v10[0]), reverse=False)
+ del times[listsize:]
+
+ msgstring = ""
+ for item in times:
+ # Same as in listscores for the xnickx
+ #msgstring += "x" + item[0] + "x: "+ str(round(item[1],2)) + " | "
+ msgstring.join("x{0}x: {1} | ".format(item[0], str(round(item[1],2))))
+ if item:
+ if msgstring:
+ irc.reply("\_o< ~ DuckHunt top-" + str(listsize) + " times for " + channel + " ~ >o_/")
+ irc.reply(msgstring)
+ else:
+ irc.reply("There aren't any best times for this channel yet.")
+
+
+ times = sorted(iter(self.channelworsttimes[channel].items()), key=lambda k_v11:(k_v11[1],k_v11[0]), reverse=True)
+ del times[listsize:]
+
+ msgstring = ""
+ for item in times:
+ # Same as in listscores for the xnickx
+ #msgstring += "x" + item[0] + "x: "+ time.strftime('%H:%M:%S', time.gmtime(item[1])) + ", "
+ roundseconds = round(item[1])
+ delta = datetime.timedelta(seconds=roundseconds)
+ msgstring += "x" + item[0] + "x: " + str(delta) + " | "
+ if msgstring != "":
+ irc.reply("\_o< ~ DuckHunt top-" + str(listsize) + " longest times for " + channel + " ~ >o_/")
+ irc.reply(msgstring)
+ else:
+ irc.reply("There aren't any longest times for this channel yet.")
+
+
+ else:
+ irc.reply("Are you sure this is a channel?")
+ listtimes = wrap(listtimes, [optional('int'), 'channel'])
+
+
+
+ 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)
+
+
+
+ def bang(self, irc, msg, args):
+ """
+ Shoots the duck!
+ """
+ currentChannel = msg.args[0]
+
+ if irc.isChannel(currentChannel):
+ if(self.started.get(currentChannel) == True):
+
+ # bangdelay: how much time between the duck was launched and this shot?
+ if self.times[currentChannel]:
+ bangdelay = time.time() - self.times[currentChannel]
+ else:
+ bangdelay = False
+
+
+ # Is the player reloading?
+ if (self.reloading[currentChannel].get(msg.nick) and time.time() - self.reloading[currentChannel][msg.nick] < self.reloadtime[currentChannel]):
+ irc.reply("%s, you are reloading... (Reloading takes %i seconds)" % (msg.nick, self.reloadtime[currentChannel]))
+ return 0
+
+
+ # This player is now reloading
+ self.reloading[currentChannel][msg.nick] = time.time();
+
+ # There was a duck
+ if (self.duck[currentChannel] == True):
+
+ # Did the player missed it?
+ if (random.random() < self.missprobability[currentChannel]):
+ irc.reply("%s, you missed the duck!" % (msg.nick))
+ else:
+
+ # 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))
+
+ self.averagetime[currentChannel] += 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
+
+
+ # Now save the bang delay for the player (if it's worst than it's previous bangdelay)
+ try:
+ previoustime = self.worsttimes[currentChannel][msg.nick]
+ if(bangdelay > previoustime):
+ self.worsttimes[currentChannel][msg.nick] = bangdelay
+ except:
+ self.worsttimes[currentChannel][msg.nick] = bangdelay
+
+
+ self.duck[currentChannel] = False
+
+ # Reset the basetime for the waiting time before the next duck
+ self.lastSpoke[currentChannel] = time.time()
+
+ if self.registryValue('ducks', currentChannel):
+ maxShoots = self.registryValue('ducks', currentChannel)
+ else:
+ maxShoots = 10
+
+ # 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):
+ # This code shouldn't be here
+ self.started[currentChannel] = True
+ self._initthrottle(irc, msg, args, currentChannel)
+ if self.scores.get(currentChannel):
+ self.scores[currentChannel] = {}
+ if self.reloading.get(currentChannel):
+ self.reloading[currentChannel] = {}
+
+ self.averagetime[currentChannel] = 0
+
+
+ # 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
+
+ # Base message
+ message = 'There was no duck!'
+
+ # Adding additional message if kick
+ if self.registryValue('kickMode', currentChannel) and irc.nick in irc.state.channels[currentChannel].ops:
+ message += ' You just shot yourself!'
+
+ # Adding nick and score
+ message += " %s: %i" % (msg.nick, self.scores[currentChannel][msg.nick])
+
+ # If we were able to have a bangdelay (ie: a duck was launched before someone did bang)
+ if (bangdelay):
+ # Adding time
+ message += " (" + str(round(bangdelay,2)) + " seconds)"
+
+ # If kickMode is enabled for this channel, and the bot have op capability, let's kick!
+ if self.registryValue('kickMode', currentChannel) and irc.nick in irc.state.channels[currentChannel].ops:
+ irc.queueMsg(ircmsgs.kick(currentChannel, msg.nick, message))
+ else:
+ # Else, just say it
+ irc.reply(message)
+
+
+ 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')
+
+ bang = wrap(bang)
+
+
+ def doPrivmsg(self, irc, msg):
+ currentChannel = msg.args[0]
+ if irc.isChannel(msg.args[0]):
+ if (msg.args[1] == '\_o< quack!'):
+ message = msg.nick + ", don't pretend to be me!";
+ # If kickMode is enabled for this channel, and the bot have op capability, let's kick!
+ if self.registryValue('kickMode', currentChannel) and irc.nick in irc.state.channels[currentChannel].ops:
+ irc.queueMsg(ircmsgs.kick(currentChannel, msg.nick, message))
+ else:
+ # Else, just say it
+ irc.reply(message)
+
+
+
+
+ def _end(self, irc, msg, args):
+ """
+ End of the hunt (is called when the hunts stop "naturally" or when someone uses the !stop command)
+ """
+
+ currentChannel = msg.args[0]
+
+ # End the hunt
+ self.started[currentChannel] = False
+
+ try:
+ self.channelscores[currentChannel]
+ except:
+ self.channelscores[currentChannel] = {}
+
+
+ if not self.registryValue('autoRestart', currentChannel):
+ irc.reply("The hunt stops now!")
+
+ # Showing scores
+ if (self.scores.get(currentChannel)):
+
+ # Getting winner
+ winnernick, winnerscore = max(iter(self.scores.get(currentChannel).items()), key=lambda k_v12:(k_v12[1],k_v12[0]))
+ if self.registryValue('ducks', currentChannel):
+ maxShoots = self.registryValue('ducks', currentChannel)
+ else:
+ maxShoots = 10
+
+ # Is there a perfect?
+ if (winnerscore == maxShoots):
+ irc.reply("\o/ %s: %i ducks out of %i: perfect!!! +%i \o/" % (winnernick, winnerscore, maxShoots, self.perfectbonus))
+ self.scores[currentChannel][winnernick] += self.perfectbonus
+ else:
+ # Showing scores
+ #irc.reply("Winner: %s with %i points" % (winnernick, winnerscore))
+ #irc.reply(self.scores.get(currentChannel))
+ #TODO: Better display
+ irc.reply(sorted(iter(self.scores.get(currentChannel).items()), key=lambda k_v4:(k_v4[1],k_v4[0]), reverse=True))
+
+
+
+ # 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(iter(self.channeltimes.get(currentChannel).items()), key=lambda k_v5:(k_v5[1],k_v5[0]))
+
+ # Showing best time
+ recordmsg = ''
+ if (self.toptimes.get(currentChannel)):
+ key,value = min(iter(self.toptimes.get(currentChannel).items()), key=lambda k_v6:(k_v6[1],k_v6[0]))
+ 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))
+
+ # Getting channel worst time (to see if the worst time of this hunt is worst)
+ channelworstnick = None
+ channelworsttime = None
+ if self.channelworsttimes.get(currentChannel):
+ channelworstnick, channelworsttime = max(iter(self.channelworsttimes.get(currentChannel).items()), key=lambda k_v7:(k_v7[1],k_v7[0]))
+
+
+ # Showing worst time
+ recordmsg = ''
+ if (self.worsttimes.get(currentChannel)):
+ key,value = max(iter(self.worsttimes.get(currentChannel).items()), key=lambda k_v8:(k_v8[1],k_v8[0]))
+ if (channelworsttime and value > channelworsttime):
+ recordmsg = '. This is the new longest time for this channel! (previous longest time was held by ' + channelworstnick + ' with ' + str(round(channelworsttime,2)) + ' seconds)'
+ else:
+ try:
+ if(value > self.channelworsttimes[currentChannel][key]):
+ recordmsg = ' (this is your new longest time in this channel! Your previous longest time was ' + str(round(self.channelworsttimes[currentChannel][key],2)) + ')'
+ except:
+ recordmsg = ''
+
+ # Only display worst time if something new
+ if (recordmsg != ''):
+ irc.reply("Longest time: %s with %.2f seconds%s" % (key, value, recordmsg))
+
+ # Showing average shooting time:
+ #if (self.shoots[currentChannel] > 1):
+ #irc.reply("Average shooting time: %.2f seconds" % ((self.averagetime[currentChannel] / self.shoots[currentChannel])))
+
+ # Write the scores and times to disk
+ self._calc_scores(currentChannel)
+ self._write_scores(currentChannel)
+
+ # Did someone took the lead?
+ weekscores = {}
+ if self.channelweek.get(currentChannel):
+ if self.channelweek[currentChannel].get(self.woy):
+ msgstring = ''
+ # for each day of week
+ for i in (1,2,3,4,5,6,7):
+ if self.channelweek[currentChannel][self.woy].get(i):
+ # Getting all scores, to get the winner of the week
+ for i, players in self.channelweek[currentChannel][self.woy].items():
+ for player, value in players.items():
+ weekscores.setdefault(player, 0)
+ weekscores[player] += value
+ winnernick, winnerscore = max(iter(weekscores.items()), key=lambda k_v3:(k_v3[1],k_v3[0]))
+ if (winnernick != self.leader[currentChannel]):
+ if self.leader[currentChannel] != None:
+ irc.reply("%s took the lead for the week over %s with %i points." % (winnernick, self.leader[currentChannel], winnerscore))
+ else:
+ irc.reply("%s has the lead for the week with %i points." % (winnernick, winnerscore))
+ self.leader[currentChannel] = winnernick
+
+
+
+ else:
+ irc.reply("Not a single duck was shot during this hunt!")
+
+ # Reinit current hunt scores
+ if self.scores.get(currentChannel):
+ self.scores[currentChannel] = {}
+
+ # Reinit current hunt times
+ if self.toptimes.get(currentChannel):
+ self.toptimes[currentChannel] = {}
+ if self.worsttimes.get(currentChannel):
+ self.worsttimes[currentChannel] = {}
+
+ # No duck lauched
+ self.duck[currentChannel] = False
+
+ # Reinit number of shoots
+ self.shoots[currentChannel] = 0
+
+
+
+ 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):
+
+ # Store the time when the duck has been launched
+ self.times[currentChannel] = time.time()
+
+ # Store the fact that there's a duck now
+ self.duck[currentChannel] = True
+
+ # Send message directly (instead of queuing it with irc.reply)
+ irc.sendMsg(ircmsgs.privmsg(currentChannel, "\_o< quack!"))
+
+ # Define a new throttle[currentChannel] for the next launch
+ self.throttle[currentChannel] = random.randint(self.minthrottle[currentChannel], self.maxthrottle[currentChannel])
+
+ try:
+ self.shoots[currentChannel] += 1
+ except:
+ self.shoots[currentChannel] = 1
+ else:
+
+ irc.reply("Already a duck")
+ else:
+ irc.reply("The hunt has not started yet!")
+ else:
+ irc.error('You have to be on a channel')
+
+
+Class = DuckHunt
+
+# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff --git a/DuckHunt/test.py b/DuckHunt/test.py
new file mode 100644
index 0000000..20ebafe
--- /dev/null
+++ b/DuckHunt/test.py
@@ -0,0 +1,47 @@
+###
+# 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.test import *
+
+class DuckHuntTestCase(ChannelPluginTestCase):
+ plugins = ('DuckHunt',)
+
+
+ def tests(self):
+ self.assertResponse("bang", "There is no hunt right now! You can start a hunt with the 'start' command")
+ self.assertResponse("stop", "Nothing to stop: there's no hunt right now.")
+ self.assertResponse("start", "The hunt starts now!")
+ self.assertResponse("start", "There is already a hunt right now!")
+ self.assertRegexp("bang", "^There was no duck!")
+ self.assertResponse("stop", "The hunt stops now!")
+ self.assertNotError("listscores")
+ self.assertNotError("weekscores")
+
+
+# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: