X-Git-Url: http://git.veekun.com/zzz-dywypi.git/blobdiff_plain/b6f792df18514e68da6d73196cfe5e571ebe6abb..1c8551edc2afecf3a4b1251a59bd25f09d4c1926:/plugins/NetHack/plugin.py diff --git a/plugins/NetHack/plugin.py b/plugins/NetHack/plugin.py new file mode 100644 index 0000000..6560f80 --- /dev/null +++ b/plugins/NetHack/plugin.py @@ -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: