Carbon Poker offers a 5 card stud game that wasn't listed here. It's not available...
[fpdb-dooglus.git] / pyfpdb / WinamaxToFpdb.py
blob9ab406d37ae3719820e562840b2bb27b69cfbda0
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # Copyright 2008-2011, Carl Gherardi
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 ########################################################################
21 import L10n
22 _ = L10n.get_translation()
24 import sys
25 import exceptions
27 import logging
28 # logging has been set up in fpdb.py or HUD_main.py, use their settings:
30 import Configuration
31 from HandHistoryConverter import *
32 from decimal_wrapper import Decimal
33 import time
35 # Winamax HH Format
37 class Winamax(HandHistoryConverter):
38 def Trace(f):
39 def my_f(*args, **kwds):
40 print ( "entering " + f.__name__)
41 result= f(*args, **kwds)
42 print ( "exiting " + f.__name__)
43 return result
44 my_f.__name = f.__name__
45 my_f.__doc__ = f.__doc__
46 return my_f
48 filter = "Winamax"
49 siteName = "Winamax"
50 filetype = "text"
51 codepage = ("utf8", "cp1252")
52 siteId = 14 # Needs to match id entry in Sites database
54 mixes = { } # Legal mixed games
55 sym = {'USD': "\$", 'CAD': "\$", 'T$': "", "EUR": u"\xe2\x82\xac|\u20ac", "GBP": "\xa3"} # ADD Euro, Sterling, etc HERE
56 substitutions = {
57 'LEGAL_ISO' : "USD|EUR|GBP|CAD|FPP", # legal ISO currency codes
58 'LS' : u"\$|\xe2\x82\xac|\u20ac|" # legal currency symbols - Euro(cp1252, utf-8)
61 limits = { 'no limit':'nl', 'pot limit' : 'pl','LIMIT':'fl'}
63 games = { # base, category
64 "Holdem" : ('hold','holdem'),
65 'Omaha' : ('hold','omahahi'),
66 # 'Omaha Hi/Lo' : ('hold','omahahilo'),
67 # 'Razz' : ('stud','razz'),
68 # 'RAZZ' : ('stud','razz'),
69 # '7 Card Stud' : ('stud','studhi'),
70 # 'SEVEN_CARD_STUD_HI_LO' : ('stud','studhilo'),
71 # 'Badugi' : ('draw','badugi'),
72 # 'Triple Draw 2-7 Lowball' : ('draw','27_3draw'),
73 # '5 Card Draw' : ('draw','fivedraw')
76 # Static regexes
77 # ***** End of hand R5-75443872-57 *****
78 re_SplitHands = re.compile(r'\n\n')
82 # Winamax Poker - CashGame - HandId: #279823-223-1285031451 - Holdem no limit (0.02€/0.05€) - 2010/09/21 03:10:51 UTC
83 # Table: 'Charenton-le-Pont' 9-max (real money) Seat #5 is the button
84 re_HandInfo = re.compile(u"""
85 \s*Winamax\sPoker\s-\s
86 (?P<RING>CashGame)?
87 (?P<TOUR>Tournament\s
88 (?P<TOURNAME>.+)?\s
89 buyIn:\s(?P<BUYIN>(?P<BIAMT>[%(LS)s\d\,]+)?\s\+?\s(?P<BIRAKE>[%(LS)s\d\,]+)?\+?(?P<BOUNTY>[%(LS)s\d\.]+)?\s?(?P<TOUR_ISO>%(LEGAL_ISO)s)?|Freeroll|Gratuit|Ticket\suniquement)?\s
90 (level:\s(?P<LEVEL>\d+))?
91 .*)?
92 \s-\sHandId:\s\#(?P<HID1>\d+)-(?P<HID2>\d+)-(?P<HID3>\d+).*\s # REB says: HID3 is the correct hand number
93 (?P<GAME>Holdem|Omaha)\s
94 (?P<LIMIT>no\slimit|pot\slimit)\s
96 (((%(LS)s)?(?P<ANTE>[.0-9]+)(%(LS)s)?)/)?
97 ((%(LS)s)?(?P<SB>[.0-9]+)(%(LS)s)?)/
98 ((%(LS)s)?(?P<BB>[.0-9]+)(%(LS)s)?)
99 \)\s-\s
100 (?P<DATETIME>.*)
101 Table:\s\'(?P<TABLE>[^(]+)
102 (.(?P<TOURNO>\d+).\#(?P<TABLENO>\d+))?.*
104 \s(?P<MAXPLAYER>\d+)\-max
105 """ % substitutions, re.MULTILINE|re.DOTALL|re.VERBOSE)
107 re_TailSplitHands = re.compile(r'\n\s*\n')
108 re_Button = re.compile(r'Seat\s#(?P<BUTTON>\d+)\sis\sthe\sbutton')
109 re_Board = re.compile(r"\[(?P<CARDS>.+)\]")
110 re_Total = re.compile(r"Total pot (?P<TOTAL>[\.\d]+).*(No rake|Rake (?P<RAKE>[\.\d]+))" % substitutions)
112 # 2010/09/21 03:10:51 UTC
113 re_DateTime = re.compile("""
114 (?P<Y>[0-9]{4})/
115 (?P<M>[0-9]+)/
116 (?P<D>[0-9]+)\s
117 (?P<H>[0-9]+):(?P<MIN>[0-9]+):(?P<S>[0-9]+)\s
119 """, re.MULTILINE|re.VERBOSE)
121 # Seat 1: some_player (5€)
122 # Seat 2: some_other_player21 (6.33€)
124 re_PlayerInfo = re.compile(u'Seat\s(?P<SEAT>[0-9]+):\s(?P<PNAME>.*)\s\((%(LS)s)?(?P<CASH>[.0-9]+)(%(LS)s)?\)' % substitutions)
126 def compilePlayerRegexs(self, hand):
127 players = set([player[1] for player in hand.players])
128 if not players <= self.compiledPlayers: # x <= y means 'x is subset of y'
129 # we need to recompile the player regexs.
130 # TODO: should probably rename re_HeroCards and corresponding method,
131 # since they are used to find all cards on lines starting with "Dealt to:"
132 # They still identify the hero.
133 self.compiledPlayers = players
134 #ANTES/BLINDS
135 #helander2222 posts blind ($0.25), lopllopl posts blind ($0.50).
136 player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")"
137 subst = {'PLYR': player_re, 'CUR': self.sym[hand.gametype['currency']]}
138 self.re_PostSB = re.compile('%(PLYR)s posts small blind (%(CUR)s)?(?P<SB>[\.0-9]+)(%(CUR)s)?' % subst, re.MULTILINE)
139 self.re_PostBB = re.compile('%(PLYR)s posts big blind (%(CUR)s)?(?P<BB>[\.0-9]+)(%(CUR)s)?' % subst, re.MULTILINE)
140 self.re_DenySB = re.compile('(?P<PNAME>.*) deny SB' % subst, re.MULTILINE)
141 self.re_Antes = re.compile(r"^%(PLYR)s posts ante (%(CUR)s)?(?P<ANTE>[\.0-9]+)(%(CUR)s)?" % subst, re.MULTILINE)
142 self.re_BringIn = re.compile(r"^%(PLYR)s brings[- ]in( low|) for (%(CUR)s)?(?P<BRINGIN>[\.0-9]+(%(CUR)s)?)" % subst, re.MULTILINE)
143 self.re_PostBoth = re.compile('(?P<PNAME>.*): posts small \& big blind \( (%(CUR)s)?(?P<SBBB>[\.0-9]+)(%(CUR)s)?\)' % subst)
144 self.re_PostDead = re.compile('(?P<PNAME>.*) posts dead blind \((%(CUR)s)?(?P<DEAD>[\.0-9]+)(%(CUR)s)?\)' % subst, re.MULTILINE)
145 self.re_HeroCards = re.compile('Dealt\sto\s%(PLYR)s\s\[(?P<CARDS>.*)\]' % subst)
147 self.re_Action = re.compile('(, )?(?P<PNAME>.*?)(?P<ATYPE> bets| checks| raises| calls| folds)( (%(CUR)s)?(?P<BET>[\d\.]+)(%(CUR)s)?)?( and is all-in)?' % subst)
148 self.re_ShowdownAction = re.compile('(?P<PNAME>[^\(\)\n]*) (\((small blind|big blind|button)\) )?shows \[(?P<CARDS>.+)\]')
150 self.re_CollectPot = re.compile('\s*(?P<PNAME>.*)\scollected\s(%(CUR)s)?(?P<POT>[\.\d]+)(%(CUR)s)?.*' % subst)
151 self.re_ShownCards = re.compile("^Seat (?P<SEAT>[0-9]+): %(PLYR)s showed \[(?P<CARDS>.*)\].*" % subst, re.MULTILINE)
152 self.re_sitsOut = re.compile('(?P<PNAME>.*) sits out')
154 def readSupportedGames(self):
155 return [
156 ["ring", "hold", "fl"],
157 ["ring", "hold", "nl"],
158 ["ring", "hold", "pl"],
159 ["tour", "hold", "fl"],
160 ["tour", "hold", "nl"],
161 ["tour", "hold", "pl"],
164 def determineGameType(self, handText):
165 # Inspect the handText and return the gametype dict
166 # gametype dict is: {'limitType': xxx, 'base': xxx, 'category': xxx}
167 info = {}
169 m = self.re_HandInfo.search(handText)
170 if not m:
171 tmp = handText[0:100]
172 log.error(_("Unable to recognise gametype from: '%s'") % tmp)
173 log.error("determineGameType: " + _("Raising FpdbParseError"))
174 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp)
176 mg = m.groupdict()
178 if mg.get('TOUR'):
179 info['type'] = 'tour'
180 elif mg.get('RING'):
181 info['type'] = 'ring'
183 info['currency'] = 'EUR'
185 if 'LIMIT' in mg:
186 if mg['LIMIT'] in self.limits:
187 info['limitType'] = self.limits[mg['LIMIT']]
188 else:
189 tmp = handText[0:100]
190 log.error(_("limit not found in self.limits(%s). hand: '%s'") % (str(mg),tmp))
191 log.error("determineGameType: " + _("Raising FpdbParseError"))
192 raise FpdbParseError(_("limit not found in self.limits(%s). hand: '%s'") % (str(mg),tmp))
193 if 'GAME' in mg:
194 (info['base'], info['category']) = self.games[mg['GAME']]
195 if 'SB' in mg:
196 info['sb'] = mg['SB']
197 if 'BB' in mg:
198 info['bb'] = mg['BB']
200 return info
202 def readHandInfo(self, hand):
203 info = {}
204 m = self.re_HandInfo.search(hand.handText)
206 if m:
207 info.update(m.groupdict())
209 #log.debug("readHandInfo: %s" % info)
210 for key in info:
211 if key == 'DATETIME':
212 a = self.re_DateTime.search(info[key])
213 if a:
214 datetimestr = "%s/%s/%s %s:%s:%s" % (a.group('Y'),a.group('M'), a.group('D'), a.group('H'),a.group('MIN'),a.group('S'))
215 else:
216 datetimestr = "2010/Jan/01 01:01:01"
217 log.error("readHandInfo: " + _("DATETIME not matched: '%s'") % info[key])
218 #print "DEBUG: readHandInfo: DATETIME not matched: '%s'" % info[key]
219 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S")
220 hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, "CET", "UTC")
221 if key == 'HID1':
222 # Need to remove non-alphanumerics for MySQL
223 # hand.handid = "1%.9d%s%s"%(int(info['HID2']),info['HID1'],info['HID3'])
224 hand.handid = "%s%s%s"%(int(info['HID1']),info['HID2'],info['HID3'])
225 if len (hand.handid) > 19:
226 hand.handid = "%s%s" % (int(info['HID2']), int(info['HID3']))
228 # if key == 'HID3':
229 # hand.handid = int(info['HID3']) # correct hand no (REB)
230 if key == 'TOURNO':
231 hand.tourNo = info[key]
232 if key == 'TABLE':
233 hand.tablename = info[key]
234 # TODO: long-term solution for table naming on Winamax.
235 if hand.tablename.endswith(u'No Limit Hold\'em'):
236 hand.tablename = hand.tablename[:-len(u'No Limit Hold\'em')] + u'NLHE'
237 if key == 'MAXPLAYER' and info[key] != None:
238 hand.maxseats = int(info[key])
240 if key == 'BUYIN':
241 if hand.tourNo!=None:
242 #print "DEBUG: info['BUYIN']: %s" % info['BUYIN']
243 #print "DEBUG: info['BIAMT']: %s" % info['BIAMT']
244 #print "DEBUG: info['BIRAKE']: %s" % info['BIRAKE']
245 #print "DEBUG: info['BOUNTY']: %s" % info['BOUNTY']
246 for k in ['BIAMT','BIRAKE']:
247 if k in info.keys() and info[k]:
248 info[k] = info[k].replace(',','.')
250 if info[key] == 'Gratuit' or info[key] == 'Freeroll':
251 hand.buyin = 0
252 hand.fee = 0
253 hand.buyinCurrency = "FREE"
254 else:
255 if info[key].find("$")!=-1:
256 hand.buyinCurrency="USD"
257 elif info[key].find(u"€")!=-1:
258 hand.buyinCurrency="EUR"
259 elif info[key].find("FPP")!=-1:
260 hand.buyinCurrency="PSFP"
261 else:
262 #FIXME: handle other currencies (are there other currencies?)
263 raise FpdbParseError(_("Failed to detect currency.") + " " + _("Hand ID: %s: '%s'") % (hand.handid, info[key]))
265 info['BIAMT'] = info['BIAMT'].strip(u'$€FPP')
267 if hand.buyinCurrency!="PSFP":
268 if info['BOUNTY'] != None:
269 # There is a bounty, Which means we need to switch BOUNTY and BIRAKE values
270 tmp = info['BOUNTY']
271 info['BOUNTY'] = info['BIRAKE']
272 info['BIRAKE'] = tmp
273 info['BOUNTY'] = info['BOUNTY'].strip(u'$€') # Strip here where it isn't 'None'
274 hand.koBounty = int(100*Decimal(info['BOUNTY']))
275 hand.isKO = True
276 else:
277 hand.isKO = False
279 info['BIRAKE'] = info['BIRAKE'].strip(u'$€')
281 # TODO: Is this correct? Old code tried to
282 # conditionally multiply by 100, but we
283 # want hand.buyin in 100ths of
284 # dollars/euros (so hand.buyin = 90 for $0.90 BI).
285 hand.buyin = int(100 * Decimal(info['BIAMT']))
286 hand.fee = int(100 * Decimal(info['BIRAKE']))
287 else:
288 hand.buyin = int(Decimal(info['BIAMT']))
289 hand.fee = 0
291 if key == 'LEVEL':
292 hand.level = info[key]
294 m = self.re_Button.search(hand.handText)
295 hand.buttonpos = m.groupdict().get('BUTTON', None)
297 hand.mixed = None
299 def readPlayerStacks(self, hand):
300 log.debug(_("readplayerstacks: re is '%s'") % self.re_PlayerInfo)
301 m = self.re_PlayerInfo.finditer(hand.handText)
302 for a in m:
303 hand.addPlayer(int(a.group('SEAT')), a.group('PNAME'), a.group('CASH'))
306 def markStreets(self, hand):
307 m = re.search(r"\*\*\* ANTE\/BLINDS \*\*\*(?P<PREFLOP>.+(?=\*\*\* FLOP \*\*\*)|.+)"
308 r"(\*\*\* FLOP \*\*\*(?P<FLOP> \[\S\S \S\S \S\S\].+(?=\*\*\* TURN \*\*\*)|.+))?"
309 r"(\*\*\* TURN \*\*\* \[\S\S \S\S \S\S](?P<TURN>\[\S\S\].+(?=\*\*\* RIVER \*\*\*)|.+))?"
310 r"(\*\*\* RIVER \*\*\* \[\S\S \S\S \S\S \S\S](?P<RIVER>\[\S\S\].+))?", hand.handText,re.DOTALL)
312 try:
313 hand.addStreets(m)
314 # print "adding street", m.group(0)
315 # print "---"
316 except:
317 print (_("Failed to add streets. handtext=%s"))
319 #Needs to return a list in the format
320 # ['player1name', 'player2name', ...] where player1name is the sb and player2name is bb,
321 # addtional players are assumed to post a bb oop
323 def readButton(self, hand):
324 m = self.re_Button.search(hand.handText)
325 if m:
326 hand.buttonpos = int(m.group('BUTTON'))
327 log.debug(_('readButton: button on pos %d') % hand.buttonpos)
328 else:
329 log.warning(_('readButton: not found'))
331 # def readCommunityCards(self, hand, street):
332 # #print hand.streets.group(street)
333 # if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
334 # m = self.re_Board.search(hand.streets.group(street))
335 # hand.setCommunityCards(street, m.group('CARDS').split(','))
337 def readCommunityCards(self, hand, street): # street has been matched by markStreets, so exists in this hand
338 if street in ('FLOP','TURN','RIVER'): # a list of streets which get dealt community cards (i.e. all but PREFLOP)
339 #print "DEBUG readCommunityCards:", street, hand.streets.group(street)
340 m = self.re_Board.search(hand.streets[street])
341 hand.setCommunityCards(street, m.group('CARDS').split(' '))
343 def readBlinds(self, hand):
344 if not self.re_DenySB.search(hand.handText):
345 try:
346 m = self.re_PostSB.search(hand.handText)
347 hand.addBlind(m.group('PNAME'), 'small blind', m.group('SB'))
348 except exceptions.AttributeError: # no small blind
349 log.warning( _("readBlinds in noSB exception - no SB created")+str(sys.exc_info()) )
350 #hand.addBlind(None, None, None)
351 for a in self.re_PostBB.finditer(hand.handText):
352 hand.addBlind(a.group('PNAME'), 'big blind', a.group('BB'))
353 for a in self.re_PostDead.finditer(hand.handText):
354 #print "DEBUG: Found dead blind: addBlind(%s, 'secondsb', %s)" %(a.group('PNAME'), a.group('DEAD'))
355 hand.addBlind(a.group('PNAME'), 'secondsb', a.group('DEAD'))
356 for a in self.re_PostBoth.finditer(hand.handText):
357 hand.addBlind(a.group('PNAME'), 'small & big blinds', a.group('SBBB'))
359 def readAntes(self, hand):
360 log.debug(_("reading antes"))
361 m = self.re_Antes.finditer(hand.handText)
362 for player in m:
363 #~ logging.debug("hand.addAnte(%s,%s)" %(player.group('PNAME'), player.group('ANTE')))
364 hand.addAnte(player.group('PNAME'), player.group('ANTE'))
366 def readBringIn(self, hand):
367 m = self.re_BringIn.search(hand.handText,re.DOTALL)
368 if m:
369 #~ logging.debug("readBringIn: %s for %s" %(m.group('PNAME'), m.group('BRINGIN')))
370 hand.addBringIn(m.group('PNAME'), m.group('BRINGIN'))
372 def readHeroCards(self, hand):
373 # streets PREFLOP, PREDRAW, and THIRD are special cases beacause
374 # we need to grab hero's cards
375 for street in ('PREFLOP', 'DEAL', 'BLINDSANTES'):
376 if street in hand.streets.keys():
377 m = self.re_HeroCards.finditer(hand.streets[street])
378 if m == []:
379 log.debug(_("No hole cards found for %s") % street)
380 for found in m:
381 hand.hero = found.group('PNAME')
382 newcards = found.group('CARDS').split(' ')
383 # print "DEBUG: addHoleCards(%s, %s, %s)" %(street, hand.hero, newcards)
384 hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True)
385 log.debug(_("Hero cards %s: %s") % (hand.hero, newcards))
387 def readAction(self, hand, street):
388 m = self.re_Action.finditer(hand.streets[street])
389 for action in m:
390 acts = action.groupdict()
391 if action.group('ATYPE') == ' raises':
392 hand.addRaiseBy( street, action.group('PNAME'), action.group('BET') )
393 elif action.group('ATYPE') == ' calls':
394 hand.addCall( street, action.group('PNAME'), action.group('BET') )
395 elif action.group('ATYPE') == ' bets':
396 hand.addBet( street, action.group('PNAME'), action.group('BET') )
397 elif action.group('ATYPE') == ' folds':
398 hand.addFold( street, action.group('PNAME'))
399 elif action.group('ATYPE') == ' checks':
400 hand.addCheck( street, action.group('PNAME'))
401 elif action.group('ATYPE') == ' discards':
402 hand.addDiscard(street, action.group('PNAME'), action.group('BET'), action.group('DISCARDED'))
403 elif action.group('ATYPE') == ' stands pat':
404 hand.addStandsPat( street, action.group('PNAME'))
405 else:
406 log.fatal(_("DEBUG:") + _("Unimplemented %s: '%s' '%s'") % ("readAction", action.group('PNAME'), action.group('ATYPE')))
407 # print "Processed %s"%acts
408 # print "committed=",hand.pot.committed
410 def readShowdownActions(self, hand):
411 for shows in self.re_ShowdownAction.finditer(hand.handText):
412 #log.debug(_("add show actions %s") % shows)
413 cards = shows.group('CARDS')
414 cards = cards.split(' ')
415 # print "DEBUG: addShownCards(%s, %s)" %(cards, shows.group('PNAME'))
416 hand.addShownCards(cards, shows.group('PNAME'))
418 def readCollectPot(self,hand):
419 # Winamax has unfortunately thinks that a sidepot is created
420 # when there is uncalled money in the pot - something that can
421 # only happen when a player is all-in
423 # Becuase of this, we need to do the same calculations as class Pot()
424 # and determine if the amount returned is the same as the amount collected
425 # if so then the collected line is invalid
427 total = sum(hand.pot.committed.values()) + sum(hand.pot.common.values())
429 # Return any uncalled bet.
430 committed = sorted([ (v,k) for (k,v) in hand.pot.committed.items()])
431 #print "DEBUG: committed: %s" % committed
432 returned = {}
433 lastbet = committed[-1][0] - committed[-2][0]
434 if lastbet > 0: # uncalled
435 returnto = committed[-1][1]
436 #print "DEBUG: returning %f to %s" % (lastbet, returnto)
437 total -= lastbet
438 returned[returnto] = lastbet
440 collectees = []
442 tp = self.re_Total.search(hand.handText)
443 rake = tp.group('RAKE')
444 if rake == None:
445 rake = 0
446 for m in self.re_CollectPot.finditer(hand.handText):
447 collectees.append([m.group('PNAME'), m.group('POT')])
449 #print "DEBUG: Total pot: %s" % tp.groupdict()
450 #print "DEBUG: According to pot: %s" % total
451 #print "DEBUG: Rake: %s" % rake
453 if len(collectees) == 1:
454 plyr, p = collectees[0]
455 # p may be wrong, use calculated total - rake
456 p = total - Decimal(rake)
457 #print "DEBUG: len1: addCollectPot(%s,%s)" %(plyr, p)
458 hand.addCollectPot(player=plyr,pot=p)
459 else:
460 for plyr, p in collectees:
461 if plyr in returned.keys():
462 p = Decimal(p) - returned[plyr]
463 if p > 0:
464 #print "DEBUG: addCollectPot(%s,%s)" %(plyr, p)
465 hand.addCollectPot(player=plyr,pot=p)
467 def readShownCards(self,hand):
468 for m in self.re_ShownCards.finditer(hand.handText):
469 log.debug(_("Read shown cards: %s") % m.group(0))
470 cards = m.group('CARDS')
471 cards = cards.split(' ') # needs to be a list, not a set--stud needs the order
472 (shown, mucked) = (False, False)
473 if m.group('CARDS') is not None:
474 shown = True
475 hand.addShownCards(cards=cards, player=m.group('PNAME'), shown=shown, mucked=mucked)