Ported old dywypi code into a NetHack plugin.
authorEevee <git@veekun.com>
Tue, 2 Feb 2010 07:39:05 +0000 (23:39 -0800)
committerEevee <git@veekun.com>
Tue, 2 Feb 2010 07:39:05 +0000 (23:39 -0800)
Could use some, uh, configuration.  But whatever.  Works!

plugins/NetHack/README.txt [new file with mode: 0644]
plugins/NetHack/__init__.py [new file with mode: 0644]
plugins/NetHack/config.py [new file with mode: 0644]
plugins/NetHack/plugin.py [new file with mode: 0644]
plugins/NetHack/test.py [new file with mode: 0644]

diff --git a/plugins/NetHack/README.txt b/plugins/NetHack/README.txt
new file mode 100644 (file)
index 0000000..96b7ea2
--- /dev/null
@@ -0,0 +1,2 @@
+Provides a live feed of deaths and events of interest from a local NetHack
+server.
diff --git a/plugins/NetHack/__init__.py b/plugins/NetHack/__init__.py
new file mode 100644 (file)
index 0000000..d89bbcf
--- /dev/null
@@ -0,0 +1,65 @@
+###
+# Copyright (c) 2010, Alex "Eevee" Munroe
+# 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.
+
+###
+
+"""
+Provides a live feed of deaths and other NetHack events.
+"""
+
+import supybot
+import supybot.world as world
+
+# 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__ = "0.1"
+
+# XXX Replace this with an appropriate author or supybot.Author instance.
+__author__ = supybot.Author('Alex Munroe', 'Eevee', 'git@veekun.com')
+
+# 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__ = '' # 'http://supybot.com/Members/yourname/NetHack/download'
+
+import config
+import plugin
+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:
+    import test
+
+Class = plugin.Class
+configure = config.configure
+
+
+# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
diff --git a/plugins/NetHack/config.py b/plugins/NetHack/config.py
new file mode 100644 (file)
index 0000000..4b41968
--- /dev/null
@@ -0,0 +1,49 @@
+###
+# Copyright (c) 2010, Alex "Eevee" Munroe
+# 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('NetHack', True)
+
+
+NetHack = conf.registerPlugin('NetHack')
+# This is where your configuration variables (if any) should go.  For example:
+# conf.registerGlobalValue(NetHack, 'someConfigVariableName',
+#     registry.Boolean(False, """Help for someConfigVariableName."""))
+
+
+# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
diff --git a/plugins/NetHack/plugin.py b/plugins/NetHack/plugin.py
new file mode 100644 (file)
index 0000000..6560f80
--- /dev/null
@@ -0,0 +1,240 @@
+###
+# Copyright (c) 2010, Alex "Eevee" Munroe
+# 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.utils as utils
+from supybot.commands import *
+import supybot.plugins as plugins
+import supybot.ircutils as ircutils
+import supybot.ircmsgs as ircmsgs
+import supybot.callbacks as callbacks
+import supybot.schedule as schedule
+
+from glob import glob
+import os
+import os.path
+import re
+
+# dnum, used in xlogfile
+dungeons = [
+    'the Dungeons of Doom',
+    'Gehennom',
+    'the Gnomish Mines',
+    'the Quest',
+    'Sokoban',
+    'Fort Ludios',
+    "Vlad's Tower",
+    'the Elemental Planes',
+]
+
+# achievements, used by livelog
+achievements = [
+    'completed the Quest and obtained the Bell of Opening',
+    'entered Gehennom',
+    'obtained the Candelabrum of Invocation',
+    'obtained the Book of the Dead',
+    'performed the Invocation',
+    'obtained the Amulet of Yendor',
+    'reached the Elemental Planes',
+    'reached the Astral Plane',
+    'ascended to a higher plane of existence',
+    "completed Mine's End",
+    'completed Sokoban',
+    'slew Medusa',
+]
+
+def parse_xlog(line):
+    line = line.strip()
+    data = {}
+    for keyval in line.split(':'):
+        key, val = keyval.split('=', 1)
+        data[key] = val
+
+    ### Extras for us
+    # Original and ending gender/alignment are tracked separately
+    if data['gender0'] == data['gender']:
+        data['gender_delta'] = data['gender']
+    else:
+        data['gender_delta'] = "%(gender0)s->%(gender)s" % data
+
+    if data['align0'] == data['align']:
+        data['align_delta'] = data['align']
+    else:
+        data['align_delta'] = "%(align0)s->%(align)s" % data
+
+    data['level_desc'] = "%s dlvl %s" % (dungeons[ int(data['deathdnum']) ],
+                                         data['deathlev'])
+    if data['deathlev'] != data['maxlvl']:
+        data['level_desc'] += " (deepest dlvl: %(maxlvl)s)" % data
+
+    # Human-readable time played
+    realtime = int(data['realtime'])
+    time_secs = realtime % 60;  realtime //= 60
+    time_mins = realtime % 60;  realtime //= 60
+    time_hrs  = realtime % 24;  realtime //= 24
+    time_days = realtime
+    # Don't need seconds
+    if time_secs >= 30:
+        time_mins += 1
+    # Construct '0d 0h 5m 12s', then lop off the 0x bits
+    data['realtime_pretty'] = re.sub(
+        "^(0. )+",
+        "",
+        "%dd %dh %dm" % (time_days, time_hrs, time_mins)
+    )
+    return data
+
+
+def parse_livelog(line):
+    line = line.strip()
+    data = {}
+    for keyval in line.split(':'):
+        key, val = keyval.split('=', 1)
+        data[key] = val
+
+    return data
+    
+def livelog_announcement(livelog):
+    # achievement gained
+    if 'achieve_diff' in livelog:
+        # these are stored as 0xABC
+        achieve_diff = int(livelog['achieve_diff'], 16)
+
+        # each item in the achievement list is encoded as that number bit
+        for i, achievement in enumerate(achievements):
+            if achieve_diff & (1 << i):
+                return "{player} just {achievement}, on turn {turns}!".format(
+                           achievement=achievement, **livelog)
+
+        # achieve_diff is zero?  nothing changed?  can't happen, but..
+        return "{0} just accomplished nothing!".format(player)
+
+    # wishes
+    if 'wish' in livelog:
+        return "%(player)s just wished for %(wish)s, on turn %(turns)s." % livelog
+
+    # kill a player ghost
+    if 'bones_killed' in livelog:
+        return "%(player)s just killed the %(bones_monst)s of %(bones_killed)s, " \
+               "the former %(bones_rank)s, on turn %(turns)s on dlvl %(dlev)s." % livelog
+
+    # killed a unique monster
+    # may result in spam for the three horsemen..
+    if 'killed_uniq' in livelog:
+        if livelog['killed_uniq'] == 'Medusa':
+            # Medusa is already an achievement.  No need to announce twice
+            return None
+        return "%(player)s has just slain %(killed_uniq)s on turn %(turns)s!" % livelog
+
+    # stole something
+    if 'shoplifted' in livelog:
+        return "%(player)s just stole %(shoplifted)s zorkmids' worth of merchandise " \
+               "from %(shopkeeper)s's %(shop)s, on turn %(turns)s.  Tut tut." % livelog
+
+    # default??
+    return "%(player)s just did something-or-other." % livelog
+
+report_template = "{name} ({role} {race} {gender_delta} {align_delta}): " \
+                  "{death} on {level_desc}.  {points} points in {turns} turns, " \
+                  "wasting {realtime_pretty}.  {dumplog}"
+
+
+CONFIG_PLAYGROUND = '/opt/nethack.veekun.com/nethack/var'
+CONFIG_USERDATA_FILE = '/opt/nethack.veekun.com/dgldir/userdata'
+CONFIG_USERDATA_WEB = 'http://nethack.veekun.com/userdata'
+CONFIG_CHANNEL = '#cafe'
+class NetHack(callbacks.Plugin):
+    """Add the help for "@plugin help NetHack" here
+    This should describe *how* to use this plugin."""
+    def __init__(self, irc):
+        self.__parent = super(NetHack, self)
+        self.__parent.__init__(irc)
+
+        self.xlog = open(os.path.join(CONFIG_PLAYGROUND, 'xlogfile'))
+        self.livelog = open(os.path.join(CONFIG_PLAYGROUND, 'livelog'))
+        self.xlog.seek(0, os.SEEK_END)
+        self.livelog.seek(0, os.SEEK_END)
+
+        # Remove the event first, in case this is a reload
+        schedule.removePeriodicEvent('nethack-log-ping')
+
+        def callback():
+            self._checkLogs(irc)
+        schedule.addPeriodicEvent(callback, 10, name='nethack-log-ping')
+
+    def _checkLogs(self, irc):
+        """Checks the files for new lines and, if there be any, prints them to
+        IRC.
+
+        Actual work is all done here.
+        """
+
+        # Check xlogfile
+        self.xlog.seek(0, os.SEEK_CUR)
+        line = self.xlog.readline()
+        if line:
+            data = parse_xlog(line)
+
+            # Find dumplog
+            dumplog_paths = glob(
+                os.path.join(CONFIG_USERDATA_FILE,
+                             data['name'],
+                             'dumplog',
+                             data['starttime'])
+                + '*'
+            )
+            if dumplog_paths:
+                (_, dumplog_file) = os.path.split(dumplog_paths[0])
+                dumplog_url = '{base}/{name}/dumplog/{file}'.format(
+                    base=CONFIG_USERDATA_WEB,
+                    name=data['name'],
+                    file=dumplog_file,
+                )
+            else:
+                dumplog_url = "Can't find dumplog  :("
+
+            report = report_template.format(dumplog=dumplog_url, **data)
+            msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
+            irc.queueMsg(msg)
+
+        # Check livelog
+        self.livelog.seek(0, os.SEEK_CUR)
+        line = self.livelog.readline()
+        if line:
+            data = parse_livelog(line)
+            report = livelog_announcement(data)
+            if report:
+                msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
+                irc.queueMsg(msg)
+
+
+Class = NetHack
+
+
+# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff --git a/plugins/NetHack/test.py b/plugins/NetHack/test.py
new file mode 100644 (file)
index 0000000..da2694f
--- /dev/null
@@ -0,0 +1,37 @@
+###
+# Copyright (c) 2010, Alex "Eevee" Munroe
+# 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 NetHackTestCase(PluginTestCase):
+    plugins = ('NetHack',)
+
+
+# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: