Coverage for PartyPokerToFpdb.py: 0%

486 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 19:33 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3# 

4# Copyright 2009-2011, Grigorij Indigirkin 

5# 

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. 

10# 

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. 

15# 

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######################################################################## 

20 

21# import L10n 

22# _ = L10n.get_translation() 

23 

24 

25from HandHistoryConverter import HandHistoryConverter, FpdbParseError, FpdbHandPartial 

26from decimal import Decimal 

27import re 

28import logging 

29import datetime 

30import time 

31 

32# PartyPoker HH Format 

33log = logging.getLogger("parser") 

34 

35 

36class PartyPoker(HandHistoryConverter): 

37 sitename = "PartyPoker" 

38 codepage = ("utf8", "cp1252") 

39 siteId = 9 

40 filetype = "text" 

41 sym = {"USD": "\$", "EUR": "\u20ac", "T$": "", "play": "play"} 

42 currencies = {"\$": "USD", "$": "USD", "\xe2\x82\xac": "EUR", "\u20ac": "EUR", "": "T$", "play": "play"} 

43 substitutions = { 

44 "LEGAL_ISO": "USD|EUR", # legal ISO currency codes 

45 "LS": "\$|\u20ac|\xe2\x82\xac|", # Currency symbols - Euro(cp1252, utf-8) 

46 "NUM": ".,'\dKMB", 

47 } 

48 limits = {"NL": "nl", "PL": "pl", "": "fl", "FL": "fl", "Limit": "fl"} 

49 games = { # base, category 

50 "Texas Hold'em": ("hold", "holdem"), 

51 "Texas Holdem": ("hold", "holdem"), 

52 "Hold'em": ("hold", "holdem"), 

53 "Holdem": ("hold", "holdem"), 

54 "Omaha": ("hold", "omahahi"), 

55 "Omaha Hi": ("hold", "omahahi"), 

56 "Omaha Hi-Lo": ("hold", "omahahilo"), 

57 "7 Card Stud Hi-Lo": ("stud", "studhilo"), 

58 "7 Card Stud": ("stud", "studhi"), 

59 "Double Hold'em": ("hold", "2_holdem"), 

60 "Double Holdem": ("hold", "2_holdem"), 

61 "Short Deck": ("hold", "6_holdem"), 

62 } 

63 

64 Lim_Blinds = { 

65 "0.04": ("0.01", "0.02"), 

66 "0.08": ("0.02", "0.04"), 

67 "0.10": ("0.02", "0.05"), 

68 "0.20": ("0.05", "0.10"), 

69 "0.30": ("0.07", "0.15"), 

70 "0.50": ("0.10", "0.25"), 

71 "1.00": ("0.25", "0.50"), 

72 "1": ("0.25", "0.50"), 

73 "2.00": ("0.50", "1.00"), 

74 "2": ("0.50", "1.00"), 

75 "4.00": ("1.00", "2.00"), 

76 "4": ("1.00", "2.00"), 

77 "6.00": ("1.00", "3.00"), 

78 "6": ("1.00", "3.00"), 

79 "8.00": ("2.00", "4.00"), 

80 "8": ("2.00", "4.00"), 

81 "10.00": ("2.00", "5.00"), 

82 "10": ("2.00", "5.00"), 

83 "12.00": ("3.00", "6.00"), 

84 "12": ("3.00", "6.00"), 

85 "20.00": ("5.00", "10.00"), 

86 "20": ("5.00", "10.00"), 

87 "30.00": ("10.00", "15.00"), 

88 "30": ("10.00", "15.00"), 

89 "40.00": ("10.00", "20.00"), 

90 "40": ("10.00", "20.00"), 

91 "50.00": ("10.00", "25.00"), 

92 "50": ("10.00", "25.00"), 

93 "60.00": ("15.00", "30.00"), 

94 "60": ("15.00", "30.00"), 

95 "80.00": ("20.00", "40.00"), 

96 "80": ("20.00", "40.00"), 

97 "100.00": ("25.00", "50.00"), 

98 "100": ("25.00", "50.00"), 

99 "200.00": ("50.00", "100.00"), 

100 "200": ("50.00", "100.00"), 

101 "400.00": ("100.00", "200.00"), 

102 "400": ("100.00", "200.00"), 

103 "500.00": ("125.00", "250.00"), 

104 "500": ("125.00", "250.00"), 

105 "800.00": ("200.00", "400.00"), 

106 "800": ("200.00", "400.00"), 

107 } 

108 NLim_Blinds_20bb = { 

109 "0.80": ("0.01", "0.02"), 

110 "1.60": ("0.02", "0.04"), 

111 "4": ("0.05", "0.10"), 

112 "10": ("0.10", "0.25"), 

113 "20": ("0.25", "0.50"), 

114 "40": ("0.50", "1.00"), 

115 "240": ("3.00", "6.00"), 

116 #'10': ('0.10', '0.25'), 

117 #'10': ('0.10', '0.25'), 

118 #'10': ('0.10', '0.25'), 

119 #'10': ('0.10', '0.25'), 

120 } 

121 

122 months = { 

123 "January": 1, 

124 "Jan": 1, 

125 "February": 2, 

126 "Feb": 2, 

127 "March": 3, 

128 "Mar": 3, 

129 "April": 4, 

130 "Apr": 4, 

131 "May": 5, 

132 "June": 6, 

133 "Jun": 6, 

134 "July": 7, 

135 "Jul": 7, 

136 "August": 8, 

137 "Aug": 8, 

138 "September": 9, 

139 "Sep": 9, 

140 "October": 10, 

141 "Oct": 10, 

142 "November": 11, 

143 "Nov": 11, 

144 "December": 12, 

145 "Dec": 12, 

146 } 

147 

148 sites = { 

149 "Poker Stars": ("PokerStars", 2), 

150 "PokerMaster": ("PokerMaster", 25), 

151 "IPoker": ("iPoker", 14), 

152 "Party": ("PartyPoker", 9), 

153 #'PMU Poker' : ('PMU Poker', 31), 

154 "Pacific": ("PacificPoker", 10), 

155 "WPN": ("WinningPoker", 24), 

156 "PokerBros": ("PokerBros", 29), 

157 } 

158 

159 # Static regexes 

160 re_GameInfo = re.compile( 

161 """ 

162 \*{5}\sHand\sHistory\s(F|f)or\sGame\s(?P<HID>\w+)\s\*{5}(\s\((?P<SITE>Poker\sStars|PokerMaster|Party|PartyPoker|IPoker|Pacific|WPN|PokerBros)\))?\s+ 

163 (.+?\shas\sleft\sthe\stable\.\s+)* 

164 (.+?\sfinished\sin\s\d+\splace\.\s+)* 

165 ((?P<CURRENCY>[%(LS)s]))?\s* 

166 ( 

167 ([%(LS)s]?(?P<SB>[%(NUM)s]+)/[%(LS)s]?(?P<BB>[%(NUM)s]+)\s*(?:%(LEGAL_ISO)s)?\s+(?P<FAST3>fastforward\s)?((?P<LIMIT3>NL|PL|FL|)\s+)?)| 

168 ((?P<CASHBI>[%(NUM)s]+)\s*(?:%(LEGAL_ISO)s)?\s*)(?P<FAST2>fastforward\s)?(?P<LIMIT2>(NL|PL|FL|))?\s* 

169 ) 

170 (Tourney\s*)? 

171 (?P<GAME>(Texas\sHold\'?em|Hold\'?em|Omaha\sHi-Lo|Omaha(\sHi)?|7\sCard\sStud\sHi-Lo|7\sCard\sStud|Double\sHold\'?em|Short\sDeck))\s* 

172 (Game\sTable\s*)? 

173 ( 

174 (\((?P<LIMIT>(NL|PL|FL|Limit|))\)\s*)? 

175 (\((?P<SNG>SNG|STT|MTT)(\sJackPot)?\sTournament\s\#(?P<TOURNO>\d+)\)\s*)? 

176 )? 

177 (?:\s\(Buyin\s(?P<BUYIN>[%(LS)s][%(NUM)s]+)\s\+\s(?P<FEE>[%(LS)s][%(NUM)s]+)\))? 

178 \s*-\s* 

179 (?P<DATETIME>.+) 

180 """ 

181 % substitutions, 

182 re.VERBOSE | re.UNICODE, 

183 ) 

184 

185 re_HandInfo = re.compile( 

186 """ 

187 Table\s(?P<TABLE>.+?)?\s+ 

188 ((?: \#|\(|)(?P<TABLENO>\d+)\)?\s+)? 

189 (\(No\sDP\)\s)? 

190 \(\s?(?P<PLAY>Real|Play)\s+Money\s?\)\s+(--\s*)? # FIXME: check if play money is correct 

191 Seat\s+(?P<BUTTON>\d+)\sis\sthe\sbutton 

192 (\s+Total\s+number\s+of\s+players\s+\:\s+(?P<PLYRS>\d+)/?(?P<MAX>\d+)?)? 

193 """, 

194 re.VERBOSE | re.MULTILINE | re.DOTALL, 

195 ) 

196 

197 re_GameInfoTrny1 = re.compile( 

198 """ 

199 \*{5}\sHand\sHistory\s(F|f)or\sGame\s(?P<HID>\w+)\s\*{5}\s+ 

200 (?P<LIMIT>(NL|PL|FL|))\s* 

201 (?P<GAME>(Texas\sHold\'em|Hold\'?em|Omaha\sHi-Lo|Omaha(\sHi)?|7\sCard\sStud\sHi-Lo|7\sCard\sStud|Double\sHold\'em|Short\sDeck))\s+ 

202 (?:(?P<BUYIN>[%(LS)s]?\s?[%(NUM)s]+)\s*(?P<BUYIN_CURRENCY>%(LEGAL_ISO)s)?\s*Buy-in\s+)? 

203 (\+\s(?P<FEE>[%(LS)s]?\s?[%(NUM)s]+)\sEntry\sFee\s+)? 

204 Trny:\s?(?P<TOURNO>\d+)\s+ 

205 Level:\s*(?P<LEVEL>\d+)\s+ 

206 ((Blinds|Stakes)(?:-Antes)?)\( 

207 (?P<SB>[%(NUM)s ]+)\s* 

208 /(?P<BB>[%(NUM)s ]+) 

209 (?:\s*-\s*(?P<ANTE>[%(NUM)s ]+)\$?)? 

210 \) 

211 \s*\-\s* 

212 (?P<DATETIME>.+) 

213 """ 

214 % substitutions, 

215 re.VERBOSE | re.UNICODE, 

216 ) 

217 

218 re_GameInfoTrny2 = re.compile( 

219 """ 

220 \*{5}\sHand\sHistory\s(F|f)or\sGame\s(?P<HID>\w+)\s\*{5}\s+ 

221 (?P<LIMIT>(NL|PL|FL|))\s* 

222 (?P<GAME>(Texas\sHold\'em|Hold\'?em|Omaha\sHi-Lo|Omaha(\sHi)?|7\sCard\sStud\sHi-Lo|7\sCard\sStud|Double\sHold\'em|Short\sDeck))\s+ 

223 (?:(?P<BUYIN>[%(LS)s]?\s?[%(NUM)s]+)\s*(?P<BUYIN_CURRENCY>%(LEGAL_ISO)s)?\s*Buy-in\s+)? 

224 (\+\s(?P<FEE>[%(LS)s]?\s?[%(NUM)s]+)\sEntry\sFee\s+)? 

225 \s*\-\s* 

226 (?P<DATETIME>.+) 

227 """ 

228 % substitutions, 

229 re.VERBOSE | re.UNICODE, 

230 ) 

231 

232 re_GameInfoTrny3 = re.compile( 

233 """ 

234 \*{5}\sHand\sHistory\s(F|f)or\sGame\s(?P<HID>\w+)\s\*{5}\s\((?P<SITE>Poker\sStars|PokerMaster|Party|IPoker|Pacific|WPN|PokerBros)\)\s+ 

235 Tourney\sHand\s 

236 (?P<LIMIT>(NL|PL|FL|))\s* 

237 (?P<GAME>(Texas\sHold\'em|Hold\'?em|Omaha\sHi-Lo|Omaha(\sHi)?|7\sCard\sStud\sHi-Lo|7\sCard\sStud|Double\sHold\'em|Short\sDeck))\s+ 

238 \s*\-\s* 

239 (?P<DATETIME>.+) 

240 """ 

241 % substitutions, 

242 re.VERBOSE | re.UNICODE, 

243 ) 

244 

245 re_Blinds = re.compile( 

246 """ 

247 ^((Blinds|Stakes)(?:-Antes)?)\( 

248 (?P<SB>[%(NUM)s ]+)\s* 

249 /(?P<BB>[%(NUM)s ]+) 

250 (?:\s*-\s*(?P<ANTE>[%(NUM)s ]+)\$?)? 

251 \)$""" 

252 % substitutions, 

253 re.VERBOSE | re.MULTILINE, 

254 ) 

255 

256 re_TourNoLevel = re.compile( 

257 """ 

258 Trny:\s?(?P<TOURNO>\d+)\s+ 

259 Level:\s*(?P<LEVEL>\d+) 

260 """, 

261 re.VERBOSE, 

262 ) 

263 

264 re_PlayerInfo = re.compile( 

265 """ 

266 (S|s)eat\s?(?P<SEAT>\d+):\s 

267 (?P<PNAME>.*)\s 

268 \(\s*[%(LS)s]?(?P<CASH>[%(NUM)s]+)\s*(?:%(LEGAL_ISO)s|)\s*\) 

269 """ 

270 % substitutions, 

271 re.VERBOSE | re.UNICODE, 

272 ) 

273 

274 re_NewLevel = re.compile( 

275 "Blinds(-Antes)?\((?P<SB>[%(NUM)s ]+)/(?P<BB>[%(NUM)s ]+)(?:\s*-\s*(?P<ANTE>[%(NUM)s ]+))?\)" % substitutions, 

276 re.VERBOSE | re.MULTILINE | re.DOTALL, 

277 ) 

278 re_CountedSeats = re.compile("Total\s+number\s+of\s+players\s*:\s*(?P<COUNTED_SEATS>\d+)", re.MULTILINE) 

279 re_Identify = re.compile("\*{5}\sHand\sHistory\s[fF]or\sGame\s\d+\w+?\s") 

280 re_SplitHands = re.compile("\n\n+") 

281 re_TailSplitHands = re.compile("(\x00+)") 

282 lineSplitter = "\n" 

283 re_Button = re.compile("Seat (?P<BUTTON>\d+) is the button", re.MULTILINE) 

284 re_Board = re.compile(r"\[(?P<CARDS>.+)\]") 

285 re_NoSmallBlind = re.compile( 

286 "^There is no Small Blind in this hand as the Big Blind " "of the previous hand left the table", re.MULTILINE 

287 ) 

288 re_20BBmin = re.compile(r"Table 20BB Min") 

289 re_Cancelled = re.compile("Table\sClosed\s?", re.MULTILINE) 

290 re_Disconnected = re.compile("Connection\sLost\sdue\sto\ssome\sreason\s?", re.MULTILINE) 

291 re_GameStartLine = re.compile("Game\s\#\d+\sstarts", re.MULTILINE) 

292 re_emailedHand = re.compile(r"\*\*\sSummary\s\*\*") 

293 

294 def allHandsAsList(self): 

295 list = HandHistoryConverter.allHandsAsList(self) 

296 if list is None: 

297 return [] 

298 return filter(lambda text: len(text.strip()), list) 

299 

300 def compilePlayerRegexs(self, hand): 

301 players = set([player[1] for player in hand.players]) 

302 if not players <= self.compiledPlayers: # x <= y means 'x is subset of y' 

303 self.compiledPlayers = players 

304 player_re = "(?P<PNAME>" + "|".join(map(re.escape, players)) + ")" 

305 subst = { 

306 "PLYR": player_re, 

307 "CUR_SYM": self.sym[hand.gametype["currency"]], 

308 "CUR": hand.gametype["currency"] if hand.gametype["currency"] != "T$" else "(chips|)", 

309 "BRAX": "\[\(\)\]", 

310 } 

311 self.re_PostSB = re.compile( 

312 r"%(PLYR)s posts small blind [%(BRAX)s]?%(CUR_SYM)s?(?P<SB>[.,0-9]+)\s*(%(CUR)s)?[%(BRAX)s]?\.?\s*$" 

313 % subst, 

314 re.MULTILINE, 

315 ) 

316 self.re_PostBB = re.compile( 

317 r"%(PLYR)s posts big blind [%(BRAX)s]?%(CUR_SYM)s?(?P<BB>[.,0-9]+)\s*(%(CUR)s)?[%(BRAX)s]?\.?\s*$" 

318 % subst, 

319 re.MULTILINE, 

320 ) 

321 self.re_PostDead = re.compile( 

322 r"%(PLYR)s posts big blind \+ dead [%(BRAX)s]?%(CUR_SYM)s?(?P<BBNDEAD>[.,0-9]+)\s*%(CUR_SYM)s?[%(BRAX)s]?\.?\s*$" 

323 % subst, 

324 re.MULTILINE, 

325 ) 

326 self.re_PostBUB = re.compile( 

327 r"%(PLYR)s posts button blind ?[%(BRAX)s]?%(CUR_SYM)s?(?P<BUB>[.,0-9]+)\s*%(CUR)s?[%(BRAX)s]?\.?\s*$" 

328 % subst, 

329 re.MULTILINE, 

330 ) 

331 self.re_Antes = re.compile( 

332 r"%(PLYR)s posts ante( of)? [%(BRAX)s]?%(CUR_SYM)s?(?P<ANTE>[.,0-9]+)\s*(%(CUR)s)?[%(BRAX)s]?\.?\s*$" 

333 % subst, 

334 re.MULTILINE, 

335 ) 

336 self.re_HeroCards = re.compile(r"Dealt to %(PLYR)s \[\s*(?P<NEWCARDS>.+)\s*\]" % subst, re.MULTILINE) 

337 self.re_Action = re.compile( 

338 r""" 

339 (?P<PNAME>.+?)\s(?P<ATYPE>bets|checks|raises|completes|bring-ins|calls|folds|is\sall-In|double\sbets) 

340 (?:\s*[%(BRAX)s]?\s?%(CUR_SYM)s?(?P<BET>[.,\d]+)\s*(%(CUR)s)?\s?[%(BRAX)s]?)? 

341 (\sto\s[.,\d]+)? 

342 \.?\s*$""" 

343 % subst, 

344 re.MULTILINE | re.VERBOSE, 

345 ) 

346 if not hand.emailedHand: 

347 self.re_ShownCards = re.compile( 

348 r"%s (?P<SHOWED>(?:doesn\'t )?shows?) " % player_re + r"\[ *(?P<CARDS>.+) *\](?P<COMBINATION>.+)\.", 

349 re.MULTILINE, 

350 ) 

351 else: 

352 # Michow111 balance $113, bet $50, collected $110.25, net +$60.25 [ 8h 9h ] [ a straight, seven to jack -- Jc,Td,9h,8h,7c ] 

353 # babunchik balance $0, lost $50 [ Kd Js ] [ a pair of jacks -- Kd,Js,Jc,Td,7c ] 

354 self.re_ShownCards = re.compile( 

355 r"%(PLYR)s balance.*" % subst + r"\[ (?P<CARDS>.+) \] *\[ *(?P<COMBINATION>.+) \-\-", re.MULTILINE 

356 ) 

357 if not hand.emailedHand: 

358 self.re_CollectPot = re.compile( 

359 r"""%(PLYR)s\s+wins\s+(Lo\s\()?%(CUR_SYM)s?(?P<POT>[.,\d]+)\s*(%(CUR)s)?\)?""" % subst, 

360 re.MULTILINE | re.VERBOSE, 

361 ) 

362 else: 

363 self.re_CollectPot = re.compile( 

364 r"""%(PLYR)s(\sbalance\s%(CUR_SYM)s?[.,\d]+,)(\sbet\s%(CUR_SYM)s?[.,\d]+,)(\scollected\s%(CUR_SYM)s?(?P<POT>[.,\d]+),)""" 

365 % subst, 

366 re.MULTILINE | re.VERBOSE, 

367 ) 

368 

369 def readSupportedGames(self): 

370 return [ 

371 ["ring", "hold", "nl"], 

372 ["ring", "hold", "pl"], 

373 ["ring", "hold", "fl"], 

374 ["ring", "stud", "fl"], 

375 ["tour", "hold", "nl"], 

376 ["tour", "hold", "pl"], 

377 ["tour", "hold", "fl"], 

378 ["tour", "stud", "fl"], 

379 ] 

380 

381 def determineGameType(self, handText): 

382 handText = handText.replace("\x00", "") 

383 info, extra = {}, {} 

384 m = self.re_GameInfo.search(handText) 

385 if not m: 

386 m = self.re_GameInfoTrny1.search(handText) 

387 if not m: 

388 m = self.re_GameInfoTrny2.search(handText) 

389 m2 = self.re_TourNoLevel.search(handText) 

390 m3 = self.re_Blinds.search(handText) 

391 if m2 and m3: 

392 extra.update(m2.groupdict()) 

393 extra.update(m3.groupdict()) 

394 else: 

395 m = None 

396 if not m: 

397 m = self.re_GameInfoTrny3.search(handText) 

398 if not m: 

399 m = self.re_Disconnected.search(handText) 

400 if m: 

401 message = "Player Disconnected" 

402 raise FpdbHandPartial("Partial hand history: %s" % message) 

403 m = self.re_Cancelled.search(handText) 

404 if m: 

405 message = "Table Closed" 

406 raise FpdbHandPartial("Partial hand history: %s" % message) 

407 m = self.re_GameStartLine.match(handText) 

408 if m and len(handText) < 50: 

409 message = "Game start line" 

410 raise FpdbHandPartial("Partial hand history: %s" % message) 

411 tmp = handText[0:200] 

412 log.error(("PartyPokerToFpdb.determineGameType: '%s'") % tmp) 

413 raise FpdbParseError 

414 

415 mg = m.groupdict() 

416 mg.update(extra) 

417 # print "DEBUG: mg: %s" % mg 

418 

419 if "SITE" in mg and mg["SITE"] is not None: 

420 self.sitename = self.sites[mg["SITE"]][0] 

421 self.siteId = self.sites[mg["SITE"]][1] # Needs to match id entry in Sites database 

422 print("self.siteId", self.siteId) 

423 print("self.sitename", self.sitename) 

424 if "LIMIT" in mg and mg["LIMIT"] is not None: 

425 info["limitType"] = self.limits[mg["LIMIT"]] 

426 if "LIMIT2" in mg and mg["LIMIT2"] is not None: 

427 info["limitType"] = self.limits[mg["LIMIT2"]] 

428 if "LIMIT3" in mg and mg["LIMIT3"] is not None: 

429 info["limitType"] = self.limits[mg["LIMIT3"]] 

430 if "FAST2" in mg and mg["FAST2"] is not None: 

431 info["fast"] = True 

432 elif "FAST3" in mg and mg["FAST3"] is not None: 

433 info["fast"] = True 

434 else: 

435 info["fast"] = False 

436 if mg["LIMIT"] is None and mg["LIMIT2"] is None and mg["LIMIT3"] is None: 

437 info["limitType"] = "fl" 

438 if "GAME" in mg: 

439 (info["base"], info["category"]) = self.games[mg["GAME"]] 

440 if "CASHBI" in mg and mg["CASHBI"] is not None: 

441 # The summary is using buyin rather then listing the blinds 

442 # Only with NL games? 

443 mg["CASHBI"] = self.clearMoneyString(mg["CASHBI"]) 

444 m_20BBmin = self.re_20BBmin.search(handText) 

445 if m_20BBmin is not None: 

446 try: 

447 info["sb"] = self.NLim_Blinds_20bb[mg["CASHBI"]][0] 

448 info["bb"] = self.NLim_Blinds_20bb[mg["CASHBI"]][1] 

449 info["buyinType"] = "shallow" 

450 except KeyError: 

451 tmp = handText[0:200] 

452 log.error( 

453 ("PartyPokerToFpdb.determineGameType: NLim_Blinds_20bb has no lookup for '%s' - '%s'") 

454 % (mg["CASHBI"], tmp) 

455 ) 

456 raise FpdbParseError 

457 else: 

458 try: 

459 if Decimal(mg["CASHBI"]) >= 10000: 

460 nl_bb = str((Decimal(mg["CASHBI"]) / 100).quantize(Decimal("0.01"))) 

461 info["buyinType"] = "deep" 

462 else: 

463 nl_bb = str((Decimal(mg["CASHBI"]) / 50).quantize(Decimal("0.01"))) 

464 info["buyinType"] = "regular" 

465 info["sb"] = self.Lim_Blinds[nl_bb][0] 

466 info["bb"] = self.Lim_Blinds[nl_bb][1] 

467 except KeyError: 

468 tmp = handText[0:200] 

469 log.error( 

470 ("PartyPokerToFpdb.determineGameType: Lim_Blinds has no lookup for '%s' - '%s'") % (nl_bb, tmp) 

471 ) 

472 raise FpdbParseError 

473 else: 

474 if info["category"] == "6_holdem": 

475 info["sb"] = "0" 

476 info["bb"] = self.clearMoneyString(mg["SB"]) 

477 else: 

478 m = self.re_NewLevel.search(handText) 

479 if m: 

480 mg["SB"] = m.group("SB") 

481 mg["BB"] = m.group("BB") 

482 if "SB" in mg: 

483 info["sb"] = self.clearMoneyString(mg["SB"]) 

484 else: 

485 info["sb"] = None 

486 if "BB" in mg: 

487 info["bb"] = self.clearMoneyString(mg["BB"]) 

488 else: 

489 info["bb"] = None 

490 info["buyinType"] = "regular" 

491 if "CURRENCY" in mg: 

492 if mg["CURRENCY"] is None: 

493 info["currency"] = self.currencies["$"] 

494 else: 

495 info["currency"] = self.currencies[mg["CURRENCY"]] 

496 if "MIXED" in mg: 

497 if mg["MIXED"] is not None: 

498 info["mix"] = self.mixes[mg["MIXED"]] 

499 

500 if "TOURNO" in mg and mg["TOURNO"] is None: 

501 info["type"] = "ring" 

502 else: 

503 info["type"] = "tour" 

504 info["currency"] = "T$" 

505 

506 if info["limitType"] == "fl" and info["bb"] is not None: 

507 if info["type"] == "ring": 

508 try: 

509 info["sb"] = self.Lim_Blinds[mg["BB"]][0] 

510 info["bb"] = self.Lim_Blinds[mg["BB"]][1] 

511 except KeyError: 

512 tmp = handText[0:200] 

513 log.error( 

514 ("PartyPokerToFpdb.determineGameType: Lim_Blinds has no lookup for '%s' - '%s'") 

515 % (mg["BB"], tmp) 

516 ) 

517 raise FpdbParseError 

518 else: 

519 info["sb"] = str((Decimal(mg["SB"]) / 2).quantize(Decimal("0.01"))) 

520 info["bb"] = str(Decimal(mg["SB"]).quantize(Decimal("0.01"))) 

521 # print "DEUBG: DGT.info: %s" % info 

522 return info 

523 

524 def readHandInfo(self, hand): 

525 info, m2, extra, type3 = {}, None, {}, False 

526 hand.handText = hand.handText.replace("\x00", "") 

527 if self.re_emailedHand.search(hand.handText): 

528 hand.emailedHand = True 

529 else: 

530 hand.emailedHand = False 

531 m = self.re_HandInfo.search(hand.handText, re.DOTALL) 

532 if hand.gametype["type"] == "ring" or hand.emailedHand: 

533 m2 = self.re_GameInfo.search(hand.handText) 

534 else: 

535 m2 = self.re_GameInfoTrny1.search(hand.handText) 

536 if not m2: 

537 m2 = self.re_GameInfoTrny2.search(hand.handText) 

538 m3 = self.re_TourNoLevel.search(hand.handText) 

539 m4 = self.re_Blinds.search(hand.handText) 

540 if m3 and m4: 

541 extra.update(m3.groupdict()) 

542 extra.update(m4.groupdict()) 

543 else: 

544 m2 = self.re_GameInfoTrny3.search(hand.handText) 

545 type3 = True 

546 if m is None or m2 is None: 

547 tmp = hand.handText[0:200] 

548 log.error(("PartyPokerToFpdb.readHandInfo: '%s'") % tmp) 

549 raise FpdbParseError 

550 info.update(m.groupdict()) 

551 info.update(m2.groupdict()) 

552 info.update(extra) 

553 

554 for key in info: 

555 if key == "DATETIME": 

556 # Saturday, July 25, 07:53:52 EDT 2009 

557 # Thursday, July 30, 21:40:41 MSKS 2009 

558 # Sunday, October 25, 13:39:07 MSK 2009 

559 # Mon Jul 12 13:38:32 EDT 2010 

560 timezone = "ET" 

561 m2 = re.search( 

562 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+)", 

563 info[key], 

564 re.UNICODE, 

565 ) 

566 if m2.group("TZ"): 

567 timezone = m2.group("TZ") 

568 month = self.months[m2.group("M")] 

569 datetimestr = "%s/%s/%s %s:%s:%s" % ( 

570 m2.group("Y"), 

571 month, 

572 m2.group("D"), 

573 m2.group("H"), 

574 m2.group("MIN"), 

575 m2.group("S"), 

576 ) 

577 hand.startTime = datetime.datetime.strptime(datetimestr, "%Y/%m/%d %H:%M:%S") 

578 hand.startTime = HandHistoryConverter.changeTimezone(hand.startTime, timezone, "UTC") 

579 # FIXME: some timezone correction required 

580 # tzShift = defaultdict(lambda:0, {'EDT': -5, 'EST': -6, 'MSKS': 3}) 

581 # hand.starttime -= datetime.timedelta(hours=tzShift[m2.group('TZ')]) 

582 

583 if key == "HID": 

584 if str(info[key]) == "1111111111": 

585 hand.handid = str(int(time.time() * 1000)) 

586 hand.roundPenny = True 

587 else: 

588 if re.search("[a-z]", info[key]): 

589 hand.handid = info[key][:13] 

590 hand.roundPenny = True 

591 else: 

592 hand.handid = info[key] 

593 if key == "TABLE": 

594 if "TOURNO" in info and info["TOURNO"] is None: 

595 if info["TABLENO"] is not None: 

596 hand.tablename = info[key] + " " + info["TABLENO"] 

597 else: 

598 hand.tablename = info[key] 

599 else: 

600 hand.tablename = info["TABLENO"] 

601 if key == "BUTTON": 

602 hand.buttonpos = info[key] 

603 if key == "TOURNO": 

604 hand.tourNo = info[key] 

605 if hand.emailedHand: 

606 hand.buyin = 0 

607 hand.fee = 0 

608 hand.buyinCurrency = "NA" 

609 if key == "TABLE_ID_WRAPPER": 

610 if info[key] == "#": 

611 # FIXME: there is no such property in Hand class 

612 self.isSNG = True 

613 if key == "BUYIN": 

614 if info.get("TABLE") is not None and "Freeroll" in info.get("TABLE"): 

615 # Freeroll tourney 

616 hand.buyin = 0 

617 hand.fee = 0 

618 hand.buyinCurrency = "FREE" 

619 elif info[key] is None: 

620 # Freeroll tourney 

621 hand.buyin = 0 

622 hand.fee = 0 

623 hand.buyinCurrency = "NA" 

624 elif hand.tourNo is not None: 

625 hand.buyin = 0 

626 hand.fee = 0 

627 hand.buyinCurrency = "NA" 

628 if info[key].find("$") != -1: 

629 hand.buyinCurrency = "USD" 

630 elif info[key].find("€") != -1: 

631 hand.buyinCurrency = "EUR" 

632 else: 

633 log.error( 

634 ("PartyPokerToFpdb.readHandInfo: Failed to detect currency Hand ID: '%s' - '%s'") 

635 % (hand.handid, info[key]) 

636 ) 

637 raise FpdbParseError 

638 info[key] = self.clearMoneyString(info[key].strip("$€")) 

639 hand.buyin = int(100 * Decimal(info[key])) 

640 if "FEE" in info and info["FEE"] is not None: 

641 info["FEE"] = self.clearMoneyString(info["FEE"].strip("$€")) 

642 hand.fee = int(100 * Decimal(info["FEE"])) 

643 if key == "LEVEL": 

644 hand.level = info[key] 

645 if key == "PLAY" and info["PLAY"] != "Real": 

646 # if realy party doesn's save play money hh 

647 hand.gametype["currency"] = "play" 

648 if key == "MAX" and info[key] is not None: 

649 hand.maxseats = int(info[key]) 

650 

651 if type3: 

652 hand.tourNo = info["TABLE"] 

653 hand.buyin = 0 

654 hand.fee = 0 

655 hand.buyinCurrency = "NA" 

656 

657 def readButton(self, hand): 

658 m = self.re_Button.search(hand.handText) 

659 if m: 

660 hand.buttonpos = int(m.group("BUTTON")) 

661 else: 

662 log.info("readButton: " + ("not found")) 

663 

664 def readPlayerStacks(self, hand): 

665 log.debug("readPlayerStacks") 

666 m = self.re_PlayerInfo.finditer(hand.handText) 

667 maxKnownStack = 0 

668 zeroStackPlayers = [] 

669 self.playerMap = {} 

670 for a in m: 

671 pname = a.group("PNAME") 

672 if hand.emailedHand: 

673 subst = {"PLYR": re.escape(a.group("PNAME")), "SPACENAME": "\s(.+)? "} 

674 re_PlayerName = re.compile( 

675 r"""^%(PLYR)s(?P<PNAMEEXTRA>%(SPACENAME)s)balance\s""" % subst, re.MULTILINE | re.VERBOSE 

676 ) 

677 m1 = re_PlayerName.search(hand.handText) 

678 if m1 and len(m1.group("PNAMEEXTRA")) > 1: 

679 pname = a.group("PNAME") + m1.group("PNAMEEXTRA") 

680 pname = pname.strip() 

681 self.playerMap[a.group("PNAME")] = pname 

682 if a.group("CASH") > "0": 

683 # record max known stack for use with players with unknown stack 

684 maxKnownStack = max(a.group("CASH"), maxKnownStack) 

685 hand.addPlayer(int(a.group("SEAT")), pname, self.clearMoneyString(a.group("CASH"))) 

686 else: 

687 # zero stacked players are added later 

688 zeroStackPlayers.append([int(a.group("SEAT")), pname, self.clearMoneyString(a.group("CASH"))]) 

689 if hand.gametype["type"] == "ring": 

690 # finds first vacant seat after an exact seat 

691 def findFirstEmptySeat(startSeat): 

692 i = 0 

693 while startSeat in occupiedSeats: 

694 if (startSeat >= hand.maxseats and hand.maxseats is not None) or len( 

695 occupiedSeats 

696 ) >= hand.maxseats: 

697 startSeat = 0 

698 startSeat += 1 

699 i += 1 

700 if i > 10: 

701 break 

702 return startSeat 

703 

704 re_HoleCards = re.compile(r"\*{2} Dealing down cards \*{2}") 

705 # re_SplitTest = re.compile(r"(joined the table|left the table|is sitting out)") 

706 re_JoiningPlayers = re.compile(r"(?P<PLAYERNAME>.+?) has joined the table") 

707 re_BBPostingPlayers = re.compile(r"(?P<PLAYERNAME>.+?) posts big blind", re.MULTILINE) 

708 re_LeavingPlayers = re.compile(r"(?P<PLAYERNAME>.+?) has left the table") 

709 re_PreDeal = re_HoleCards.split(hand.handText)[0] 

710 

711 match_JoiningPlayers = re_JoiningPlayers.findall(re_PreDeal) 

712 match_LeavingPlayers = re_LeavingPlayers.findall(re_PreDeal) 

713 match_BBPostingPlayers = [] 

714 m = re_BBPostingPlayers.finditer(re_PreDeal) 

715 for player in m: 

716 match_BBPostingPlayers.append(player.group("PLAYERNAME")) 

717 

718 # add every player with zero stack, but: 

719 # if a zero stacked player is just joined the table in this very hand then set his stack to maxKnownStack 

720 for p in zeroStackPlayers: 

721 if p[1] in match_JoiningPlayers: 

722 p[2] = self.clearMoneyString(str(maxKnownStack)) 

723 if not p[1] in match_LeavingPlayers: 

724 hand.addPlayer(p[0], p[1], p[2]) 

725 

726 seatedPlayers = list([(f[1]) for f in hand.players]) 

727 

728 # it works for all known cases as of 2010-09-28 

729 # should be refined with using match_ActivePlayers instead of match_BBPostingPlayers 

730 # as a leaving and rejoining player could be active without posting a BB (sample HH needed) 

731 unseatedActivePlayers = list(set(match_BBPostingPlayers) - set(seatedPlayers)) 

732 

733 if unseatedActivePlayers: 

734 for player in unseatedActivePlayers: 

735 occupiedSeats = list([(f[0]) for f in hand.players]) 

736 occupiedSeats.sort() 

737 # previousBBPoster = match_BBPostingPlayers[match_BBPostingPlayers.index(player)-1] 

738 # previousBBPosterSeat = dict([(f[1], f[0]) for f in hand.players])[previousBBPoster] 

739 # newPlayerSeat = findFirstEmptySeat(previousBBPosterSeat) 

740 # The commented out code above is 'correct' unless the unseated player is the only BB 

741 # I'm willing to live with the unseated player being placed in the lowest seat for now. 

742 newPlayerSeat = findFirstEmptySeat(1) 

743 hand.addPlayer(newPlayerSeat, player, self.clearMoneyString(str(maxKnownStack))) 

744 

745 def markStreets(self, hand): 

746 if hand.gametype["base"] in ("hold"): 

747 m = re.search( 

748 r"\*{2} Dealing down cards \*{2}" 

749 r"(?P<PREFLOP>.+?)" 

750 r"(?:\*{2} Dealing Flop \*{2} (:?\s*)?(?P<FLOP>\[ \S\S, \S\S, \S\S \].+?))?" 

751 r"(?:\*{2} Dealing Turn \*{2} (:?\s*)?(?P<TURN>\[ \S\S \].+?))?" 

752 r"(?:\*{2} Dealing River \*{2} (:?\s*)?(?P<RIVER>\[ \S\S \].+?))?$", 

753 hand.handText, 

754 re.DOTALL, 

755 ) 

756 elif hand.gametype["base"] in ("stud"): 

757 m = re.search( 

758 r"(?P<ANTES>.+(?=\*\* Dealing \*\*)|.+)" 

759 r"(\*\* Dealing \*\*(?P<THIRD>.+(?=\*\* Dealing Fourth street \*\*)|.+))?" 

760 r"(\*\* Dealing Fourth street \*\*(?P<FOURTH>.+(?=\*\* Dealing Fifth street \*\*)|.+))?" 

761 r"(\*\* Dealing Fifth street \*\*(?P<FIFTH>.+(?=\*\* Dealing Sixth street \*\*)|.+))?" 

762 r"(\*\* Dealing Sixth street \*\*(?P<SIXTH>.+(?=\*\* Dealing River \*\*)|.+))?" 

763 r"(\*\* Dealing River \*\*(?P<SEVENTH>.+))?", 

764 hand.handText, 

765 re.DOTALL, 

766 ) 

767 

768 hand.addStreets(m) 

769 

770 def readCommunityCards(self, hand, street): 

771 if street in ("FLOP", "TURN", "RIVER"): 

772 m = self.re_Board.search(hand.streets[street]) 

773 hand.setCommunityCards(street, renderCards(m.group("CARDS"))) 

774 

775 def readAntes(self, hand): 

776 log.debug("reading antes") 

777 m = self.re_Antes.finditer(hand.handText) 

778 for player in m: 

779 hand.addAnte(player.group("PNAME"), self.clearMoneyString(player.group("ANTE"))) 

780 

781 def readBlinds(self, hand): 

782 noSmallBlind = bool(self.re_NoSmallBlind.search(hand.handText)) 

783 if ( 

784 hand.gametype["type"] == "ring" 

785 or hand.gametype["sb"] is None 

786 or hand.gametype["bb"] is None 

787 or hand.roundPenny 

788 ): 

789 try: 

790 assert noSmallBlind is False 

791 for m in self.re_PostSB.finditer(hand.handText): 

792 hand.addBlind(m.group("PNAME"), "small blind", self.clearMoneyString(m.group("SB"))) 

793 if hand.gametype["sb"] is None: 

794 hand.gametype["sb"] = self.clearMoneyString(m.group("SB")) 

795 except (AttributeError, IndexError): # no small blind 

796 hand.addBlind(None, None, None) 

797 

798 for a in self.re_PostBB.finditer(hand.handText): 

799 hand.addBlind(a.group("PNAME"), "big blind", self.clearMoneyString(a.group("BB"))) 

800 if hand.gametype["bb"] is None: 

801 hand.gametype["bb"] = self.clearMoneyString(a.group("BB")) 

802 

803 for a in self.re_PostBUB.finditer(hand.handText): 

804 hand.addBlind(a.group("PNAME"), "button blind", self.clearMoneyString(a.group("BUB"))) 

805 

806 for a in self.re_PostDead.finditer(hand.handText): 

807 hand.addBlind(a.group("PNAME"), "both", self.clearMoneyString(a.group("BBNDEAD"))) 

808 else: 

809 # party doesn't track blinds for tournaments 

810 # so there're some cra^Wcaclulations 

811 if hand.buttonpos == 0: 

812 self.readButton(hand) 

813 # NOTE: code below depends on Hand's implementation 

814 # playersMap - dict {seat: (pname,stack)} 

815 playersMap = dict([(f[0], f[1:3]) for f in hand.players if f[1] in hand.handText.split("Trny:")[-1]]) 

816 maxSeat = max(playersMap) 

817 

818 def findFirstNonEmptySeat(startSeat): 

819 while startSeat not in playersMap: 

820 if startSeat >= maxSeat: 

821 startSeat = 0 

822 startSeat += 1 

823 return startSeat 

824 

825 smartMin = lambda A, B: A if float(A) <= float(B) else B 

826 

827 if noSmallBlind: 

828 hand.addBlind(None, None, None) 

829 smallBlindSeat = int(hand.buttonpos) 

830 else: 

831 if len(hand.players) == 2: 

832 smallBlindSeat = int(hand.buttonpos) 

833 else: 

834 smallBlindSeat = findFirstNonEmptySeat(int(hand.buttonpos) + 1) 

835 blind = smartMin(hand.sb, playersMap[smallBlindSeat][1]) 

836 hand.addBlind(playersMap[smallBlindSeat][0], "small blind", blind) 

837 

838 if hand.gametype["category"] == "6_holdem": 

839 bigBlindSeat = findFirstNonEmptySeat(smallBlindSeat + 1) 

840 blind = smartMin(hand.bb, playersMap[bigBlindSeat][1]) 

841 hand.addBlind(playersMap[bigBlindSeat][0], "button blind", blind) 

842 else: 

843 bigBlindSeat = findFirstNonEmptySeat(smallBlindSeat + 1) 

844 blind = smartMin(hand.bb, playersMap[bigBlindSeat][1]) 

845 hand.addBlind(playersMap[bigBlindSeat][0], "big blind", blind) 

846 

847 def readBringIn(self, hand): 

848 pass 

849 # m = self.re_BringIn.search(hand.handText,re.DOTALL) 

850 # if m: 

851 # #~ logging.debug("readBringIn: %s for %s" %(m.group('PNAME'), m.group('BRINGIN'))) 

852 # hand.addBringIn(m.group('PNAME'), m.group('BRINGIN')) 

853 

854 def readHoleCards(self, hand): 

855 # we need to grab hero's cards 

856 for street in ("PREFLOP",): 

857 if street in hand.streets.keys(): 

858 m = self.re_HeroCards.finditer(hand.streets[street]) 

859 for found in m: 

860 hand.hero = found.group("PNAME") 

861 newcards = renderCards(found.group("NEWCARDS")) 

862 hand.addHoleCards(street, hand.hero, closed=newcards, shown=False, mucked=False, dealt=True) 

863 

864 for street, text in hand.streets.iteritems(): 

865 if not text or street in ("PREFLOP", "DEAL"): 

866 continue # already done these 

867 m = self.re_HeroCards.finditer(hand.streets[street]) 

868 for found in m: 

869 player = found.group("PNAME") 

870 if street != "SEVENTH": 

871 newcards = renderCards(found.group("NEWCARDS")) 

872 oldcards = [] 

873 else: 

874 oldcards = renderCards(found.group("NEWCARDS")) 

875 newcards = [] 

876 

877 if street == "THIRD" and len(newcards) == 3: # hero in stud game 

878 hand.hero = player 

879 hand.dealt.add(player) # need this for stud?? 

880 hand.addHoleCards( 

881 street, player, closed=newcards[0:2], open=[newcards[2]], shown=False, mucked=False, dealt=False 

882 ) 

883 else: 

884 hand.addHoleCards( 

885 street, player, open=newcards, closed=oldcards, shown=False, mucked=False, dealt=False 

886 ) 

887 

888 def readAction(self, hand, street): 

889 m = self.re_Action.finditer(hand.streets[street]) 

890 for action in m: 

891 action.groupdict() 

892 # print "DEBUG: acts: %s %s" % (street, acts) 

893 playerName = action.group("PNAME") 

894 if ":" in playerName: 

895 continue # captures chat 

896 if self.playerMap.get(playerName): 

897 playerName = self.playerMap.get(playerName) 

898 amount = self.clearMoneyString(action.group("BET")) if action.group("BET") else None 

899 actionType = action.group("ATYPE") 

900 

901 if actionType == "folds": 

902 hand.addFold(street, playerName) 

903 elif actionType == "checks": 

904 hand.addCheck(street, playerName) 

905 elif actionType == "calls": 

906 hand.addCall(street, playerName, amount) 

907 elif actionType == "raises": 

908 if street == "PREFLOP" and playerName in [ 

909 item[0] for item in hand.actions["BLINDSANTES"] if item[2] != "ante" 

910 ]: 

911 # preflop raise from blind 

912 hand.addCallandRaise(street, playerName, amount) 

913 else: 

914 hand.addCallandRaise(street, playerName, amount) 

915 elif actionType == "bets" or actionType == "double bets": 

916 hand.addBet(street, playerName, amount) 

917 elif actionType == "completes": 

918 hand.addComplete(street, playerName, amount) 

919 elif actionType == "bring-ins": 

920 hand.addBringIn(playerName, amount) 

921 elif actionType == "is all-In": 

922 if amount: 

923 hand.addAllIn(street, playerName, amount) 

924 else: 

925 log.error( 

926 (("PartyPokerToFpdb: Unimplemented %s: '%s' '%s'") + " hid:%s") 

927 % ("readAction", playerName, actionType, hand.handid) 

928 ) 

929 raise FpdbParseError 

930 

931 def readShowdownActions(self, hand): 

932 # all action in readShownCards 

933 pass 

934 

935 def readCollectPot(self, hand): 

936 hand.setUncalledBets(True) 

937 for m in self.re_CollectPot.finditer(hand.handText): 

938 hand.addCollectPot(player=m.group("PNAME"), pot=self.clearMoneyString(m.group("POT"))) 

939 

940 def readShownCards(self, hand): 

941 for m in self.re_ShownCards.finditer(hand.handText): 

942 if m.group("CARDS") is not None: 

943 cards = renderCards(m.group("CARDS")) 

944 

945 mucked = "SHOWED" in m.groupdict() and m.group("SHOWED") != "show" 

946 

947 hand.addShownCards( 

948 cards=cards, player=m.group("PNAME"), shown=True, mucked=mucked, string=m.group("COMBINATION") 

949 ) 

950 

951 @staticmethod 

952 def getTableTitleRe(type, table_name=None, tournament=None, table_number=None): 

953 "Returns string to search in windows titles" 

954 log.info( 

955 "Party.getTableTitleRe: table_name='%s' tournament='%s' table_number='%s'" 

956 % (table_name, tournament, table_number) 

957 ) 

958 regex = "%s" % (table_name) 

959 if type == "tour": 

960 if table_name: 

961 TableName = table_name.split(" ") 

962 if len(TableName[1]) > 6: 

963 regex = "#?%s" % (table_number) 

964 else: 

965 regex = "%s.+Table\s#?%s" % (TableName[0], table_number) 

966 else: 

967 # 

968 # sng's seem to get passed in with: 

969 # table_name = None 

970 # tournament=8-digit tourney number 

971 # table_number = 7 digit table number 

972 # screen string is normally Turbo|Speed|(etc) #table_number 

973 # 

974 regex = "%s.*%s" % (tournament, table_number) 

975 log.info("Party.getTableTitleRe: returns: '%s'" % (regex)) 

976 return regex 

977 

978 

979def renderCards(string): 

980 "Splits strings like ' Js, 4d '" 

981 cards = string.strip().split(" ") 

982 return filter(len, map(lambda x: x.strip(" ,"), cards))