PageRenderTime 59ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Tc2SitePokerStarsHandGrabber.py

http://tablecrab.googlecode.com/
Python | 872 lines | 830 code | 21 blank | 21 comment | 51 complexity | 0cdcc5ad773e916f81146bbba2b50d03 MD5 | raw file
  1. # -*- coding: UTF-8 -*-
  2. #TODO: tons. maybe iso hand to a generic library of poker hand types and work from there
  3. import Tc2Config
  4. from PyQt4 import QtCore
  5. import re, codecs
  6. #***********************************************************************************************
  7. #
  8. #***********************************************************************************************
  9. class Hand(QtCore.QObject):
  10. """dand object"""
  11. StreetNone = 0
  12. StreetBlinds = 1
  13. StreetPreflop = 2
  14. StreetFlop = 3
  15. StreetTurn = 4
  16. StreetRiver = 5
  17. StreetShowdown = 6
  18. StreetSummary = 7
  19. GameTypeNone = 0x0
  20. GameTypeHoldem = 1 << 0
  21. GameTypeOmaha = 1 << 1
  22. GameSubTypeHiLo = 1 << 20
  23. GameLimitNoLimit = 1 << 40
  24. GameLimitPotLimit = 1 << 41
  25. GameLimitLimit = 1 << 42
  26. class Action(object):
  27. TypeNone = 0
  28. TypeBet = 1
  29. TypeCheck = 2
  30. TypeCall = 3
  31. TypeFold = 4
  32. TypeRaise = 5
  33. TypePostBlindAnte = 6
  34. TypePostBlindSmall = 7
  35. TypePostBlindBig = 8
  36. TypePostBuyIn = 9
  37. def __init__(self, player=None, type=TypeNone, amount=0.0):
  38. self.player = player
  39. self.type = type
  40. self.amount = amount
  41. class Player(object):
  42. def __init__(self, name='', stack=0.0, cards=None):
  43. self.name = name
  44. self.stack = stack
  45. self.cards = ['', ''] if cards is None else cards
  46. def __init__(self):
  47. QtCore.QObject.__init__(self)
  48. self.handHistory = ''
  49. self.gameType = self.GameTypeNone
  50. self.numPlayercards = 0
  51. self.seats = [] # len(seats) == maxPlayers. empty seat is set to None
  52. self.cards = ['', '', '', '', '']
  53. self.blindAnte = 0.0
  54. self.blindSmall = 0.0
  55. self.blindBig = 0.0
  56. self.hasCents = True # flag indicating if bet cents is discovered or not
  57. self.seatNoButton = None
  58. self.tableName = ''
  59. self.actions = {
  60. self.StreetBlinds: [],
  61. self.StreetPreflop: [],
  62. self.StreetFlop: [],
  63. self.StreetTurn: [],
  64. self.StreetRiver: [],
  65. }
  66. def calcPotSizes(self):
  67. streets = (self.StreetBlinds, self.StreetPreflop, self.StreetFlop, self.StreetTurn, self.StreetRiver)
  68. result = dict([(street, 0.0) for street in streets])
  69. players = [player for player in self.seats if player is not None]
  70. bets = dict( [(player, 0.0) for player in players])
  71. for street in streets:
  72. for player in players:
  73. actions = [action for action in self.actions[street] if action.player is player]
  74. for action in actions:
  75. amount = action.amount
  76. if action.type == action.TypeRaise:
  77. amount -= bets[player]
  78. bets[player] += amount
  79. result[street] += amount
  80. bets = dict( [(player, 0.0) for player in players] )
  81. result[street] += result[streets[streets.index(street)-1]]
  82. return result
  83. def playerFromName(self, playerName):
  84. for player in self.seats:
  85. if player is None: continue
  86. if player.name == playerName:
  87. return player
  88. return None
  89. def seatsButtonOrdered(self):
  90. """returns seats of hand orderd button player first, excluding empty seats"""
  91. seats = self.seats[self.seatNoButton:] + self.seats[:self.seatNoButton]
  92. return [seat for seat in seats if seat is not None]
  93. def __nonzero__(self):
  94. return bool(self.seats)
  95. #********************************************************************************************************
  96. # hand parser
  97. #********************************************************************************************************
  98. class NoGameHeaderError(Exception): pass
  99. class HandParser(object):
  100. """hand history parser
  101. """
  102. Currencies = u'$â&#x201A;?Â?'
  103. GameTypeMapping= {
  104. "Hold'em No Limit": {
  105. 'gameType': Hand.GameTypeHoldem | Hand.GameLimitNoLimit,
  106. 'numPlayerCards': 2,
  107. },
  108. "Hold'em Pot Limit": {
  109. 'gameType': Hand.GameTypeHoldem | Hand.GameLimitPotLimit,
  110. 'numPlayerCards': 2,
  111. },
  112. "Hold'em Limit": {
  113. 'gameType': Hand.GameTypeHoldem | Hand.GameLimitLimit,
  114. 'numPlayerCards': 2,
  115. },
  116. 'Omaha Limit': {
  117. 'gameType': Hand.GameTypeOmaha | Hand.GameLimitLimit,
  118. 'numPlayerCards': 4,
  119. },
  120. 'Omaha Pot Limit': {
  121. 'gameType': Hand.GameTypeOmaha | Hand.GameLimitPotLimit,
  122. 'numPlayerCards': 4,
  123. },
  124. 'Omaha Hi/Lo Limit': {
  125. 'gameType': Hand.GameTypeOmaha | Hand.GameSubTypeHiLo | Hand.GameLimitLimit,
  126. 'numPlayerCards': 4,
  127. },
  128. 'Omaha Hi/Lo Pot Limit': {
  129. 'gameType': Hand.GameTypeOmaha | Hand.GameSubTypeHiLo | Hand.GameLimitLimit,
  130. 'numPlayerCards': 4,
  131. },
  132. }
  133. def __init__(self,):
  134. """"""
  135. pass
  136. def stringToFloat(self, string):
  137. return float(string.replace(',', ''))
  138. def stringToCards(self, string, zfill=None):
  139. cards = string.split(' ')
  140. if zfill:
  141. cards += ['']*zfill
  142. return cards[:zfill]
  143. return cards
  144. PatGameHeader1 = re.compile('^PokerStars\s (Home\s)? Game\s \#[0-9]+\:\s .*? \s(?P<gameType>%s)\s.*' % '|'.join([re.escape(i).replace('\ ', '\s') for i in GameTypeMapping]), re.X)
  145. PatGameHeader2 = re.compile('^PokerStars\s (Home\sGame\s)? Hand\s \#[0-9]+\:\s .*? \s(?P<gameType>%s)\s.*' % '|'.join([re.escape(i).replace('\ ', '\s') for i in GameTypeMapping]), re.X)
  146. PatGameHeader3 = re.compile('^PokerStars\s Zoom\s Hand\s \#[0-9]+\:\s .*? \s(?P<gameType>%s)\s.*' % '|'.join([re.escape(i).replace('\ ', '\s') for i in GameTypeMapping]), re.X)
  147. def matchGameHeader(self, hand, streetCurrent, line):
  148. result = self.PatGameHeader1.match(line)
  149. if result is None:
  150. result = self.PatGameHeader2.match(line)
  151. if result is None:
  152. result = self.PatGameHeader3.match(line)
  153. if result is not None:
  154. gameType = self.GameTypeMapping[result.group('gameType')]
  155. hand.gameType = gameType['gameType']
  156. hand.numPlayerCards = gameType['numPlayerCards']
  157. return result is not None
  158. #NOTE: in tourneys <tableName> is composed of 'tourneyID tableNo'. no idea if this is of any relevance to us
  159. PatternTableInfo1 = re.compile('^Table \s \' (?P<tableName>.+?) \' \s (?P<maxPlayers>[0-9]+)\-max \s Seat \s \#(?P<seatNoButton>[0-9]+) \s is \s the \s button', re.X)
  160. #NOTE: zoom poker does not indicate button player
  161. PatternTableInfo2 = re.compile('^Table \s \' (?P<tableName>.+?) \' \s (?P<maxPlayers>[0-9]+)\-max', re.X)
  162. def matchTableInfo(self, hand, streetCurrent, line):
  163. result = self.PatternTableInfo1.match(line)
  164. if result is not None:
  165. hand.tableName = result.group('tableName')
  166. hand.seats = [None for i in range( int(result.group('maxPlayers') ))]
  167. hand.seatNoButton = int(result.group('seatNoButton')) -1
  168. else:
  169. result = self.PatternTableInfo2.match(line)
  170. if result is not None:
  171. hand.tableName = result.group('tableName')
  172. hand.seats = [None for i in range( int(result.group('maxPlayers') ))]
  173. return result is not None
  174. PatternSeat = re.compile('^Seat \s(?P<seatNo>[0-9]+)\:\s (?P<player>.*) \s\( [%s]? (?P<stack>[\d\.]+)\s?.* \)' % Currencies, re.X)
  175. def matchSeat(self, hand, streetCurrent, line):
  176. result= self.PatternSeat.match(line)
  177. if result is not None:
  178. player = hand.Player(
  179. name=result.group('player'),
  180. stack=self.stringToFloat(result.group('stack')),
  181. cards=[''] * hand.numPlayerCards,
  182. )
  183. seatNo = int(result.group('seatNo')) -1
  184. hand.seats[seatNo] = player
  185. return result is not None
  186. PatternDealtTo = re.compile('Dealt \s to \s (?P<player>.+?) \s \[ (?P<cards>.+?) \]', re.X)
  187. def matchDealtTo(self, hand, streetCurrent, line):
  188. result = self.PatternDealtTo.match(line)
  189. if result is not None:
  190. hand.playerFromName(result.group('player')).cards = self.stringToCards(result.group('cards'))
  191. return result is not None
  192. #FIXME: determine hand.BlindAnte/BlindSmall/BlindBig from what player posted is quite stupid. have to parse hand header
  193. # instead. but ..dont like parsing this mess + it is broken anyways. ante is not mentioned for cash games. maybe
  194. # stars get their stuff sorted out somedays. gogogogo stars
  195. PatternPostAnte = re.compile('^(?P<player>.*?)\: \s posts \s the \s ante \s [%s]? (?P<amount>[0-9\.\,]+ )' % Currencies, re.X)
  196. def matchPostAnte(self, hand, streetCurrent, line):
  197. result = self.PatternPostAnte.match(line)
  198. if result is not None:
  199. amount = self.stringToFloat(result.group('amount'))
  200. player = hand.playerFromName(result.group('player'))
  201. action = hand.Action(player=player, type=hand.Action.TypePostBlindAnte, amount=amount)
  202. hand.actions[streetCurrent].append(action)
  203. hand.blindAnte = max(amount, hand.blindAnte)
  204. return result is not None
  205. PatternPostSmallBlind = re.compile('^(?P<player>.*?)\: \s posts \s small \s blind \s [%s]? (?P<amount>[0-9\.\,]+)' % Currencies, re.X)
  206. def matchPostSmallBlind(self, hand, streetCurrent, line):
  207. result = self.PatternPostSmallBlind.match(line)
  208. if result is not None:
  209. amount = self.stringToFloat(result.group('amount'))
  210. player = hand.playerFromName(result.group('player'))
  211. action = hand.Action(player=player, type=hand.Action.TypePostBlindSmall, amount=amount)
  212. hand.actions[streetCurrent].append(action)
  213. hand.blindSmall = max(amount, hand.blindSmall)
  214. return result is not None
  215. PatternPostBigBlind = re.compile('^(?P<player>.*?)\: \s posts \s big \s blind \s [%s]? (?P<amount>[0-9\.\,]+ )' % Currencies, re.X)
  216. def matchPostBigBlind(self, hand, streetCurrent, line):
  217. result = self.PatternPostBigBlind.match(line)
  218. if result is not None:
  219. amount = self.stringToFloat(result.group('amount'))
  220. player = hand.playerFromName(result.group('player'))
  221. action = hand.Action(player=player, type=hand.Action.TypePostBlindBig, amount=amount)
  222. hand.actions[streetCurrent].append(action)
  223. hand.blindBig = max(amount, hand.blindBig)
  224. return result is not None
  225. PatternPostBuyIn = re.compile('^(?P<player>.*?)\: \s posts \s small \s & \s big \s blinds \s [%s]? (?P<amount>[0-9\.\,]+ )' % Currencies, re.X)
  226. def matchPostBuyIn(self, hand, streetCurrent, line):
  227. result = self.PatternPostBuyIn.match(line)
  228. if result is not None:
  229. amount = self.stringToFloat(result.group('amount'))
  230. player = hand.playerFromName(result.group('player'))
  231. action = hand.Action(player=player, type=hand.Action.TypePostBuyIn, amount=amount)
  232. hand.actions[streetCurrent].append(action)
  233. hand.blindBig = max(amount, hand.blindBig)
  234. return result is not None
  235. PatternBoardCards = re.compile('^Board \s \[ (?P<cards>.*?) \]', re.X)
  236. def matchBoardCards(self, hand, streetCurrent, line):
  237. result = self.PatternBoardCards.match(line)
  238. if result is not None:
  239. hand.cards = self.stringToCards(result.group('cards'), zfill=5)
  240. return result is not None
  241. PatternShowsCards = re.compile('^(?P<player>.+?)\: \s shows \s\[ (?P<cards>.+?) \]', re.X)
  242. def matchShowsCards(self, hand, streetCurrent, line):
  243. result = self.PatternShowsCards.match(line)
  244. if result is not None:
  245. hand.playerFromName(result.group('player')).cards = self.stringToCards(result.group('cards'))
  246. return result is not None
  247. PatternShowedCards = re.compile('^Seat\s[1-9]+\:\s (?P<player>.+?) \s showed \s\[ (?P<cards>.+?) \]', re.X)
  248. def matchShowedCards(self, hand, streetCurrent, line):
  249. result = self.PatternShowedCards.match(line)
  250. if result is not None:
  251. hand.playerFromName(result.group('player')).cards = self.stringToCards(result.group('cards'))
  252. return result is not None
  253. PatternMuckedCards = re.compile('^Seat\s[1-9]+\:\s (?P<player>.+?) \s mucked \s\[ (?P<cards>.+?) \]', re.X)
  254. def matchMuckedCards(self, hand, streetCurrent, line):
  255. result = self.PatternMuckedCards.match(line)
  256. if result is not None:
  257. hand.playerFromName(result.group('player')).cards = self.stringToCards(result.group('cards'))
  258. return result is not None
  259. PatternCheck = re.compile('^(?P<player>.+?) \:\s checks', re.X)
  260. def matchCheck(self, hand, streetCurrent, line):
  261. result = self.PatternCheck.match(line)
  262. if result is not None:
  263. player = hand.playerFromName(result.group('player'))
  264. action = hand.Action(player=player, type=hand.Action.TypeCheck)
  265. hand.actions[streetCurrent].append(action)
  266. return result is not None
  267. #TODO: test. stars introduced "fold + show", so we may get to see some cards here. have only seen
  268. # show both so far. hope it works for show one as well.
  269. PatternFold = re.compile('^(?P<player>.+?) \:\s folds (\s \[ (?P<cards>.+?) \])', re.X)
  270. def matchFold(self, hand, streetCurrent, line):
  271. result = self.PatternFold.match(line)
  272. if result is not None:
  273. player = hand.playerFromName(result.group('player'))
  274. action = hand.Action(player=player, type=hand.Action.TypeFold)
  275. hand.actions[streetCurrent].append(action)
  276. if result.group('cards'):
  277. hand.playerFromName(result.group('player')).cards = self.stringToCards(result.group('cards'))
  278. return result is not None
  279. PatternCall = re.compile('^(?P<player>.+?) \:\s calls \s [%s]? (?P<amount>[0-9\.\,]+)' % Currencies, re.X)
  280. def matchCall(self, hand, streetCurrent, line):
  281. result = self.PatternCall.match(line)
  282. if result is not None:
  283. player = hand.playerFromName(result.group('player'))
  284. action = hand.Action(player=player, type=hand.Action.TypeCall, amount=self.stringToFloat(result.group('amount')))
  285. hand.actions[streetCurrent].append(action)
  286. return result is not None
  287. PatternBet = re.compile('^(?P<player>.+?) \:\s bets \s [%s]? (?P<amount>[0-9\.\,]+)' % Currencies, re.X)
  288. def matchBet(self, hand, streetCurrent, line):
  289. result = self.PatternBet.match(line)
  290. if result is not None:
  291. player = hand.playerFromName(result.group('player'))
  292. action = hand.Action(player=player, type=hand.Action.TypeBet, amount=self.stringToFloat(result.group('amount')))
  293. hand.actions[streetCurrent].append(action)
  294. return result is not None
  295. PatternRaise = re.compile('^(?P<player>.+?) \:\s raises \s .*?\s to \s [%s]? (?P<amount>[0-9\.\,]+)' % Currencies, re.X)
  296. def matchRaise(self, hand, streetCurrent, line):
  297. result = self.PatternRaise.match(line)
  298. if result is not None:
  299. player = hand.playerFromName(result.group('player'))
  300. action = hand.Action(player=player, type=hand.Action.TypeRaise, amount=self.stringToFloat(result.group('amount')))
  301. hand.actions[streetCurrent].append(action)
  302. return result is not None
  303. def parse(self, handHistory):
  304. """parses a hand history from a string containing exactly one hand history
  305. @param handHistory: (str)
  306. @return: L{Hand} or None if string could not be parsed
  307. """
  308. # create new hand object
  309. hand = Hand()
  310. hand.handHistory = handHistory.strip().replace('\r', '')
  311. streetCurrent = hand.StreetBlinds
  312. # clean handHistory up a bit to make it easier on us..
  313. handHistory = hand.handHistory.replace('(small blind) ', '').replace('(big blind) ', '').replace('(button) ', '').replace('(Play Money) ', '')
  314. for lineno, line in enumerate(handHistory.split('\n')):
  315. line = line.strip()
  316. if not line: continue
  317. if lineno == 0:
  318. if self.matchGameHeader(hand, streetCurrent, line): continue
  319. raise NoGameHeaderError('No gane header found')
  320. # determine street we are in
  321. if line.startswith('*** HOLE CARDS ***'):
  322. streetCurrent = hand.StreetPreflop
  323. continue
  324. elif line.startswith('*** FLOP ***'):
  325. streetCurrent = hand.StreetFlop
  326. continue
  327. elif line.startswith('*** TURN ***'):
  328. streetCurrent = hand.StreetTurn
  329. continue
  330. elif line.startswith('*** RIVER ***'):
  331. streetCurrent = hand.StreetRiver
  332. continue
  333. elif line.startswith('*** SHOWDOWN ***'):
  334. streetCurrent = hand.StreetShowdown
  335. continue
  336. elif line.startswith('*** SUMMARY ***'):
  337. streetCurrent = hand.StreetSummary
  338. continue
  339. # parse streets
  340. if streetCurrent == hand.StreetBlinds:
  341. if self.matchTableInfo(hand, streetCurrent, line): continue
  342. if self.matchSeat(hand, streetCurrent, line): continue
  343. if self.matchPostAnte(hand, streetCurrent,line): continue
  344. if self.matchPostSmallBlind(hand, streetCurrent,line): continue
  345. if self.matchPostBigBlind(hand, streetCurrent,line): continue
  346. if self.matchPostBuyIn(hand, streetCurrent,line): continue
  347. elif streetCurrent == hand.StreetShowdown:
  348. #TODO: just a guess that it is possible to show cards on showdown
  349. if self.matchShowsCards(hand, streetCurrent, line): continue
  350. elif streetCurrent == hand.StreetSummary:
  351. if self.matchBoardCards(hand, streetCurrent, line): continue
  352. if self.matchShowedCards(hand, streetCurrent, line): continue
  353. if self.matchMuckedCards(hand, streetCurrent, line): continue
  354. else:
  355. if streetCurrent == hand.StreetPreflop:
  356. if self.matchDealtTo(hand, streetCurrent, line): continue
  357. if self.matchShowsCards(hand, streetCurrent, line): continue
  358. if self.matchCheck(hand, streetCurrent, line): continue
  359. if self.matchFold(hand, streetCurrent, line): continue
  360. if self.matchCall(hand, streetCurrent, line): continue
  361. if self.matchBet(hand, streetCurrent, line): continue
  362. if self.matchRaise(hand, streetCurrent, line): continue
  363. # postprocess hand
  364. hand.hasCents = (hand.blindAnte, hand.blindSmall, hand.blindBig) != (int(hand.blindAnte), int(hand.blindSmall), int(hand.blindBig))
  365. #NOTE: when introducing zoom poker stars missed indicating button player in
  366. # hand histories. following lines try our best to determine this player
  367. if hand.seatNoButton is None and hand.actions[hand.StreetBlinds]:
  368. players = [player for player in hand.seats if player is not None]
  369. if len(players) < 2:
  370. raise ValueError('too bad, we can not handle this case :-)')
  371. # try to find small blind player
  372. for action in hand.actions[hand.StreetBlinds]:
  373. if action.type == action.TypePostBlindSmall:
  374. i = players.index(action.player)
  375. if len(players) == 2:
  376. hand.seatNoButton = i
  377. else:
  378. player = players[i-1]
  379. hand.seatNoButton = hand.seats.index(player)
  380. break
  381. if hand.seatNoButton is None:
  382. # try to find big blind player
  383. for action in hand.actions[hand.StreetBlinds]:
  384. if action.type == action.TypePostBlindBig:
  385. i = players.index(action.player)
  386. if len(players) == 2:
  387. hand.seatNoButton = i-1
  388. else:
  389. player = players[i-2]
  390. hand.seatNoButton = hand.seats.index(player)
  391. break
  392. if hand.seatNoButton is None:
  393. # out of luck, hand may be ante only so we set random plyer as button
  394. hand.seatNoButton = hand.seats.index[players[0]]
  395. # errorcheck
  396. if hand.seatNoButton is None:
  397. raise ValueError('Could not determine button player')
  398. return hand
  399. #********************************************************************************************************
  400. # hand formatters
  401. #********************************************************************************************************
  402. HtmlCardSuitMapping = { # suit --> (entity, htmlKlass)
  403. 's': ('&spades;', 'cardSuitSpade'),
  404. 'c': ('&clubs;', 'cardSuitClub'),
  405. 'd': ('&diams;', 'cardSuitDiamond'),
  406. 'h': ('&hearts;', 'cardSuitHeart'),
  407. }
  408. HandFormatters = {}
  409. class HandFormatterMeta(type):
  410. """meta class for hand formatters"""
  411. def __new__(klass, name, bases, kws):
  412. newKlass = type.__new__(klass, name, bases, kws)
  413. if newKlass.Name is not None:
  414. HandFormatters[newKlass.Name] = newKlass
  415. return newKlass
  416. class HandFormatterBase(object):
  417. """base class for hand formatters"""
  418. __metaclass__ = HandFormatterMeta
  419. Name = None
  420. class HandFormatterHtmlTabular(HandFormatterBase):
  421. """Hand formatter that formats a hand as a tabular html"""
  422. Name = 'HtmlTabular'
  423. PrefixBet = 'b'
  424. PostfixBet = ''
  425. PrefixRaise = 'r'
  426. PostfixRaise = ''
  427. PrefixCall = 'c'
  428. PostfixCall = ''
  429. PrefixAnte = ''
  430. PostfixAnte = ''
  431. PrefixSmallBlind = 'sb'
  432. PostfixSmallBlind = ''
  433. PrefixBigBlind = 'bb'
  434. PostfixBigBlind = ''
  435. PrefixBuyIn = 'bi'
  436. PostfixBuyIn = ''
  437. PrefixCheck = 'ck'
  438. PrefixFold = 'f'
  439. MaxPlayerName = -1
  440. # and this is the css for the html file
  441. StyleSheet = '''.handBody{}
  442. .handTable{
  443. border-spacing: 0px;
  444. border-collapse: collapse;
  445. }
  446. .handSource{margin-top: 1em;}
  447. .playerCell{
  448. vertical-align: top;
  449. border: 1px solid black;
  450. }
  451. .playerName{}
  452. .playerStack{}
  453. .playerCardsCell{border: 1px solid black;}
  454. .playerCards{
  455. border: 0px;
  456. border-spacing: 0px;
  457. width: 100%;
  458. }
  459. .playerActionsCell{
  460. white-space: nowrap;
  461. vertical-align: top;
  462. padding-left: 0.1em;
  463. border: 1px solid black;
  464. }
  465. .playerActionFold{}
  466. .playerActionCall{background-color: #87CEFA ;}
  467. .playerActionCheck{background-color: #98FB98;}
  468. .playerActionBet{background-color: #FFA54F;}
  469. .playerActionRaise{background-color: #FF6EB4;}
  470. .playerActionPostBlindBig{}
  471. .playerActionPostBlindSmall{}
  472. .playerActionPostBuyIn{}
  473. .potCellExtra{
  474. padding-left: 1em;
  475. border: 1px solid black;
  476. }
  477. .potCell{
  478. text-align: center;
  479. border: 1px solid black;
  480. }
  481. .boardCardCellExtra{border: 1px solid black; }
  482. .boardCardCell{
  483. border:1px solid black;
  484. margin-left: auto; /* centers contents of the cell */
  485. margin-right: auto; /* centers contents of the cell */
  486. }
  487. .boardCards{
  488. border: 0px;
  489. border-spacing: 0px;
  490. margin-left: auto; /* centers contents of the cell */
  491. margin-right: auto; /* centers contents of the cell */
  492. }
  493. .cardCell{padding: 0px;}
  494. .card{
  495. border: solid 1px;
  496. background-color: red;
  497. border-spacing: 0px;
  498. margin-left: auto; /* centers contents of the cell */
  499. margin-right: auto; /* centers contents of the cell */
  500. }
  501. .cardShape{
  502. padding: 0px 0px 0px 0px;
  503. background-color: white;
  504. }
  505. .cardSuit{
  506. text-indent:0.3em;
  507. padding: 0px 0px 0px 0px;
  508. background-color: white;
  509. }
  510. .cardSuitSpade{color: black;}
  511. .cardSuitClub{color: black;}
  512. .cardSuitHeart{color: red;}
  513. .cardSuitDiamond{color: red;}
  514. .cardBack{
  515. color: #355b73;
  516. background-color: #355b73;
  517. }
  518. '''
  519. class IndentBuffer(object):
  520. """write buffer with indentation support"""
  521. def __init__(self, indent='\x20\x20\x20\x20'):
  522. """
  523. @param indent: (str) chars to use for indentation
  524. @ivar indentLevel: (int) current level of indentation
  525. @ivar indent: (str) chars to use for indentation
  526. @ivar: data: (str) data contained in the buffer
  527. """
  528. self.indentLevel = 0
  529. self.indent = indent
  530. self.data = ''
  531. def __rshift__(self, chars):
  532. """add chars to the buffer and increases indentLevel for all following adds
  533. """
  534. self.data += (self.indent*self.indentLevel) + chars + '\n'
  535. self.indentLevel += 1
  536. def __lshift__(self, chars):
  537. """decreases indentLevel and add chars to the buffer
  538. """
  539. self.indentLevel -= 1
  540. if self.indentLevel < 0: raise ValueError('indent level exceeded')
  541. self.data += (self.indent*self.indentLevel) + chars + '\n'
  542. def __or__(self, chars):
  543. """adds chars to the buffer without altering the current indentLevel"""
  544. self.data += (self.indent*self.indentLevel) + chars + '\n'
  545. def __init__(self):
  546. self.settingsHandViewer = None
  547. self.settingsHandViewerStyleSheet = None
  548. Tc2Config.globalObject.initSettingsFinished.connect(self.onGlobalObjectInitSettingsFinished)
  549. def onGlobalObjectInitSettingsFinished(self, globalObject):
  550. self.settingsHandViewer = globalObject.settingsHandViewer
  551. self.settingsHandViewerStyleSheet = globalObject.settingsHandViewerStyleSheet
  552. def formatNum(self, hand, num):
  553. if not num:
  554. result = ''
  555. elif hand.hasCents:
  556. if self.settingsHandViewer.noFloatingPoint():
  557. result = Tc2Config.locale.toString( int(num*100) )
  558. else:
  559. result = Tc2Config.locale.toString(num, 'f', 2)
  560. else:
  561. result = Tc2Config.locale.toString( int(num))
  562. return result
  563. def htmlEscapeString(self, string, spaces=True):
  564. string = string.replace('&', '&#38;').replace('"', '&#34;').replace("'", '&#39;').replace('<', '&#60;').replace('>', '&#62;')
  565. if spaces:
  566. string = string.replace(' ', '&nbsp;')
  567. return string
  568. def htmlFormatCards(self, p, cardsType, *cards):
  569. if cardsType == 'playerCards':
  570. p >> '<table class="playerCards">'
  571. elif cardsType == 'boardCards':
  572. p >> '<table class="boardCards">'
  573. else:
  574. raise ValueError('unknown cardsType: %s' % cardsType)
  575. p >> '<tr>'
  576. for card in cards:
  577. if not card:
  578. #TODO: == card back. dont realy know what to return in this case - i am not not a Css guru.
  579. shape = '&nbsp;'
  580. htmlSuit = '&nbsp;&nbsp;'
  581. htmlKlass = 'cardBack'
  582. else:
  583. shape = card[0]
  584. htmlSuit, htmlKlass = HtmlCardSuitMapping[card[1]]
  585. p >> '<td class="cardCell">'
  586. #p >> '<div class="card">'
  587. p | '<div class="card"><div class="cardShape %s">%s</div><div class="cardSuit %s">%s</div></div>' % (htmlKlass, shape, htmlKlass, htmlSuit)
  588. #p << '</div>'
  589. p << '</td>'
  590. p << '</tr>'
  591. p << '</table>'
  592. #TODO: use Tc2Config.truncateString()
  593. def formatPlayerName(self, playerName):
  594. maxPlayerName = self.settingsHandViewer.maxPlayerName()
  595. if maxPlayerName > -1 and len(playerName) > maxPlayerName:
  596. if maxPlayerName <= 0:
  597. playerName = ''
  598. elif maxPlayerName == 1:
  599. playerName = '.'
  600. else:
  601. playerName = playerName[:maxPlayerName-2] + '..'
  602. return self.htmlEscapeString(playerName, spaces=True)
  603. def dump(self, hand):
  604. p = self.IndentBuffer()
  605. # setup html page
  606. p >> '<html>'
  607. p >> '<head>'
  608. p | '<meta name="author" content="TableCrab">'
  609. p | '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
  610. p | '<style type="text/css"><!-- %s --></style>' % self.settingsHandViewerStyleSheet.styleSheet()
  611. p << '</head>'
  612. p >> '<body class="handBody">'
  613. p >> '<table class="handTable">'
  614. for player in hand.seats:
  615. if player is None: continue
  616. p >> '<tr>'
  617. # add player summary column
  618. p >> '<td class="playerCell">'
  619. p | '<div class="playerName">%s</div><div class="playerStack">%s</div>' % (
  620. self.formatPlayerName(player.name),
  621. self.formatNum(hand, player.stack)
  622. )
  623. p << '</td>'
  624. # add pocket cards column
  625. p >> '<td class="playerCardsCell">'
  626. self.htmlFormatCards(p, 'playerCards', *player.cards)
  627. p << '</td>'
  628. # add player actions
  629. for street in (hand.StreetBlinds, hand.StreetPreflop, hand.StreetFlop, hand.StreetTurn, hand.StreetRiver):
  630. actions = [action for action in hand.actions[street] if action.player is player]
  631. p >> '<td class="playerActionsCell">'
  632. nActions = None
  633. for nActions, action in enumerate(actions):
  634. if action.type == action.TypeFold:
  635. p | '<div class="playerActionFold">%s</div>' % self.settingsHandViewer.actionPrefix('Fold')
  636. elif action.type == action.TypeCheck:
  637. p | '<div class="playerActionCheck">%s</div>' % self.settingsHandViewer.actionPrefix('Check')
  638. elif action.type == action.TypeBet:
  639. p | '<div class="playerActionBet">%s%s%s</div>' % (
  640. self.settingsHandViewer.actionPrefix('Bet'),
  641. self.formatNum(hand, action.amount),
  642. self.settingsHandViewer.actionPostfix('Bet')
  643. )
  644. elif action.type == action.TypeRaise:
  645. p | '<div class="playerActionRaise">%s%s%s</div>' % (
  646. self.settingsHandViewer.actionPrefix('Raise'),
  647. self.formatNum(hand, action.amount),
  648. self.settingsHandViewer.actionPostfix('Raise')
  649. )
  650. elif action.type == action.TypeCall:
  651. p | '<div class="playerActionCall">%s%s%s</div>' % (
  652. self.settingsHandViewer.actionPrefix('Call'),
  653. self.formatNum(hand, action.amount),
  654. self.settingsHandViewer.actionPostfix('Call')
  655. )
  656. elif action.type == action.TypePostBlindBig:
  657. p | '<div class="playerActionPostBlindBig">%s%s%s</div>' % (
  658. self.settingsHandViewer.actionPrefix('BigBlind'),
  659. self.formatNum(hand, action.amount),
  660. self.settingsHandViewer.actionPostfix('BigBlind')
  661. )
  662. elif action.type == action.TypePostBlindSmall:
  663. p | '<div class="playerActionPostBlindSmall">%s%s%s</div>' % (
  664. self.settingsHandViewer.actionPrefix('SmallBlind'),
  665. self.formatNum(hand, action.amount),
  666. self.settingsHandViewer.actionPostfix('SmallBlind')
  667. )
  668. elif action.type == action.TypePostBuyIn:
  669. p | '<div class="playerActionPostBuyIn">%s%s%s</div>' % (
  670. self.settingsHandViewer.actionPrefix('BuyIn'),
  671. self.formatNum(hand, action.amount),
  672. self.settingsHandViewer.actionPostfix('BuyIn')
  673. )
  674. if nActions is None:
  675. p | '&nbsp;'
  676. p << '</td>'
  677. p << '</tr>'
  678. # add pot size
  679. p >> '<tr>'
  680. pot = hand.calcPotSizes()
  681. #TODO: to save some space we don't display ante for individual player. good idea or not?
  682. potCellExtra = (
  683. self.settingsHandViewer.actionPrefix('Ante') +
  684. self.formatNum(hand, hand.blindAnte) +
  685. self.settingsHandViewer.actionPostfix('Ante')
  686. ) if hand.blindAnte else '&nbsp;'
  687. p | '<td colspan="2" class="potCellExtra">%s</td>' % potCellExtra
  688. p | '<td class="potCell">%s</td>' % self.formatNum(hand, pot[hand.StreetBlinds])
  689. p | '<td class="potCell">%s</td>' % self.formatNum(hand, pot[hand.StreetPreflop])
  690. p | '<td class="potCell">%s</td>' % (self.formatNum(hand, pot[hand.StreetFlop]) if hand.cards[2] else '&nbsp;')
  691. p | '<td class="potCell">%s</td>' % (self.formatNum(hand, pot[hand.StreetTurn]) if hand.cards[3] else '&nbsp;')
  692. p | '<td class="potCell">%s</td>' % (self.formatNum(hand, pot[hand.StreetRiver]) if hand.cards[4] else '&nbsp;')
  693. p << '</tr>'
  694. # add board cards + hand history source
  695. p >> '<tr>'
  696. p | '<td class="boardCardCellExtra" colspan="4">&nbsp;</td>'
  697. p >> '<td class="boardCardCell">'
  698. self.htmlFormatCards(p, 'boardCards', hand.cards[0], hand.cards[1], hand.cards[2])
  699. p << '</td>'
  700. p >> '<td class="boardCardCell">'
  701. self.htmlFormatCards(p, 'boardCards', hand.cards[3])
  702. p << '</td>'
  703. p >> '<td class="boardCardCell">'
  704. self.htmlFormatCards(p, 'boardCards', hand.cards[4])
  705. p << '</td>'
  706. p << '</tr>'
  707. # dump html to file
  708. p << '</table>'
  709. p | '<pre class="handSource">%s</pre>' % self.htmlEscapeString(hand.handHistory, spaces=False)
  710. p << '</body>'
  711. p << '</html>'
  712. return p.data.encode('utf-8')
  713. PatternHand = re.compile('.*\<pre \s+ class=\"HandSource\"\>(.+)\<\/pre\>', re.X|re.I|re.M|re.S)
  714. def handFromHtml(self, html):
  715. hand = Hand()
  716. result = self.PatternHand.search(html)
  717. if result is not None:
  718. handHistory = result.group(1)
  719. parser = HandParser()
  720. try:
  721. hand = parser.parse(handHistory)
  722. except NoGameHeaderError:
  723. pass
  724. return hand
  725. #************************************************************************************
  726. #
  727. #************************************************************************************
  728. class HandHistoryFile(object):
  729. def __init__(self, filePath):
  730. self.filePath = filePath
  731. self._handHistories = []
  732. self._data = None
  733. self._parse()
  734. def _parse(self):
  735. self._data = self._readFileData(self.filePath)
  736. handHistory = None
  737. #TODO: we could do a replace('\r', '\n') here
  738. for line in self._data.split('\n'):
  739. line = line.strip()
  740. if line.startswith('PokerStars Game #') or \
  741. line.startswith('PokerStars Home Game #') or \
  742. line.startswith('PokerStars Hand #') or \
  743. line.startswith('PokerStars Home Game Hand #') or \
  744. line.startswith('PokerStars Zoom Hand #'):
  745. handHistory = [line, ]
  746. continue
  747. elif handHistory and line:
  748. handHistory.append(line)
  749. elif handHistory and not line:
  750. p = '\n'.join(handHistory)
  751. self._handHistories.append(p)
  752. handHistory = None
  753. if handHistory:
  754. p = '\n'.join(handHistory)
  755. self._handHistories.append(p)
  756. def _readFileData(self, filePath):
  757. # stars seems to have switched to utf-8 at some time. more or less a guess
  758. # that it used to be iso-8859-1 before.
  759. fp = codecs.open(self.filePath, encoding='utf-8')
  760. try:
  761. data = fp.read()
  762. #NOTE: remove BOM if present
  763. if data.startswith(unicode(codecs.BOM_UTF8, 'utf-8')):
  764. data = data[1:]
  765. return data
  766. except UnicodeDecodeError: pass
  767. finally:
  768. fp.close()
  769. fp = codecs.open(self.filePath, encoding='iso-8859-1')
  770. try:
  771. return fp.read()
  772. finally:
  773. fp.close()
  774. def __len__(self): return len(self._handHistories)
  775. def __getitem__(self, i): return self._handHistories[i]
  776. def __iter__(self): return iter(self._handHistories)
  777. def raw(self): return self._data