Ported old dywypi code into a NetHack plugin.
[zzz-dywypi.git] / plugins / NetHack / plugin.py
1 ###
2 # Copyright (c) 2010, Alex "Eevee" Munroe
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are met:
7 #
8 # * Redistributions of source code must retain the above copyright notice,
9 # this list of conditions, and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions, and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 # * Neither the name of the author of this software nor the name of
14 # contributors to this software may be used to endorse or promote products
15 # derived from this software without specific prior written consent.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 ###
30
31 import supybot.utils as utils
32 from supybot.commands import *
33 import supybot.plugins as plugins
34 import supybot.ircutils as ircutils
35 import supybot.ircmsgs as ircmsgs
36 import supybot.callbacks as callbacks
37 import supybot.schedule as schedule
38
39 from glob import glob
40 import os
41 import os.path
42 import re
43
44 # dnum, used in xlogfile
45 dungeons = [
46 'the Dungeons of Doom',
47 'Gehennom',
48 'the Gnomish Mines',
49 'the Quest',
50 'Sokoban',
51 'Fort Ludios',
52 "Vlad's Tower",
53 'the Elemental Planes',
54 ]
55
56 # achievements, used by livelog
57 achievements = [
58 'completed the Quest and obtained the Bell of Opening',
59 'entered Gehennom',
60 'obtained the Candelabrum of Invocation',
61 'obtained the Book of the Dead',
62 'performed the Invocation',
63 'obtained the Amulet of Yendor',
64 'reached the Elemental Planes',
65 'reached the Astral Plane',
66 'ascended to a higher plane of existence',
67 "completed Mine's End",
68 'completed Sokoban',
69 'slew Medusa',
70 ]
71
72 def parse_xlog(line):
73 line = line.strip()
74 data = {}
75 for keyval in line.split(':'):
76 key, val = keyval.split('=', 1)
77 data[key] = val
78
79 ### Extras for us
80 # Original and ending gender/alignment are tracked separately
81 if data['gender0'] == data['gender']:
82 data['gender_delta'] = data['gender']
83 else:
84 data['gender_delta'] = "%(gender0)s->%(gender)s" % data
85
86 if data['align0'] == data['align']:
87 data['align_delta'] = data['align']
88 else:
89 data['align_delta'] = "%(align0)s->%(align)s" % data
90
91 data['level_desc'] = "%s dlvl %s" % (dungeons[ int(data['deathdnum']) ],
92 data['deathlev'])
93 if data['deathlev'] != data['maxlvl']:
94 data['level_desc'] += " (deepest dlvl: %(maxlvl)s)" % data
95
96 # Human-readable time played
97 realtime = int(data['realtime'])
98 time_secs = realtime % 60; realtime //= 60
99 time_mins = realtime % 60; realtime //= 60
100 time_hrs = realtime % 24; realtime //= 24
101 time_days = realtime
102 # Don't need seconds
103 if time_secs >= 30:
104 time_mins += 1
105 # Construct '0d 0h 5m 12s', then lop off the 0x bits
106 data['realtime_pretty'] = re.sub(
107 "^(0. )+",
108 "",
109 "%dd %dh %dm" % (time_days, time_hrs, time_mins)
110 )
111 return data
112
113
114 def parse_livelog(line):
115 line = line.strip()
116 data = {}
117 for keyval in line.split(':'):
118 key, val = keyval.split('=', 1)
119 data[key] = val
120
121 return data
122
123 def livelog_announcement(livelog):
124 # achievement gained
125 if 'achieve_diff' in livelog:
126 # these are stored as 0xABC
127 achieve_diff = int(livelog['achieve_diff'], 16)
128
129 # each item in the achievement list is encoded as that number bit
130 for i, achievement in enumerate(achievements):
131 if achieve_diff & (1 << i):
132 return "{player} just {achievement}, on turn {turns}!".format(
133 achievement=achievement, **livelog)
134
135 # achieve_diff is zero? nothing changed? can't happen, but..
136 return "{0} just accomplished nothing!".format(player)
137
138 # wishes
139 if 'wish' in livelog:
140 return "%(player)s just wished for %(wish)s, on turn %(turns)s." % livelog
141
142 # kill a player ghost
143 if 'bones_killed' in livelog:
144 return "%(player)s just killed the %(bones_monst)s of %(bones_killed)s, " \
145 "the former %(bones_rank)s, on turn %(turns)s on dlvl %(dlev)s." % livelog
146
147 # killed a unique monster
148 # may result in spam for the three horsemen..
149 if 'killed_uniq' in livelog:
150 if livelog['killed_uniq'] == 'Medusa':
151 # Medusa is already an achievement. No need to announce twice
152 return None
153 return "%(player)s has just slain %(killed_uniq)s on turn %(turns)s!" % livelog
154
155 # stole something
156 if 'shoplifted' in livelog:
157 return "%(player)s just stole %(shoplifted)s zorkmids' worth of merchandise " \
158 "from %(shopkeeper)s's %(shop)s, on turn %(turns)s. Tut tut." % livelog
159
160 # default??
161 return "%(player)s just did something-or-other." % livelog
162
163 report_template = "{name} ({role} {race} {gender_delta} {align_delta}): " \
164 "{death} on {level_desc}. {points} points in {turns} turns, " \
165 "wasting {realtime_pretty}. {dumplog}"
166
167
168 CONFIG_PLAYGROUND = '/opt/nethack.veekun.com/nethack/var'
169 CONFIG_USERDATA_FILE = '/opt/nethack.veekun.com/dgldir/userdata'
170 CONFIG_USERDATA_WEB = 'http://nethack.veekun.com/userdata'
171 CONFIG_CHANNEL = '#cafe'
172 class NetHack(callbacks.Plugin):
173 """Add the help for "@plugin help NetHack" here
174 This should describe *how* to use this plugin."""
175 def __init__(self, irc):
176 self.__parent = super(NetHack, self)
177 self.__parent.__init__(irc)
178
179 self.xlog = open(os.path.join(CONFIG_PLAYGROUND, 'xlogfile'))
180 self.livelog = open(os.path.join(CONFIG_PLAYGROUND, 'livelog'))
181 self.xlog.seek(0, os.SEEK_END)
182 self.livelog.seek(0, os.SEEK_END)
183
184 # Remove the event first, in case this is a reload
185 schedule.removePeriodicEvent('nethack-log-ping')
186
187 def callback():
188 self._checkLogs(irc)
189 schedule.addPeriodicEvent(callback, 10, name='nethack-log-ping')
190
191 def _checkLogs(self, irc):
192 """Checks the files for new lines and, if there be any, prints them to
193 IRC.
194
195 Actual work is all done here.
196 """
197
198 # Check xlogfile
199 self.xlog.seek(0, os.SEEK_CUR)
200 line = self.xlog.readline()
201 if line:
202 data = parse_xlog(line)
203
204 # Find dumplog
205 dumplog_paths = glob(
206 os.path.join(CONFIG_USERDATA_FILE,
207 data['name'],
208 'dumplog',
209 data['starttime'])
210 + '*'
211 )
212 if dumplog_paths:
213 (_, dumplog_file) = os.path.split(dumplog_paths[0])
214 dumplog_url = '{base}/{name}/dumplog/{file}'.format(
215 base=CONFIG_USERDATA_WEB,
216 name=data['name'],
217 file=dumplog_file,
218 )
219 else:
220 dumplog_url = "Can't find dumplog :("
221
222 report = report_template.format(dumplog=dumplog_url, **data)
223 msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
224 irc.queueMsg(msg)
225
226 # Check livelog
227 self.livelog.seek(0, os.SEEK_CUR)
228 line = self.livelog.readline()
229 if line:
230 data = parse_livelog(line)
231 report = livelog_announcement(data)
232 if report:
233 msg = ircmsgs.privmsg(CONFIG_CHANNEL, report)
234 irc.queueMsg(msg)
235
236
237 Class = NetHack
238
239
240 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: