2 # -*- coding: utf-8 -*-
4 # Copyright 2009-2010, Grigorij Indigirkin
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 ########################################################################
22 _
= L10n
.get_translation()
25 from collections
import defaultdict
27 from Configuration
import LOCALE_ENCODING
28 from Exceptions
import FpdbParseError
29 from HandHistoryConverter
import *
31 # PartyPoker HH Format
33 class FpdbParseError(FpdbParseError
):
34 "Usage: raise FpdbParseError(<msg>[, hh=<hh>][, hid=<hid>])"
36 def __init__(self
, msg
='', hh
=None, hid
=None):
37 return super(FpdbParseError
, self
).__init
__(msg
, hid
=hid
)
40 return ("%(DELIMETER)s\n%(HH)s\n%(DELIMETER)s") % \
41 {'DELIMETER': '#'*50, 'HH': hh
}
43 class PartyPoker(HandHistoryConverter
):
44 sitename
= "PartyPoker"
48 sym
= {'USD': "\$", 'EUR': u
"\u20ac", 'T$': ""}
49 currencies
= {"\$": "USD", "$": "USD", u
"\xe2\x82\xac": "EUR", u
"\u20ac": "EUR", '': "T$"}
51 'LEGAL_ISO' : "USD|EUR", # legal ISO currency codes
52 'LS' : "\$|\u20AC|\xe2\x82\xac|" # legal currency symbols - Euro(cp1252, utf-8)
56 # $5 USD NL Texas Hold'em - Saturday, July 25, 07:53:52 EDT 2009
57 # NL Texas Hold'em $1 USD Buy-in Trny:45685440 Level:8 Blinds-Antes(600/1 200 -50) - Sunday, May 17, 11:25:07 MSKS 2009
58 re_GameInfoRing
= re
.compile(u
"""
59 (?P<CURRENCY>[%(LS)s])\s*(?P<RINGLIMIT>[.,0-9]+)([.,0-9/$]+)?\s*(?:%(LEGAL_ISO)s)?\s*
60 (?P<LIMIT>(NL|PL|))\s*
61 (?P<GAME>(Texas\ Hold\'em|Omaha|7\ Card\ Stud\ Hi-Lo))
64 """ % substitutions
, re
.VERBOSE | re
.UNICODE
)
65 re_GameInfoTrny
= re
.compile("""
66 (?P<LIMIT>(NL|PL|))\s*
67 (?P<GAME>(Texas\ Hold\'em|Omaha))\s+
68 (?:(?P<BUYIN>\$?[.,0-9]+)\s*(?P<BUYIN_CURRENCY>%(LEGAL_ISO)s)?\s*Buy-in\s+)?
69 Trny:\s?(?P<TOURNO>\d+)\s+
70 Level:\s*(?P<LEVEL>\d+)\s+
71 ((Blinds|Stakes)(?:-Antes)?)\(
74 (?:\s*-\s*(?P<ANTE>[.,0-9 ]+)\$?)?
78 """ % substitutions
, re
.VERBOSE | re
.UNICODE
)
79 re_Hid
= re
.compile("Game \#(?P<HID>\d+) starts.")
81 re_PlayerInfo
= re
.compile(u
"""
82 Seat\s(?P<SEAT>\d+):\s
84 \(\s*[%(LS)s]?(?P<CASH>[0-9,.]+)\s*(?:%(LEGAL_ISO)s|)\s*\)
85 """ % substitutions
, re
.VERBOSE| re
.UNICODE
)
87 re_HandInfo
= re
.compile("""
88 ^Table\s+(?P<TTYPE>[$a-zA-Z0-9 ]+)?\s+
89 (?: \#|\(|)(?P<TABLE>\d+)\)?\s+
90 (?:[a-zA-Z0-9 ]+\s+\#(?P<MTTTABLE>\d+).+)?
92 \((?P<PLAY>Real|Play)\s+Money\)\s+ # FIXME: check if play money is correct
93 Seat\s+(?P<BUTTON>\d+)\sis\sthe\sbutton
94 \s+Total\s+number\s+of\s+players\s+\:\s+(?P<PLYRS>\d+)/?(?P<MAX>\d+)?
96 re
.VERBOSE|re
.MULTILINE|re
.DOTALL
)
98 re_CountedSeats
= re
.compile("^Total\s+number\s+of\s+players\s*:\s*(?P<COUNTED_SEATS>\d+)", re
.MULTILINE
)
99 re_SplitHands
= re
.compile('\x00+')
100 re_TailSplitHands
= re
.compile('(\x00+)')
102 re_Button
= re
.compile('Seat (?P<BUTTON>\d+) is the button', re
.MULTILINE
)
103 re_Board
= re
.compile(r
"\[(?P<CARDS>.+)\]")
104 re_NoSmallBlind
= re
.compile(
105 '^There is no Small Blind in this hand as the Big Blind '
106 'of the previous hand left the table', re
.MULTILINE
)
107 re_20BBmin
= re
.compile(r
"Table 20BB Min")
109 def allHandsAsList(self
):
110 list = HandHistoryConverter
.allHandsAsList(self
)
113 return filter(lambda text
: len(text
.strip()), list)
115 def guessMaxSeats(self
, hand
):
116 """Return a guess at max_seats when not specified in HH."""
117 mo
= self
.maxOccSeat(hand
)
118 if mo
== 10: return mo
121 # there are 9-max tables for cash and 10-max for tournaments
122 return 9 if hand
.gametype
['type']=='ring' else 10
124 def compilePlayerRegexs(self
, hand
):
125 players
= set([player
[1] for player
in hand
.players
])
126 if not players
<= self
.compiledPlayers
: # x <= y means 'x is subset of y'
128 self
.compiledPlayers
= players
129 player_re
= "(?P<PNAME>" + "|".join(map(re
.escape
, players
)) + ")"
130 subst
= {'PLYR': player_re
, 'CUR_SYM': self
.sym
[hand
.gametype
['currency']],
131 'CUR': hand
.gametype
['currency'] if hand
.gametype
['currency']!='T$' else ''}
132 self
.re_PostSB
= re
.compile(
133 r
"^%(PLYR)s posts small blind \[%(CUR_SYM)s(?P<SB>[.,0-9]+) ?%(CUR)s\]\."
134 % subst
, re
.MULTILINE
)
135 self
.re_PostBB
= re
.compile(
136 u
"%(PLYR)s posts big blind \[%(CUR_SYM)s(?P<BB>[.,0-9]+) ?%(CUR)s\]\."
137 % subst
, re
.MULTILINE
)
138 self
.re_PostDead
= re
.compile(
139 r
"^%(PLYR)s posts big blind + dead \[(?P<BBNDEAD>[.,0-9]+) ?%(CUR_SYM)s\]\." % subst
,
141 self
.re_Antes
= re
.compile(
142 r
"^%(PLYR)s posts ante \[%(CUR_SYM)s(?P<ANTE>[.,0-9]+) ?%(CUR)s\]" % subst
,
144 self
.re_HeroCards
= re
.compile(
145 r
"^Dealt to %(PLYR)s \[\s*(?P<NEWCARDS>.+)\s*\]" % subst
,
147 self
.re_Action
= re
.compile(u
"""
148 ^%(PLYR)s\s+(?P<ATYPE>bets|checks|raises|calls|folds|is\sall-In)
149 (?:\s+\[%(CUR_SYM)s(?P<BET>[.,\d]+)\s*%(CUR)s\])?
150 """ % subst
, re
.MULTILINE|re
.VERBOSE
)
151 self
.re_ShownCards
= re
.compile(
152 r
"^%s (?P<SHOWED>(?:doesn\'t )?shows?) " % player_re
+
153 r
"\[ *(?P<CARDS>.+) *\](?P<COMBINATION>.+)\.",
155 self
.re_CollectPot
= re
.compile(
156 r
"""^%(PLYR)s \s+ wins \s+
157 %(CUR_SYM)s(?P<POT>[.,\d]+)\s*%(CUR)s""" % subst
,
158 re
.MULTILINE|re
.VERBOSE
)
160 def readSupportedGames(self
):
161 return [["ring", "hold", "nl"],
162 ["ring", "hold", "pl"],
163 ["ring", "hold", "fl"],
165 ["tour", "hold", "nl"],
166 ["tour", "hold", "pl"],
167 ["tour", "hold", "fl"],
170 def _getGameType(self
, handText
):
171 if not hasattr(self
, '_gameType'):
172 self
._gameType
= None
173 if self
._gameType
is None:
174 # let's determine whether hand is trny
175 # and whether 5-th line contains head line
176 headLine
= handText
.split(self
.lineSplitter
)[4]
177 for headLineContainer
in headLine
, handText
:
178 for regexp
in self
.re_GameInfoTrny
, self
.re_GameInfoRing
:
179 m
= regexp
.search(headLineContainer
)
182 return self
._gameType
183 return self
._gameType
185 def determineGameType(self
, handText
):
186 """inspect the handText and return the gametype dict
189 {'limitType': xxx, 'base': xxx, 'category': xxx}"""
192 m
= self
._getGameType
(handText
)
193 m_20BBmin
= self
.re_20BBmin
.search(handText
)
195 tmp
= handText
[0:100]
196 log
.error(_("determineGameType: Unable to recognise gametype from: '%s'") % tmp
)
197 log
.error(_("determineGameType: Raising FpdbParseError"))
198 raise FpdbParseError(_("Unable to recognise gametype from: '%s'") % tmp
)
202 # translations from captured groups to fpdb info strings
203 limits
= { 'NL':'nl', 'PL':'pl', '':'fl' }
204 games
= { # base, category
205 "Texas Hold'em" : ('hold','holdem'),
206 'Omaha' : ('hold','omahahi'),
207 "7 Card Stud Hi-Lo" : ('stud','studhi'),
210 for expectedField
in ['LIMIT', 'GAME']:
211 if mg
[expectedField
] is None:
212 raise FpdbParseError(_("Cannot fetch field '%s'") % expectedField
)
214 info
['limitType'] = limits
[mg
['LIMIT'].strip()]
216 raise FpdbParseError(_("Unknown limit '%s'") % mg
['LIMIT'])
219 (info
['base'], info
['category']) = games
[mg
['GAME']]
221 raise FpdbParseError(_("Unknown game type '%s'") % mg
['GAME'])
224 info
['type'] = 'tour'
226 info
['type'] = 'ring'
228 if info
['type'] == 'ring':
229 if m_20BBmin
is None:
230 bb
= float(mg
['RINGLIMIT'])/100.0
232 bb
= float(mg
['RINGLIMIT'])/40.0
239 info
['bb'] = "%.2f" % (bb
)
240 info
['sb'] = "%.2f" % (sb
)
241 info
['currency'] = self
.currencies
[mg
['CURRENCY']]
243 info
['sb'] = self
.clearMoneyString(mg
['SB'])
244 info
['bb'] = self
.clearMoneyString(mg
['BB'])
245 info
['currency'] = 'T$'
250 def readHandInfo(self
, hand
):
253 info
.update(self
.re_Hid
.search(hand
.handText
).groupdict())
254 except AttributeError, e
:
255 raise FpdbParseError(_("Cannot read HID for current hand: %s" % e
))
258 info
.update(self
.re_HandInfo
.search(hand
.handText
,re
.DOTALL
).groupdict())
260 raise FpdbParseError(_("Cannot read Handinfo for current hand"), hid
= info
['HID'])
263 info
.update(self
._getGameType
(hand
.handText
).groupdict())
265 raise FpdbParseError(_("Cannot read GameType for current hand"), hid
= info
['HID'])
268 m
= self
.re_CountedSeats
.search(hand
.handText
)
269 if m
: info
.update(m
.groupdict())
272 # FIXME: it's dirty hack
273 # party doesnt subtract uncalled money from commited money
274 # so hand.totalPot calculation has to be redefined
275 from Hand
import Pot
, HoldemOmahaHand
276 def getNewTotalPot(origTotalPot
):
278 if self
.totalpot
is None:
280 self
.totalpot
= self
.pot
.total
281 for i
,v
in enumerate(self
.collected
):
282 if v
[0] in self
.pot
.returned
:
283 self
.collected
[i
][1] = Decimal(v
[1]) - self
.pot
.returned
[v
[0]]
284 self
.collectees
[v
[0]] -= self
.pot
.returned
[v
[0]]
285 self
.pot
.returned
[v
[0]] = 0
286 return origTotalPot()
288 instancemethod
= type(hand
.totalPot
)
289 hand
.totalPot
= instancemethod(getNewTotalPot(hand
.totalPot
), hand
, HoldemOmahaHand
)
293 log
.debug("readHandInfo: %s" % info
)
295 if key
== 'DATETIME':
296 #Saturday, July 25, 07:53:52 EDT 2009
297 #Thursday, July 30, 21:40:41 MSKS 2009
298 #Sunday, October 25, 13:39:07 MSK 2009
300 r
"\w+,\s+(?P<M>\w+)\s+(?P<D>\d+),\s+(?P<H>\d+):(?P<MIN>\d+):(?P<S>\d+)\s+(?P<TZ>[A-Z]+)\s+(?P<Y>\d+)",
304 months
= ['January', 'February', 'March', 'April','May', 'June',
305 'July','August','September','October','November','December']
306 if m2
.group('M') not in months
:
307 raise FpdbParseError("Only english hh is supported", hid
=info
["HID"])
308 month
= months
.index(m2
.group('M')) + 1
309 datetimestr
= "%s/%s/%s %s:%s:%s" % (m2
.group('Y'), month
,m2
.group('D'),m2
.group('H'),m2
.group('MIN'),m2
.group('S'))
310 hand
.startTime
= datetime
.datetime
.strptime(datetimestr
, "%Y/%m/%d %H:%M:%S")
311 # FIXME: some timezone correction required
312 #tzShift = defaultdict(lambda:0, {'EDT': -5, 'EST': -6, 'MSKS': 3})
313 #hand.starttime -= datetime.timedelta(hours=tzShift[m2.group('TZ')])
316 hand
.handid
= info
[key
]
318 hand
.tablename
= info
[key
]
319 if key
== 'MTTTABLE':
320 if info
[key
] != None:
321 hand
.tablename
= info
[key
]
322 hand
.tourNo
= info
['TABLE']
324 hand
.buttonpos
= info
[key
]
326 hand
.tourNo
= info
[key
]
327 if key
== 'TABLE_ID_WRAPPER':
329 # FIXME: there is no such property in Hand class
332 # FIXME: it's dirty hack T_T
333 # code below assumes that tournament rake is equal to zero
334 if info
[key
] == None:
337 cur
= info
[key
][0] if info
[key
][0] not in '0123456789' else ''
338 hand
.buyin
= info
[key
] + '+%s0' % cur
340 hand
.level
= info
[key
]
341 if key
== 'PLAY' and info
['PLAY'] != 'Real':
342 # if realy party doesn's save play money hh
343 hand
.gametype
['currency'] = 'play'
344 if key
== 'MAX' and info
[key
] is not None:
345 hand
.maxseats
= int(info
[key
])
348 def readButton(self
, hand
):
349 m
= self
.re_Button
.search(hand
.handText
)
351 hand
.buttonpos
= int(m
.group('BUTTON'))
353 log
.info(_('readButton: not found'))
355 def readPlayerStacks(self
, hand
):
356 log
.debug("readPlayerStacks")
357 m
= self
.re_PlayerInfo
.finditer(hand
.handText
)
359 zeroStackPlayers
= []
361 if a
.group('CASH') > '0':
362 #record max known stack for use with players with unknown stack
363 maxKnownStack
= max(a
.group('CASH'),maxKnownStack
)
364 hand
.addPlayer(int(a
.group('SEAT')), a
.group('PNAME'), self
.clearMoneyString(a
.group('CASH')))
366 #zero stacked players are added later
367 zeroStackPlayers
.append([int(a
.group('SEAT')), a
.group('PNAME'), self
.clearMoneyString(a
.group('CASH'))])
368 if hand
.gametype
['type'] == 'ring':
369 #finds first vacant seat after an exact seat
370 def findFirstEmptySeat(startSeat
):
371 while startSeat
in occupiedSeats
:
372 if startSeat
>= hand
.maxseats
:
377 re_JoiningPlayers
= re
.compile(r
"(?P<PLAYERNAME>.*) has joined the table")
378 re_BBPostingPlayers
= re
.compile(r
"(?P<PLAYERNAME>.*) posts big blind")
380 match_JoiningPlayers
= re_JoiningPlayers
.findall(hand
.handText
)
381 match_BBPostingPlayers
= re_BBPostingPlayers
.findall(hand
.handText
)
383 #add every player with zero stack, but:
384 #if a zero stacked player is just joined the table in this very hand then set his stack to maxKnownStack
385 for p
in zeroStackPlayers
:
386 if p
[1] in match_JoiningPlayers
:
387 p
[2] = self
.clearMoneyString(maxKnownStack
)
388 hand
.addPlayer(p
[0],p
[1],p
[2])
390 seatedPlayers
= list([(f
[1]) for f
in hand
.players
])
392 #it works for all known cases as of 2010-09-28
393 #should be refined with using match_ActivePlayers instead of match_BBPostingPlayers
394 #as a leaving and rejoining player could be active without posting a BB (sample HH needed)
395 unseatedActivePlayers
= list(set(match_BBPostingPlayers
) - set(seatedPlayers
))
397 if unseatedActivePlayers
:
398 for player
in unseatedActivePlayers
:
399 previousBBPoster
= match_BBPostingPlayers
[match_BBPostingPlayers
.index(player
)-1]
400 previousBBPosterSeat
= dict([(f
[1], f
[0]) for f
in hand
.players
])[previousBBPoster
]
401 occupiedSeats
= list([(f
[0]) for f
in hand
.players
])
403 newPlayerSeat
= findFirstEmptySeat(previousBBPosterSeat
)
404 hand
.addPlayer(newPlayerSeat
,player
,self
.clearMoneyString(maxKnownStack
))
406 def markStreets(self
, hand
):
408 r
"\*{2} Dealing down cards \*{2}"
410 r
"(?:\*{2} Dealing Flop \*{2} (?P<FLOP>\[ \S\S, \S\S, \S\S \].+?))?"
411 r
"(?:\*{2} Dealing Turn \*{2} (?P<TURN>\[ \S\S \].+?))?"
412 r
"(?:\*{2} Dealing River \*{2} (?P<RIVER>\[ \S\S \].+?))?$"
413 , hand
.handText
,re
.DOTALL
)
416 def readCommunityCards(self
, hand
, street
):
417 if street
in ('FLOP','TURN','RIVER'):
418 m
= self
.re_Board
.search(hand
.streets
[street
])
419 hand
.setCommunityCards(street
, renderCards(m
.group('CARDS')))
421 def readAntes(self
, hand
):
422 log
.debug("reading antes")
423 m
= self
.re_Antes
.finditer(hand
.handText
)
425 hand
.addAnte(player
.group('PNAME'), player
.group('ANTE'))
427 def readBlinds(self
, hand
):
428 noSmallBlind
= bool(self
.re_NoSmallBlind
.search(hand
.handText
))
429 if hand
.gametype
['type'] == 'ring':
431 assert noSmallBlind
==False
433 for m
in self
.re_PostSB
.finditer(hand
.handText
):
435 hand
.addBlind(m
.group('PNAME'), 'small blind', m
.group('SB'))
438 # Post dead blinds as ante
439 hand
.addBlind(m
.group('PNAME'), 'secondsb', m
.group('SB'))
440 except: # no small blind
441 hand
.addBlind(None, None, None)
443 for a
in self
.re_PostBB
.finditer(hand
.handText
):
444 hand
.addBlind(a
.group('PNAME'), 'big blind', a
.group('BB'))
446 deadFilter
= lambda s
: s
.replace(',', '.')
447 for a
in self
.re_PostDead
.finditer(hand
.handText
):
448 hand
.addBlind(a
.group('PNAME'), 'both', deadFilter(a
.group('BBNDEAD')))
450 # party doesn't track blinds for tournaments
451 # so there're some cra^Wcaclulations
452 if hand
.buttonpos
== 0:
453 self
.readButton(hand
)
454 # NOTE: code below depends on Hand's implementation
455 # playersMap - dict {seat: (pname,stack)}
456 playersMap
= dict([(f
[0], f
[1:3]) for f
in hand
.players
])
457 maxSeat
= max(playersMap
)
459 def findFirstNonEmptySeat(startSeat
):
460 while startSeat
not in playersMap
:
461 if startSeat
>= maxSeat
:
465 smartMin
= lambda A
,B
: A
if float(A
) <= float(B
) else B
468 hand
.addBlind(None, None, None)
469 smallBlindSeat
= int(hand
.buttonpos
)
471 smallBlindSeat
= findFirstNonEmptySeat(int(hand
.buttonpos
) + 1)
472 blind
= smartMin(hand
.sb
, playersMap
[smallBlindSeat
][1])
473 hand
.addBlind(playersMap
[smallBlindSeat
][0], 'small blind', blind
)
475 bigBlindSeat
= findFirstNonEmptySeat(smallBlindSeat
+ 1)
476 blind
= smartMin(hand
.bb
, playersMap
[bigBlindSeat
][1])
477 hand
.addBlind(playersMap
[bigBlindSeat
][0], 'big blind', blind
)
479 def readHeroCards(self
, hand
):
480 # we need to grab hero's cards
481 for street
in ('PREFLOP',):
482 if street
in hand
.streets
.keys():
483 m
= self
.re_HeroCards
.finditer(hand
.streets
[street
])
485 hand
.hero
= found
.group('PNAME')
486 newcards
= renderCards(found
.group('NEWCARDS'))
487 hand
.addHoleCards(street
, hand
.hero
, closed
=newcards
, shown
=False, mucked
=False, dealt
=True)
489 def readAction(self
, hand
, street
):
490 m
= self
.re_Action
.finditer(hand
.streets
[street
])
492 acts
= action
.groupdict()
493 playerName
= action
.group('PNAME')
494 amount
= self
.clearMoneyString(action
.group('BET')) if action
.group('BET') else None
495 actionType
= action
.group('ATYPE')
497 if actionType
== 'is all-In':
498 # party's allin can mean either raise or bet or call
499 Bp
= hand
.lastBet
[street
]
502 elif Bp
< Decimal(amount
):
503 actionType
= 'raises'
507 if actionType
== 'raises':
508 if street
== 'PREFLOP' and \
509 playerName
in [item
[0] for item
in hand
.actions
['BLINDSANTES'] if item
[2]!='ante']:
510 # preflop raise from blind
511 hand
.addCallandRaise( street
, playerName
, amount
)
513 hand
.addCallandRaise( street
, playerName
, amount
)
514 elif actionType
== 'calls':
515 hand
.addCall( street
, playerName
, amount
)
516 elif actionType
== 'bets':
517 hand
.addBet( street
, playerName
, amount
)
518 elif actionType
== 'folds':
519 hand
.addFold( street
, playerName
)
520 elif actionType
== 'checks':
521 hand
.addCheck( street
, playerName
)
523 raise FpdbParseError(
524 _("Unimplemented readAction: '%s' '%s'") % (playerName
,actionType
,),
527 def readShowdownActions(self
, hand
):
528 # all action in readShownCards
531 def readCollectPot(self
,hand
):
532 for m
in self
.re_CollectPot
.finditer(hand
.handText
):
533 hand
.addCollectPot(player
=m
.group('PNAME'),pot
=self
.clearMoneyString(m
.group('POT')))
535 def readShownCards(self
,hand
):
536 for m
in self
.re_ShownCards
.finditer(hand
.handText
):
537 if m
.group('CARDS') is not None:
538 cards
= renderCards(m
.group('CARDS'))
540 mucked
= m
.group('SHOWED') != "show"
542 hand
.addShownCards(cards
=cards
, player
=m
.group('PNAME'), shown
=True, mucked
=mucked
)
545 def getTableTitleRe(type, table_name
=None, tournament
= None, table_number
=None):
546 "Returns string to search in windows titles"
548 TableName
= table_name
.split(" ")
549 print 'party', 'getTableTitleRe', "%s.+Table\s#%s" % (TableName
[0], table_number
)
550 if len(TableName
[1]) > 6:
551 return "#%s" % (table_number
)
553 return "%s.+Table\s#%s" % (TableName
[0], table_number
)
557 def renderCards(string
):
558 "Splits strings like ' Js, 4d '"
559 cards
= string
.strip().split(' ')
560 return filter(len, map(lambda x
: x
.strip(' ,'), cards
))
563 if __name__
== "__main__":
564 parser
= OptionParser()
565 parser
.add_option("-i", "--input", dest
="ipath", help=_("parse input hand history"))
566 parser
.add_option("-o", "--output", dest
="opath", help=_("output translation to"), default
="-")
567 parser
.add_option("-f", "--follow", dest
="follow", help=_("follow (tail -f) the input"), action
="store_true", default
=False)
568 parser
.add_option("-q", "--quiet",
569 action
="store_const", const
=logging
.CRITICAL
, dest
="verbosity", default
=logging
.INFO
)
570 parser
.add_option("-v", "--verbose",
571 action
="store_const", const
=logging
.INFO
, dest
="verbosity")
572 parser
.add_option("--vv",
573 action
="store_const", const
=logging
.DEBUG
, dest
="verbosity")
575 (options
, args
) = parser
.parse_args()
577 e
= PartyPoker(in_path
= options
.ipath
, out_path
= options
.opath
, follow
= options
.follow
)