Coverage for Hand.py: 9%
1412 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-18 00:10 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-18 00:10 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
4# Copyright 2008-2011 Carl Gherardi
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as published by
7# the Free Software Foundation, version 3 of the License.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16# In the "official" distribution you can find the license in agpl-3.0.txt.
17from __future__ import print_function
20# import L10n
21# _ = L10n.get_translation()
23# TODO: get writehand() encoding correct
25import sys
26from decimal import Decimal
27import datetime
29import pprint
31import logging
34import Configuration
35from Exceptions import FpdbHandDuplicate, FpdbHandPartial, FpdbParseError
36import DerivedStats
37import Card
39Configuration.set_logfile("fpdb-log.txt")
41# logging has been set up in fpdb.py or HUD_main.py, use their settings:
42log = logging.getLogger("hand")
45class Hand(object):
46 UPS = {"a": "A", "t": "T", "j": "J", "q": "Q", "k": "K", "S": "s", "C": "c", "H": "h", "D": "d"}
47 LCS = {"H": "h", "D": "d", "C": "c", "S": "s"}
48 SYMBOL = {
49 "USD": "$",
50 "CAD": "C$",
51 "EUR": "€",
52 "GBP": "£",
53 "SEK": "kr.",
54 "RSD": "РСД",
55 "mBTC": "ⓑ",
56 "INR": "₹",
57 "CNY": "¥",
58 "T$": "",
59 "play": "",
60 }
61 MS = {"horse": "HORSE", "8game": "8-Game", "hose": "HOSE", "ha": "HA"}
62 ACTION = {
63 "ante": 1,
64 "small blind": 2,
65 "secondsb": 3,
66 "big blind": 4,
67 "both": 5,
68 "calls": 6,
69 "raises": 7,
70 "bets": 8,
71 "stands pat": 9,
72 "folds": 10,
73 "checks": 11,
74 "discards": 12,
75 "bringin": 13,
76 "completes": 14,
77 "straddle": 15,
78 "button blind": 16,
79 "cashout": 17,
80 }
82 def __init__(self, config, sitename, gametype, handText, builtFrom="HHC"):
83 self.config = config
84 self.saveActions = self.config.get_import_parameters().get("saveActions")
85 self.callHud = self.config.get_import_parameters().get("callFpdbHud")
86 self.cacheSessions = self.config.get_import_parameters().get("cacheSessions")
87 self.publicDB = self.config.get_import_parameters().get("publicDB")
88 self.sitename = sitename
89 self.siteId = self.config.get_site_id(sitename)
90 self.stats = DerivedStats.DerivedStats()
91 self.gametype = gametype
92 self.startTime = 0
93 self.handText = handText
94 self.handid = 0
95 self.in_path = None
96 self.cancelled = False
97 self.dbid_hands = 0
98 self.dbid_pids = None
99 self.dbid_hpid = None
100 self.dbid_gt = 0
101 self.tablename = ""
102 self.hero = ""
103 self.maxseats = None
104 self.counted_seats = 0
105 self.buttonpos = 0
106 self.runItTimes = 0
107 self.uncalledbets = False
108 self.checkForUncalled = False
109 self.adjustCollected = False
110 self.cashedOut = False
112 # tourney stuff
113 self.tourNo = None
114 self.tourneyId = None
115 self.tourneyName = None
116 self.tourneyTypeId = None
117 self.buyin = None
118 self.buyinCurrency = None
119 self.buyInChips = None
120 self.fee = None # the Database code is looking for this one .. ?
121 self.level = None
122 self.mixed = None
123 self.speed = "Normal"
124 self.isSng = False
125 self.isRebuy = False
126 self.rebuyCost = 0
127 self.isAddOn = False
128 self.addOnCost = 0
129 self.isKO = False
130 self.koBounty = 0
131 self.isProgressive = False
132 self.isMatrix = False
133 self.isShootout = False
134 self.isFast = False
135 self.stack = "Regular"
136 self.isStep = False
137 self.stepNo = 0
138 self.isChance = False
139 self.chanceCount = 0
140 self.isMultiEntry = False
141 self.isReEntry = False
142 self.isNewToGame = False
143 self.isHomeGame = False
144 self.isSplit = False
145 self.isFifty50 = False
146 self.isTime = False
147 self.timeAmt = 0
148 self.isSatellite = False
149 self.isDoubleOrNothing = False
150 self.isCashOut = False
151 self.isOnDemand = False
152 self.isFlighted = False
153 self.isGuarantee = False
154 self.guaranteeAmt = 0
155 self.added = None
156 self.addedCurrency = None
157 self.entryId = 1
159 self.seating = []
160 self.players = []
161 # Cache used for checkPlayerExists.
162 self.player_exists_cache = set()
163 self.posted = []
164 self.tourneysPlayersIds = {}
166 # Collections indexed by street names
167 self.bets = {}
168 self.lastBet = {}
169 self.streets = {}
170 self.actions = {} # [['mct','bets','$10'],['mika','folds'],['carlg','raises','$20']]
171 self.board = {} # dict from street names to community cards
172 self.holecards = {}
173 self.discards = {}
174 self.showdownStrings = {}
175 for street in self.allStreets:
176 self.streets[street] = "" # portions of the handText, filled by markStreets()
177 self.actions[street] = []
178 for street in self.actionStreets:
179 self.bets[street] = {}
180 self.lastBet[street] = 0
181 self.board[street] = []
182 for street in self.holeStreets:
183 self.holecards[street] = {} # dict from player names to holecards
184 for street in self.discardStreets:
185 self.discards[
186 street
187 ] = {} # dict from player names to dicts by street ... of tuples ... of discarded holecards
188 # Collections indexed by player names
189 self.rakes = {}
190 self.stacks = {}
191 self.collected = [] # list of ?
192 self.collectees = {} # dict from player names to amounts collected (?)
193 self.koCounts = {}
194 self.endBounty = {}
196 # Sets of players
197 self.folded = set()
198 self.dealt = set() # 'dealt to' line to be printed
199 self.shown = set() # cards were shown
200 self.mucked = set() # cards were mucked at showdown
201 self.sitout = set() # players sitting out or not dealt in (usually tournament)
203 # Things to do with money
204 self.pot = Pot()
205 self.totalpot = None
206 self.totalcollected = None
207 self.rake = None
208 self.roundPenny = False
209 self.fastFold = False
210 # currency symbol for this hand
211 self.sym = self.SYMBOL[self.gametype["currency"]] # save typing! delete this attr when done
212 self.pot.setSym(self.sym)
213 self.is_duplicate = False # i.e. don't update hudcache if true
215 def __str__(self):
216 vars = (
217 (("BB"), self.bb),
218 (("SB"), self.sb),
219 (("BUTTON POS"), self.buttonpos),
220 (("HAND NO."), self.handid),
221 (("SITE"), self.sitename),
222 (("TABLE NAME"), self.tablename),
223 (("HERO"), self.hero),
224 (("MAX SEATS"), self.maxseats),
225 (("LEVEL"), self.level),
226 (("MIXED"), self.mixed),
227 (("LAST BET"), self.lastBet),
228 (("ACTION STREETS"), self.actionStreets),
229 (("STREETS"), self.streets),
230 (("ALL STREETS"), self.allStreets),
231 (("COMMUNITY STREETS"), self.communityStreets),
232 (("HOLE STREETS"), self.holeStreets),
233 (("COUNTED SEATS"), self.counted_seats),
234 (("DEALT"), self.dealt),
235 (("SHOWN"), self.shown),
236 (("MUCKED"), self.mucked),
237 (("TOTAL POT"), self.totalpot),
238 (("TOTAL COLLECTED"), self.totalcollected),
239 (("RAKE"), self.rake),
240 (("START TIME"), self.startTime),
241 (("TOURNAMENT NO"), self.tourNo),
242 (("TOURNEY ID"), self.tourneyId),
243 (("TOURNEY TYPE ID"), self.tourneyTypeId),
244 (("BUYIN"), self.buyin),
245 (("BUYIN CURRENCY"), self.buyinCurrency),
246 (("BUYIN CHIPS"), self.buyInChips),
247 (("FEE"), self.fee),
248 (("IS REBUY"), self.isRebuy),
249 (("IS ADDON"), self.isAddOn),
250 (("IS KO"), self.isKO),
251 (("KO BOUNTY"), self.koBounty),
252 (("IS MATRIX"), self.isMatrix),
253 (("IS SHOOTOUT"), self.isShootout),
254 )
256 structs = (
257 (("PLAYERS"), self.players),
258 (("STACKS"), self.stacks),
259 (("POSTED"), self.posted),
260 (("POT"), self.pot),
261 (("SEATING"), self.seating),
262 (("GAMETYPE"), self.gametype),
263 (("ACTION"), self.actions),
264 (("COLLECTEES"), self.collectees),
265 (("BETS"), self.bets),
266 (("BOARD"), self.board),
267 (("DISCARDS"), self.discards),
268 (("HOLECARDS"), self.holecards),
269 (("TOURNEYS PLAYER IDS"), self.tourneysPlayersIds),
270 )
271 result = ""
272 for name, var in vars:
273 result = result + "\n%s = " % name + pprint.pformat(var)
275 for name, struct in structs:
276 result = result + "\n%s =\n" % name + pprint.pformat(struct, 4)
277 return result
279 def addHoleCards(self, street, player, open=[], closed=[], shown=False, mucked=False, dealt=False):
280 """Assigns observed holecards to a player.
281 cards list of card bigrams e.g. ['2h','Jc']
282 player (string) name of player
283 shown whether they were revealed at showdown
284 mucked whether they were mucked at showdown
285 dealt whether they were seen in a 'dealt to' line"""
286 log.debug(
287 "Hand.addHoleCards open+closed: %s, player: %s, shown: %s, mucked: %s, dealt: %s",
288 open + closed,
289 player,
290 shown,
291 mucked,
292 dealt,
293 )
294 self.checkPlayerExists(player, "addHoleCards")
296 if dealt:
297 self.dealt.add(player)
298 if shown:
299 self.shown.add(player)
300 if mucked:
301 self.mucked.add(player)
303 for i in range(len(closed)):
304 if closed[i] in ("", "Xx", "Null", "null", "X"):
305 closed[i] = "0x"
307 try:
308 self.holecards[street][player] = [open, closed]
309 except KeyError as e:
310 log.error(("Hand.addHoleCards: '%s': Major failure while adding holecards: '%s'"), self.handid, e)
311 raise FpdbParseError
313 def prepInsert(self, db, printtest=False):
314 #####
315 # Players, Gametypes, TourneyTypes are all shared functions that are needed for additional tables
316 # These functions are intended for prep insert eventually
317 #####
318 if self.gametype.get("maxSeats") is None:
319 self.gametype["maxSeats"] = self.maxseats # TODO: move up to individual parsers
320 else:
321 self.maxseats = self.gametype["maxSeats"]
322 self.dbid_pids = db.getSqlPlayerIDs([p[1] for p in self.players], self.siteId, self.hero)
323 self.dbid_gt = db.getSqlGameTypeId(self.siteId, self.gametype, printdata=printtest)
325 # Gametypes
326 hilo = Card.games[self.gametype["category"]][2]
328 self.gametyperow = (
329 self.siteId,
330 self.gametype["currency"],
331 self.gametype["type"],
332 self.gametype["base"],
333 self.gametype["category"],
334 self.gametype["limitType"],
335 hilo,
336 self.gametype["mix"],
337 int(Decimal(self.gametype["sb"]) * 100),
338 int(Decimal(self.gametype["bb"]) * 100),
339 int(Decimal(self.gametype["bb"]) * 100),
340 int(Decimal(self.gametype["bb"]) * 200),
341 int(self.gametype["maxSeats"]),
342 int(self.gametype["ante"] * 100),
343 self.gametype["buyinType"],
344 self.gametype["fast"],
345 self.gametype["newToGame"],
346 self.gametype["homeGame"],
347 self.gametype["split"],
348 )
349 # Note: the above data is calculated in db.getGameTypeId
350 # Only being calculated above so we can grab the testdata
351 if self.tourNo is not None:
352 self.tourneyTypeId = db.getSqlTourneyTypeIDs(self)
353 self.tourneyId = db.getSqlTourneyIDs(self)
354 self.tourneysPlayersIds = db.getSqlTourneysPlayersIDs(self)
356 def assembleHand(self):
357 self.stats.getStats(self)
358 self.hands = self.stats.getHands()
359 self.handsplayers = self.stats.getHandsPlayers()
360 self.handsactions = self.stats.getHandsActions()
361 self.handsstove = self.stats.getHandsStove()
362 self.handspots = self.stats.getHandsPots()
364 def getHandId(self, db, id):
365 if db.isDuplicate(self.siteId, self.hands["siteHandNo"], self.hands["heroSeat"], self.publicDB):
366 # log.debug(("Hand.insert(): hid #: %s is a duplicate") % self.hands['siteHandNo'])
367 self.is_duplicate = True # i.e. don't update hudcache
368 next = id
369 if self.publicDB:
370 raise FpdbHandDuplicate(
371 "%s-%s-%s" % (str(self.siteId), str(self.hands["siteHandNo"]), self.hands["heroSeat"])
372 )
373 else:
374 raise FpdbHandDuplicate("%s-%s" % (str(self.siteId), str(self.hands["siteHandNo"])))
375 else:
376 self.dbid_hands = id
377 self.hands["id"] = self.dbid_hands
378 next = id + db.hand_inc
379 return next
381 def insertHands(self, db, fileId, doinsert=False, printtest=False):
382 """Function to insert Hand into database
383 Should not commit, and do minimal selects. Callers may want to cache commits
384 db: a connected Database object"""
385 self.hands["gametypeId"] = self.dbid_gt
386 self.hands["seats"] = len(self.dbid_pids)
387 self.hands["fileId"] = fileId
388 db.storeHand(self.hands, doinsert, printtest)
389 db.storeBoards(self.dbid_hands, self.hands["boards"], doinsert)
391 def insertHandsPlayers(self, db, doinsert=False, printtest=False):
392 """Function to inserts HandsPlayers into database"""
393 db.storeHandsPlayers(self.dbid_hands, self.dbid_pids, self.handsplayers, doinsert, printtest)
394 if self.handspots:
395 self.handspots.sort(key=lambda x: x[1])
396 for ht in self.handspots:
397 ht[0] = self.dbid_hands
398 db.storeHandsPots(self.handspots, doinsert)
400 def insertHandsActions(self, db, doinsert=False, printtest=False):
401 """Function to inserts HandsActions into database"""
402 if self.saveActions:
403 db.storeHandsActions(self.dbid_hands, self.dbid_pids, self.handsactions, doinsert, printtest)
405 def insertHandsStove(self, db, doinsert=False):
406 """Function to inserts HandsStove into database"""
407 if self.handsstove:
408 for hs in self.handsstove:
409 hs[0] = self.dbid_hands
410 db.storeHandsStove(self.handsstove, doinsert)
412 def updateTourneyResults(self, db):
413 """Function to update Tourney Bounties if any"""
414 db.updateTourneyPlayerBounties(self)
416 def updateHudCache(self, db, doinsert=False):
417 """Function to update the HudCache"""
418 if self.callHud:
419 db.storeHudCache(self.dbid_gt, self.gametype, self.dbid_pids, self.startTime, self.handsplayers, doinsert)
421 def updateSessionsCache(self, db, tz, doinsert=False):
422 """Function to update the Sessions"""
423 if self.cacheSessions:
424 heroes = [list(self.dbid_pids.values())[0]]
425 db.storeSessions(self.dbid_hands, self.dbid_pids, self.startTime, self.tourneyId, heroes, tz, doinsert)
426 db.storeSessionsCache(
427 self.dbid_hands,
428 self.dbid_pids,
429 self.startTime,
430 self.dbid_gt,
431 self.gametype,
432 self.handsplayers,
433 heroes,
434 doinsert,
435 )
436 db.storeTourneysCache(
437 self.dbid_hands,
438 self.dbid_pids,
439 self.startTime,
440 self.tourneyId,
441 self.gametype,
442 self.handsplayers,
443 heroes,
444 doinsert,
445 )
447 def updateCardsCache(self, db, tz, doinsert=False):
448 """Function to update the CardsCache"""
449 if self.cacheSessions: # and self.hero in self.dbid_pids:
450 heroes = [list(self.dbid_pids.values())[0]]
451 db.storeCardsCache(
452 self.dbid_hands,
453 self.dbid_pids,
454 self.startTime,
455 self.dbid_gt,
456 self.tourneyTypeId,
457 self.handsplayers,
458 heroes,
459 tz,
460 doinsert,
461 )
463 def updatePositionsCache(self, db, tz, doinsert=False):
464 """Function to update the PositionsCache"""
465 if self.cacheSessions: # and self.hero in self.dbid_pids:
466 heroes = [list(self.dbid_pids.values())[0]]
467 db.storePositionsCache(
468 self.dbid_hands,
469 self.dbid_pids,
470 self.startTime,
471 self.dbid_gt,
472 self.tourneyTypeId,
473 self.handsplayers,
474 self.hands,
475 heroes,
476 tz,
477 doinsert,
478 )
480 def select(self, db, handId):
481 """Function to create Hand object from database"""
482 c = db.get_cursor()
483 q = db.sql.query["playerHand"]
484 q = q.replace("%s", db.sql.query["placeholder"])
486 c.execute("select heroseat from Hands where id = {}".format(handId))
487 heroSeat = c.fetchone()[0]
489 # PlayerStacks
490 c.execute(q, (handId,))
491 # See NOTE: below on what this does.
493 # Discripter must be set to lowercase as postgres returns all descriptors lower case and SQLight returns them as they are
494 res = [
495 dict(line)
496 for line in [list(zip([column[0].lower() for column in c.description], row)) for row in c.fetchall()]
497 ]
498 for row in res:
499 self.addPlayer(
500 row["seatno"], row["name"], str(row["chips"]), str(row["position"]), row["sitout"], str(row["bounty"])
501 )
502 cardlist = []
503 cardlist.append(Card.valueSuitFromCard(row["card1"]))
504 cardlist.append(Card.valueSuitFromCard(row["card2"]))
505 cardlist.append(Card.valueSuitFromCard(row["card3"]))
506 cardlist.append(Card.valueSuitFromCard(row["card4"]))
507 cardlist.append(Card.valueSuitFromCard(row["card5"]))
508 cardlist.append(Card.valueSuitFromCard(row["card6"]))
509 cardlist.append(Card.valueSuitFromCard(row["card7"]))
510 cardlist.append(Card.valueSuitFromCard(row["card8"]))
511 cardlist.append(Card.valueSuitFromCard(row["card9"]))
512 cardlist.append(Card.valueSuitFromCard(row["card10"]))
513 cardlist.append(Card.valueSuitFromCard(row["card11"]))
514 cardlist.append(Card.valueSuitFromCard(row["card12"]))
515 cardlist.append(Card.valueSuitFromCard(row["card13"]))
516 cardlist.append(Card.valueSuitFromCard(row["card14"]))
517 cardlist.append(Card.valueSuitFromCard(row["card15"]))
518 cardlist.append(Card.valueSuitFromCard(row["card16"]))
519 cardlist.append(Card.valueSuitFromCard(row["card17"]))
520 cardlist.append(Card.valueSuitFromCard(row["card18"]))
521 cardlist.append(Card.valueSuitFromCard(row["card19"]))
522 cardlist.append(Card.valueSuitFromCard(row["card20"]))
523 # mucked/shown/dealt is not in the database, use mucked for villain and dealt for hero
524 if row["seatno"] == heroSeat:
525 dealt = True
526 mucked = False
527 else:
528 dealt = False
529 mucked = True
530 game = Card.games[self.gametype["category"]]
531 if game[0] == "hold" and cardlist[0] != "":
532 self.addHoleCards(
533 "PREFLOP", row["name"], closed=cardlist[0 : game[5][0][1]], shown=False, mucked=mucked, dealt=dealt
534 )
535 elif game[0] == "stud" and cardlist[2] != "":
536 streets = dict((v, k) for (k, v) in list(game[3].items()))
537 for streetidx, hrange in enumerate(game[5]):
538 # FIXME shown/dealt/mucked might need some tweaking
539 self.addHoleCards(
540 streets[streetidx],
541 row["name"],
542 open=[cardlist[hrange[1] - 1]],
543 closed=cardlist[0 : hrange[1] - 1],
544 shown=False,
545 mucked=False,
546 )
547 elif game[0] == "draw":
548 streets = dict((v, k) for (k, v) in list(game[3].items()))
549 for streetidx, hrange in enumerate(game[5]):
550 self.addHoleCards(
551 streets[streetidx],
552 row["name"],
553 closed=cardlist[hrange[0] : hrange[1]],
554 shown=False,
555 mucked=mucked,
556 dealt=dealt,
557 )
558 if row["winnings"] > 0:
559 self.addCollectPot(row["name"], str(row["winnings"]))
560 if row["position"] == "0":
561 # position 0 is the button, heads-up there is no position 0
562 self.buttonpos = row["seatno"]
563 elif row["position"] == "B":
564 # Headsup the BB is the button, only set the button position if it's not set before
565 if self.buttonpos is None or self.buttonpos == 0:
566 self.buttonpos = row["seatno"]
568 # HandInfo
569 q = db.sql.query["singleHand"]
570 q = q.replace("%s", db.sql.query["placeholder"])
571 c.execute(q, (handId,))
573 # NOTE: This relies on row_factory = sqlite3.Row (set in connect() params)
574 # Need to find MySQL and Postgres equivalents
575 # MySQL maybe: cursorclass=MySQLdb.cursors.DictCursor
576 # res = c.fetchone()
578 # Using row_factory is global, and affects the rest of fpdb. The following 2 line achieves
579 # a similar result
581 # Discripter must be set to lowercase as supported dbs differ on what is returned.
582 res = [
583 dict(line)
584 for line in [list(zip([column[0].lower() for column in c.description], row)) for row in c.fetchall()]
585 ]
586 res = res[0]
588 self.tablename = res["tablename"]
589 self.handid = res["sitehandno"]
590 # FIXME: Need to figure out why some times come out of the DB as %Y-%m-%d %H:%M:%S+00:00,
591 # and others as %Y-%m-%d %H:%M:%S
593 # self.startTime currently unused in the replayer and commented out.
594 # Can't be done like this because not all dbs return the same type for starttime
595 # try:
596 # self.startTime = datetime.datetime.strptime(res['starttime'], "%Y-%m-%d %H:%M:%S+00:00")
597 # except ValueError:
598 # self.startTime = datetime.datetime.strptime(res['starttime'], "%Y-%m-%d %H:%M:%S")
599 # However a startTime is needed for a valid output by writeHand:
600 self.startTime = datetime.datetime.strptime("1970-01-01 12:00:00", "%Y-%m-%d %H:%M:%S")
602 cards = list(
603 map(
604 Card.valueSuitFromCard,
605 [res["boardcard1"], res["boardcard2"], res["boardcard3"], res["boardcard4"], res["boardcard5"]],
606 )
607 )
608 if cards[0]:
609 self.setCommunityCards("FLOP", cards[0:3])
610 if cards[3]:
611 self.setCommunityCards("TURN", [cards[3]])
612 if cards[4]:
613 self.setCommunityCards("RIVER", [cards[4]])
615 if res["runittwice"] or self.gametype["split"]:
616 # Get runItTwice boards
617 q = db.sql.query["singleHandBoards"]
618 q = q.replace("%s", db.sql.query["placeholder"])
619 c.execute(q, (handId,))
620 boards = [
621 dict(line)
622 for line in [list(zip([column[0].lower() for column in c.description], row)) for row in c.fetchall()]
623 ]
624 for b in boards:
625 cards = list(
626 map(
627 Card.valueSuitFromCard,
628 [b["boardcard1"], b["boardcard2"], b["boardcard3"], b["boardcard4"], b["boardcard5"]],
629 )
630 )
631 if cards[0]:
632 street = "FLOP" + str(b["boardid"])
633 self.setCommunityCards(street, cards[0:3])
634 if "FLOP" in self.allStreets:
635 self.allStreets.remove("FLOP")
636 self.allStreets.append(street)
637 self.actions[street] = []
638 if cards[3]:
639 street = "TURN" + str(b["boardid"])
640 self.setCommunityCards(street, [cards[3]])
641 if "TURN" in self.allStreets:
642 self.allStreets.remove("TURN")
643 self.allStreets.append(street)
644 self.actions[street] = []
645 if cards[4]:
646 street = "RIVER" + str(b["boardid"])
647 self.setCommunityCards(street, [cards[4]])
648 if "RIVER" in self.allStreets:
649 self.allStreets.remove("RIVER")
650 self.allStreets.append(street)
651 self.actions[street] = []
653 # playersVpi | playersAtStreet1 | playersAtStreet2 | playersAtStreet3 |
654 # playersAtStreet4 | playersAtShowdown | street0Raises | street1Raises |
655 # street2Raises | street3Raises | street4Raises | street1Pot | street2Pot |
656 # street3Pot | street4Pot | showdownPot | comment | commentTs | texture
658 # Actions
659 q = db.sql.query["handActions"]
660 q = q.replace("%s", db.sql.query["placeholder"])
661 c.execute(q, (handId,))
663 # Discripter must be set to lowercase as supported dbs differ on what is returned.
664 res = [
665 dict(line)
666 for line in [list(zip([column[0].lower() for column in c.description], row)) for row in c.fetchall()]
667 ]
668 for row in res:
669 name = row["name"]
670 street = row["street"]
671 act = row["actionid"]
672 # allin True/False if row['allIn'] == 0
673 bet = str(row["bet"])
674 street = self.allStreets[int(street) + 1]
675 discards = row["cardsdiscarded"]
676 log.debug("Hand.select():: name: '%s' street: '%s' act: '%s' bet: '%s'", name, street, act, bet)
677 if act == 1: # Ante
678 self.addAnte(name, bet)
679 elif act == 2: # Small Blind
680 self.addBlind(name, "small blind", bet)
681 elif act == 3: # Second small blind
682 self.addBlind(name, "secondsb", bet)
683 elif act == 4: # Big Blind
684 self.addBlind(name, "big blind", bet)
685 elif act == 5: # Post both blinds
686 self.addBlind(name, "both", bet)
687 elif act == 6: # Call
688 self.addCall(street, name, bet)
689 elif act == 7: # Raise
690 self.addRaiseBy(street, name, bet)
691 elif act == 8: # Bet
692 self.addBet(street, name, bet)
693 elif act == 9: # Stands pat
694 self.addStandsPat(street, name, discards)
695 elif act == 10: # Fold
696 self.addFold(street, name)
697 elif act == 11: # Check
698 self.addCheck(street, name)
699 elif act == 12: # Discard
700 self.addDiscard(street, name, row["numdiscarded"], discards)
701 elif act == 13: # Bringin
702 self.addBringIn(name, bet)
703 elif act == 14: # Complete
704 self.addComplete(street, name, bet)
705 elif act == 15:
706 self.addBlind(name, "straddle", bet)
707 elif act == 16:
708 self.addBlind(name, "button blind", bet)
709 elif act == 17: # Cashout
710 self.addCashout(street, name)
711 else:
712 print("DEBUG: unknown action: '%s'" % act)
714 self.totalPot()
715 self.rake = self.totalpot - self.totalcollected
717 def addPlayer(self, seat, name, chips, position=None, sitout=False, bounty=None):
718 """Adds a player to the hand, and initialises data structures indexed by player.
719 seat (int) indicating the seat
720 name (string) player name
721 chips (string) the chips the player has at the start of the hand (can be None)
722 position (string) indicating the position of the player (S,B, 0-7) (optional, not needed on Hand import from Handhistory).
723 If a player has None chips he won't be added."""
725 if len(self.players) > 0 and seat in [p[0] for p in self.players]:
726 raise FpdbHandPartial("addPlayer: " + ("Can't have 2 players in the same seat!") + ": '%s'" % self.handid)
728 log.debug("addPlayer: %s %s (%s)", seat, name, chips)
729 if chips is not None:
730 chips = chips.replace(",", "") # some sites have commas
731 self.players.append([seat, name, chips, position, bounty])
732 self.stacks[name] = Decimal(chips)
733 self.pot.addPlayer(name)
734 for street in self.actionStreets:
735 self.bets[street][name] = []
736 if sitout:
737 self.sitout.add(name)
739 def removePlayer(self, name):
740 if self.stacks.get(name):
741 self.players = [p for p in self.players if p[1] != name]
742 del self.stacks[name]
743 self.pot.removePlayer(name)
744 for street in self.actionStreets:
745 del self.bets[street][name]
746 self.sitout.discard(name)
748 def addStreets(self, match):
749 # go through m and initialise actions to empty list for each street.
750 if match:
751 # print('if match', match)
752 # print("if match.gr:",match.groupdict())
753 log.debug("type self.streets", type(self.streets))
754 self.streets.update(match.groupdict())
755 log.debug("streets:", str(self.streets))
756 log.debug("markStreets:\n" + str(self.streets))
757 else:
758 tmp = self.handText[0:100]
759 self.cancelled = True
760 raise FpdbHandPartial(
761 ("Streets didn't match - Assuming hand '%s' was cancelled.") % (self.handid)
762 + " "
763 + ("First 100 characters: %s") % tmp
764 )
766 def checkPlayerExists(self, player, source=None):
767 # Fast path, because this function is called ALL THE TIME.
768 if player in self.player_exists_cache:
769 return
771 if player not in (p[1] for p in self.players):
772 if source is not None:
773 log.error(("Hand.%s: '%s' unknown player: '%s'"), source, self.handid, player)
774 raise FpdbParseError
775 else:
776 self.player_exists_cache.add(player)
778 def setCommunityCards(self, street, cards):
779 log.debug("setCommunityCards %s %s", street, cards)
780 self.board[street] = [self.card(c) for c in cards]
782 def card(self, c):
783 """upper case the ranks but not suits, 'atjqk' => 'ATJQK'"""
784 for k, v in list(self.UPS.items()):
785 c = c.replace(k, v)
786 return c
788 def addAllIn(self, street, player, amount):
789 """For sites (currently only Merge & Microgaming) which record "all in" as a special action,
790 which can mean either "calls and is all in" or "raises all in"."""
791 self.checkPlayerExists(player, "addAllIn")
792 amount = amount.replace(",", "") # some sites have commas
793 Ai = Decimal(amount)
794 Bp = self.lastBet[street]
795 Bc = sum(self.bets[street][player])
796 C = Bp - Bc
797 if Ai <= C:
798 self.addCall(street, player, amount)
799 elif Bp == 0:
800 self.addBet(street, player, amount)
801 else:
802 Rb = Ai - C
803 Rt = Bp + Rb
804 self._addRaise(street, player, C, Rb, Rt)
806 def addSTP(self, amount):
807 amount = amount.replace(",", "") # some sites have commas
808 amount = Decimal(amount)
809 self.pot.setSTP(amount)
811 def addAnte(self, player, ante):
812 log.debug("%s %s antes %s", "BLINDSANTES", player, ante)
813 if player is not None:
814 ante = ante.replace(",", "") # some sites have commas
815 self.checkPlayerExists(player, "addAnte")
816 ante = Decimal(ante)
817 self.bets["BLINDSANTES"][player].append(ante)
818 self.stacks[player] -= ante
819 act = (player, "ante", ante, self.stacks[player] == 0)
820 self.actions["BLINDSANTES"].append(act)
821 self.pot.addCommonMoney(player, ante)
822 self.pot.addAntes(player, ante)
823 if "ante" not in list(self.gametype.keys()) or self.gametype["ante"] < ante:
824 self.gametype["ante"] = ante
826 # I think the antes should be common money, don't have enough hand history to check
828 def addBlind(self, player, blindtype, amount):
829 # if player is None, it's a missing small blind.
830 # The situation we need to cover are:
831 # Player in small blind posts
832 # - this is a bet of 1 sb, as yet uncalled.
833 # Player in the big blind posts
834 # - this is a call of 1 sb and a raise to 1 bb
835 #
836 log.debug("addBlind: %s posts %s, %s", player, blindtype, amount)
837 if player is not None:
838 self.checkPlayerExists(player, "addBlind")
839 amount = amount.replace(",", "") # some sites have commas
840 amount = Decimal(amount)
841 self.stacks[player] -= amount
842 act = (player, blindtype, amount, self.stacks[player] == 0)
843 self.actions["BLINDSANTES"].append(act)
845 if blindtype == "both":
846 # work with the real amount. limit games are listed as $1, $2, where
847 # the SB 0.50 and the BB is $1, after the turn the minimum bet amount is $2....
848 amount = Decimal(str(self.bb))
849 sb = Decimal(str(self.sb))
850 self.bets["BLINDSANTES"][player].append(sb)
851 self.pot.addCommonMoney(player, sb)
853 if blindtype == "secondsb":
854 amount = Decimal(0)
855 sb = Decimal(str(self.sb))
856 self.bets["BLINDSANTES"][player].append(sb)
857 self.pot.addCommonMoney(player, sb)
859 street = "BLAH"
861 if self.gametype["category"] == "aof_omaha":
862 street = "FLOP"
863 elif self.gametype["base"] == "hold":
864 street = "PREFLOP"
865 elif self.gametype["base"] == "draw":
866 street = "DEAL"
868 self.bets[street][player].append(amount)
869 self.pot.addMoney(player, amount)
870 if amount > self.lastBet.get(street):
871 self.lastBet[street] = amount
872 self.posted = self.posted + [[player, blindtype]]
874 def addCall(self, street, player=None, amount=None):
875 if amount is not None:
876 amount = amount.replace(",", "") # some sites have commas
877 log.debug(("%s %s calls %s"), street, player, amount)
878 # Potentially calculate the amount of the call if not supplied
879 # corner cases include if player would be all in
880 if amount is not None:
881 self.checkPlayerExists(player, "addCall")
882 amount = Decimal(amount)
883 self.bets[street][player].append(amount)
884 if street in ("PREFLOP", "DEAL", "THIRD") and self.lastBet.get(street) < amount:
885 self.lastBet[street] = amount
886 self.stacks[player] -= amount
887 act = (player, "calls", amount, self.stacks[player] == 0)
888 self.actions[street].append(act)
889 self.pot.addMoney(player, amount)
891 def addCallTo(self, street, player=None, amountTo=None):
892 if amountTo:
893 amountTo = amountTo.replace(",", "") # some sites have commas
894 # Potentially calculate the amount of the callTo if not supplied
895 # corner cases include if player would be all in
896 if amountTo is not None:
897 self.checkPlayerExists(player, "addCallTo")
898 Bc = sum(self.bets[street][player])
899 Ct = Decimal(amountTo)
900 C = Ct - Bc
901 amount = C
902 self.bets[street][player].append(amount)
903 self.stacks[player] -= amount
904 act = (player, "calls", amount, self.stacks[player] == 0)
905 self.actions[street].append(act)
906 self.pot.addMoney(player, amount)
908 def addRaiseBy(self, street, player, amountBy):
909 """Add a raise by amountBy on [street] by [player]"""
910 # Given only the amount raised by, the amount of the raise can be calculated by
911 # working out how much this player has already in the pot
912 # (which is the sum of self.bets[street][player])
913 # and how much he needs to call to match the previous player
914 # (which is tracked by self.lastBet)
915 # let Bp = previous bet
916 # Bc = amount player has committed so far
917 # Rb = raise by
918 # then: C = Bp - Bc (amount to call)
919 # Rt = Bp + Rb (raise to)
920 #
921 amountBy = amountBy.replace(",", "") # some sites have commas
922 self.checkPlayerExists(player, "addRaiseBy")
923 Rb = Decimal(amountBy)
924 Bp = self.lastBet[street]
925 Bc = sum(self.bets[street][player])
926 C = Bp - Bc
927 Rt = Bp + Rb
929 self._addRaise(street, player, C, Rb, Rt)
931 def addCallandRaise(self, street, player, amount):
932 """For sites which by "raises x" mean "calls and raises putting a total of x in the por"."""
933 self.checkPlayerExists(player, "addCallandRaise")
934 amount = amount.replace(",", "") # some sites have commas
935 CRb = Decimal(amount)
936 Bp = self.lastBet[street]
937 Bc = sum(self.bets[street][player])
938 C = Bp - Bc
939 Rb = CRb - C
940 Rt = Bp + Rb
942 self._addRaise(street, player, C, Rb, Rt)
944 def addRaiseTo(self, street, player, amountTo):
945 """Add a raise on [street] by [player] to [amountTo]"""
946 self.checkPlayerExists(player, "addRaiseTo")
947 amountTo = amountTo.replace(",", "") # some sites have commas
948 Bp = self.lastBet[street]
949 Bc = sum(self.bets[street][player])
950 Rt = Decimal(amountTo)
951 C = Bp - Bc
952 Rb = Rt - C - Bc
953 self._addRaise(street, player, C, Rb, Rt)
955 def _addRaise(self, street, player, C, Rb, Rt, action="raises"):
956 log.debug(("%s %s raise %s"), street, player, Rt)
957 self.bets[street][player].append(C + Rb)
958 self.stacks[player] -= C + Rb
959 act = (player, action, Rb, Rt, C, self.stacks[player] == 0)
960 self.actions[street].append(act)
961 self.lastBet[street] = Rt # TODO check this is correct
962 self.pot.addMoney(player, C + Rb)
964 def addBet(self, street, player, amount):
965 log.debug(("%s %s bets %s"), street, player, amount)
966 amount = amount.replace(",", "") # some sites have commas
967 amount = Decimal(amount)
968 self.checkPlayerExists(player, "addBet")
969 self.bets[street][player].append(amount)
970 self.stacks[player] -= amount
971 act = (player, "bets", amount, self.stacks[player] == 0)
972 self.actions[street].append(act)
973 self.lastBet[street] = amount
974 self.pot.addMoney(player, amount)
976 def addStandsPat(self, street, player, cards=None):
977 self.checkPlayerExists(player, "addStandsPat")
978 act = (player, "stands pat")
979 self.actions[street].append(act)
980 if cards:
981 cards = cards.split(" ")
982 self.addHoleCards(street, player, open=[], closed=cards)
984 def addFold(self, street, player):
985 log.debug(("%s %s folds"), street, player)
986 self.checkPlayerExists(player, "addFold")
987 if player in self.folded:
988 return
989 self.folded.add(player)
990 self.pot.addFold(player)
991 self.actions[street].append((player, "folds"))
993 def addCheck(self, street, player):
994 logging.debug(("%s %s checks"), street, player)
995 self.checkPlayerExists(player, "addCheck")
996 self.actions[street].append((player, "checks"))
998 def addCashout(self, street, player):
999 logging.debug(("%s %s cashout"), street, player)
1000 self.checkPlayerExists(player, "addCashout")
1001 self.actions[street].append((player, "cashout"))
1003 def discardDrawHoleCards(self, cards, player, street):
1004 log.debug("discardDrawHoleCards '%s' '%s' '%s'", cards, player, street)
1005 self.discards[street][player] = set([cards])
1007 def addDiscard(self, street, player, num, cards=None):
1008 self.checkPlayerExists(player, "addDiscard")
1009 if cards:
1010 act = (player, "discards", Decimal(num), cards)
1011 self.discardDrawHoleCards(cards, player, street)
1012 else:
1013 act = (player, "discards", Decimal(num))
1014 self.actions[street].append(act)
1016 def addCollectPot(self, player, pot):
1017 log.debug("%s collected %s", player, pot)
1018 self.checkPlayerExists(player, "addCollectPot")
1019 self.collected = self.collected + [[player, pot]]
1020 if player not in self.collectees:
1021 self.collectees[player] = Decimal(pot)
1022 else:
1023 self.collectees[player] += Decimal(pot)
1025 def addUncalled(self, street, player, amount):
1026 log.debug(("%s %s uncalled %s"), street, player, amount)
1027 amount = amount.replace(",", "") # some sites have commas
1028 amount = Decimal(amount)
1029 self.checkPlayerExists(player, "addUncalled")
1030 self.stacks[player] += amount
1031 self.pot.removeMoney(player, amount)
1033 def sittingOut(self):
1034 dealtIn = set()
1035 for street in self.actionStreets:
1036 for act in self.actions[street]:
1037 dealtIn.add(act[0])
1038 for player in list(self.collectees.keys()):
1039 dealtIn.add(player)
1040 for player in self.dealt:
1041 dealtIn.add(player)
1042 for p in list(self.players):
1043 if p[1] not in dealtIn:
1044 if self.gametype["type"] == "tour":
1045 self.sitout.add(p[1])
1046 else:
1047 self.removePlayer(p[1])
1048 if len(self.players) < 2:
1049 raise FpdbHandPartial(("Less than 2 players - Assuming hand '%s' was cancelled.") % (self.handid))
1051 def setUncalledBets(self, value):
1052 self.uncalledbets = value
1054 def totalPot(self):
1055 """If all bets and blinds have been added, totals up the total pot size"""
1056 if self.totalpot is None:
1057 try:
1058 self.pot.end()
1059 self.totalpot = self.pot.total
1060 except FpdbParseError as e:
1061 log.error("Error in totalPot calculation: %s", e)
1062 self.totalpot = 0
1064 if self.adjustCollected:
1065 self.stats.awardPots(self)
1067 def gettempcontainers(collected, collectees):
1068 collectedCopy, collecteesCopy, totalcollected = [], {}, 0
1069 for v in sorted(collected, key=lambda collectee: collectee[1], reverse=True):
1070 if Decimal(v[1]) != 0:
1071 totalcollected += Decimal(v[1])
1072 collectedCopy.append([v[0], Decimal(v[1])])
1073 for k, j in collectees.items():
1074 if j != 0:
1075 collecteesCopy[k] = j
1076 return collectedCopy, collecteesCopy, totalcollected
1078 collected, collectees, totalcollected = gettempcontainers(self.collected, self.collectees)
1079 if self.uncalledbets or ((self.totalpot - totalcollected < 0) and self.checkForUncalled):
1080 for i, v in enumerate(sorted(self.collected, key=lambda collectee: collectee[1], reverse=True)):
1081 if v[0] in self.pot.returned:
1082 collected[i][1] = Decimal(v[1]) - self.pot.returned[v[0]]
1083 collectees[v[0]] -= self.pot.returned[v[0]]
1084 self.pot.returned[v[0]] = 0
1085 self.collected, self.collectees, self.totalcollected = gettempcontainers(collected, collectees)
1087 if self.totalcollected is None:
1088 self.totalcollected = 0
1089 for entry in self.collected:
1090 self.totalcollected += Decimal(entry[1])
1092 def getGameTypeAsString(self):
1093 """Map the tuple self.gametype onto the pokerstars string describing it"""
1094 # currently it appears to be something like ["ring", "hold", "nl", sb, bb]:
1095 gs = {
1096 "holdem": "Hold'em",
1097 "omahahi": "Omaha",
1098 "fusion": "Fusion",
1099 "omahahilo": "Omaha Hi/Lo",
1100 "razz": "Razz",
1101 "studhi": "7 Card Stud",
1102 "studhilo": "7 Card Stud Hi/Lo",
1103 "fivedraw": "5 Card Draw",
1104 "27_1draw": "Single Draw 2-7 Lowball",
1105 "27_3draw": "Triple Draw 2-7 Lowball",
1106 "5_studhi": "5 Card Stud",
1107 "badugi": "Badugi",
1108 }
1109 ls = {"nl": "No Limit", "pl": "Pot Limit", "fl": "Limit", "cn": "Cap No Limit", "cp": "Cap Pot Limit"}
1111 log.debug("gametype: %s", self.gametype)
1112 retstring = "%s %s" % (gs[self.gametype["category"]], ls[self.gametype["limitType"]])
1113 return retstring
1115 def printHand(self):
1116 log.debug(self.writeHand(sys.stdout))
1118 def actionString(self, act, street=None):
1119 log.debug("Hand.actionString(act=%s, street=%s)", act, street)
1121 if act[1] == "folds":
1122 return "%s: folds " % (act[0])
1123 elif act[1] == "checks":
1124 return "%s: checks " % (act[0])
1125 elif act[1] == "cashout":
1126 return "%s: cashout " % (act[0])
1127 elif act[1] == "calls":
1128 return "%s: calls %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1129 elif act[1] == "bets":
1130 return "%s: bets %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1131 elif act[1] == "raises":
1132 return "%s: raises %s%s to %s%s%s" % (
1133 act[0],
1134 self.sym,
1135 act[2],
1136 self.sym,
1137 act[3],
1138 " and is all-in" if act[5] else "",
1139 )
1140 elif act[1] == "completes":
1141 return "%s: completes to %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1142 elif act[1] == "small blind":
1143 return "%s: posts small blind %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1144 elif act[1] == "big blind":
1145 return "%s: posts big blind %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1146 elif act[1] == "straddle":
1147 return "%s: straddles %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1148 elif act[1] == "button blind":
1149 return "%s: posts button blind %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1150 elif act[1] == "both":
1151 return "%s: posts small & big blinds %s%s%s" % (
1152 act[0],
1153 self.sym,
1154 act[2],
1155 " and is all-in" if act[3] else "",
1156 )
1157 elif act[1] == "ante":
1158 return "%s: posts the ante %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1159 elif act[1] == "bringin":
1160 return "%s: brings in for %s%s%s" % (act[0], self.sym, act[2], " and is all-in" if act[3] else "")
1161 elif act[1] == "discards":
1162 return "%s: discards %s %s%s" % (
1163 act[0],
1164 act[2],
1165 "card" if act[2] == 1 else "cards",
1166 " [" + " ".join(self.discards[street][act[0]]) + "]" if self.hero == act[0] else "",
1167 )
1168 elif act[1] == "stands pat":
1169 return "%s: stands pat" % (act[0])
1171 def get_actions_short(self, player, street):
1172 """Returns a string with shortcuts for the actions of the given player and the given street
1173 F ... fold, X ... Check, B ...Bet, C ... Call, R ... Raise, CO ... CashOut
1174 """
1175 actions = self.actions[street]
1176 result = []
1177 for action in actions:
1178 if player in action:
1179 if action[1] == "folds":
1180 result.append("F")
1181 elif action[1] == "checks":
1182 result.append("X")
1183 elif action[1] == "bets":
1184 result.append("B")
1185 elif action[1] == "calls":
1186 result.append("C")
1187 elif action[1] == "raises":
1188 result.append("R")
1189 elif action[1] == "cashout":
1190 result.append("CO")
1191 return "".join(result)
1193 def get_actions_short_streets(self, player, *streets):
1194 """Returns a string with shortcuts for the actions of the given player on all given streets seperated by ','"""
1195 result = []
1196 for street in streets:
1197 action = self.get_actions_short(player, street)
1198 if len(action) > 0: # if there is no action on later streets, nothing is added.
1199 result.append(action)
1200 return ",".join(result)
1202 def get_player_position(self, player):
1203 """Returns the given players postion (S, B, 0-7)"""
1204 # position has been added to the players list. It could be calculated from buttonpos and player seatnums,
1205 # but whats the point in calculating a value that has been there anyway?
1206 for p in self.players:
1207 if p[1] == player:
1208 return p[3]
1210 def getStakesAsString(self):
1211 """Return a string of the stakes of the current hand."""
1212 return "%s%s/%s%s" % (self.sym, self.sb, self.sym, self.bb)
1214 def getStreetTotals(self):
1215 tmp, i = [0, 0, 0, 0, 0, 0], 0
1216 for street in self.allStreets:
1217 if street != "BLINDSANTES":
1218 tmp[i] = self.pot.getTotalAtStreet(street)
1219 i += 1
1220 tmp[5] = sum(self.pot.committed.values()) + sum(self.pot.common.values())
1221 return tmp
1223 def writeGameLine(self):
1224 """Return the first HH line for the current hand."""
1225 gs = "PokerStars Game #%s: " % self.handid
1227 if self.tourNo is not None and self.mixed is not None: # mixed tournament
1228 gs = gs + "Tournament #%s, %s %s (%s) - Level %s (%s) - " % (
1229 self.tourNo,
1230 self.buyin,
1231 self.MS[self.mixed],
1232 self.getGameTypeAsString(),
1233 self.level,
1234 self.getStakesAsString(),
1235 )
1236 elif self.tourNo is not None: # all other tournaments
1237 gs = gs + "Tournament #%s, %s %s - Level %s (%s) - " % (
1238 self.tourNo,
1239 self.buyin,
1240 self.getGameTypeAsString(),
1241 self.level,
1242 self.getStakesAsString(),
1243 )
1244 elif self.mixed is not None: # all other mixed games
1245 gs = gs + " %s (%s, %s) - " % (self.MS[self.mixed], self.getGameTypeAsString(), self.getStakesAsString())
1246 else: # non-mixed cash games
1247 gs = gs + " %s (%s) - " % (self.getGameTypeAsString(), self.getStakesAsString())
1249 try:
1250 timestr = datetime.datetime.strftime(self.startTime, "%Y/%m/%d %H:%M:%S ET")
1251 except TypeError:
1252 print(
1253 (
1254 "*** ERROR - HAND: calling writeGameLine with unexpected STARTTIME value, expecting datetime.date object, received:"
1255 ),
1256 self.startTime,
1257 )
1258 print(("*** Make sure your HandHistoryConverter is setting hand.startTime properly!"))
1259 print(("*** Game String:"), gs)
1260 return gs
1261 else:
1262 return gs + timestr
1264 def writeTableLine(self):
1265 table_string = "Table "
1266 if self.gametype["type"] == "tour":
1267 table_string = table_string + "'%s %s' %s-max" % (self.tourNo, self.tablename, self.maxseats)
1268 else:
1269 table_string = table_string + "'%s' %s-max" % (self.tablename, self.maxseats)
1270 if self.gametype["currency"] == "play":
1271 table_string = table_string + " (Play Money)"
1272 if self.buttonpos is not None and self.buttonpos != 0:
1273 table_string = table_string + " Seat #%s is the button" % self.buttonpos
1274 return table_string
1276 def writeHand(self, fh=sys.__stdout__):
1277 # PokerStars format.
1278 print(self.writeGameLine(), file=fh)
1279 print(self.writeTableLine(), file=fh)
1282class HoldemOmahaHand(Hand):
1283 def __init__(self, config, hhc, sitename, gametype, handText, builtFrom="HHC", handid=None):
1284 self.config = config
1285 if gametype["base"] != "hold":
1286 pass # or indeed don't pass and complain instead
1287 log.debug("HoldemOmahaHand")
1288 self.allStreets = ["BLINDSANTES", "PREFLOP", "FLOP", "TURN", "RIVER"]
1289 if gametype["category"] == "fusion":
1290 self.holeStreets = ["PREFLOP", "FLOP", "TURN"]
1291 else:
1292 self.holeStreets = ["PREFLOP"]
1293 if gametype["category"] == "irish":
1294 self.discardStreets = ["TURN"]
1295 else:
1296 self.discardStreets = ["PREFLOP"]
1297 self.communityStreets = ["FLOP", "TURN", "RIVER"]
1298 self.actionStreets = ["BLINDSANTES", "PREFLOP", "FLOP", "TURN", "RIVER"]
1299 if gametype["category"] == "aof_omaha":
1300 self.allStreets = ["BLINDSANTES", "FLOP", "TURN", "RIVER"]
1301 self.holeStreets = ["FLOP"]
1302 self.communityStreets = ["FLOP", "TURN", "RIVER"]
1303 self.actionStreets = ["BLINDSANTES", "FLOP", "TURN", "RIVER"]
1304 Hand.__init__(self, self.config, sitename, gametype, handText, builtFrom="HHC")
1305 self.sb = gametype["sb"]
1306 self.bb = gametype["bb"]
1307 if hasattr(hhc, "in_path"):
1308 self.in_path = hhc.in_path
1309 else:
1310 self.in_path = "database"
1312 # Populate a HoldemOmahaHand
1313 # Generally, we call 'read' methods here, which get the info according to the particular filter (hhc)
1314 # which then invokes a 'addXXX' callback
1315 if builtFrom == "HHC":
1316 hhc.readHandInfo(self)
1317 if self.gametype["type"] == "tour":
1318 self.tablename = "%s %s" % (self.tourNo, self.tablename)
1319 hhc.readPlayerStacks(self)
1320 hhc.compilePlayerRegexs(self)
1321 hhc.markStreets(self)
1323 if self.cancelled:
1324 return
1326 hhc.readBlinds(self)
1328 hhc.readSTP(self)
1329 hhc.readAntes(self)
1330 hhc.readButton(self)
1331 hhc.readHoleCards(self)
1332 hhc.readShowdownActions(self)
1333 # Read actions in street order
1334 for street, text in list(self.streets.items()):
1335 if (
1336 text and (street != "PREFLOP")
1337 ): # TODO: the except PREFLOP shouldn't be necessary, but regression-test-files/cash/Everleaf/Flop/NLHE-10max-USD-0.01-0.02-201008.2Way.All-in.pre.txt fails without it
1338 hhc.readCommunityCards(self, street)
1339 for street in self.actionStreets:
1340 if self.streets[street] or gametype["split"]:
1341 hhc.readAction(self, street)
1342 self.pot.markTotal(street)
1343 hhc.readCollectPot(self)
1344 hhc.readShownCards(self)
1345 self.pot.handid = self.handid # This is only required so Pot can throw it in totalPot
1346 self.totalPot() # finalise it (total the pot)
1347 log.debug("self.totalpot", self.totalpot)
1348 hhc.getRake(self)
1349 if self.maxseats is None:
1350 self.maxseats = hhc.guessMaxSeats(self)
1351 self.sittingOut()
1352 hhc.readTourneyResults(self)
1353 # readOther is deprecated
1354 # hhc.readOther(self)
1355 elif builtFrom == "DB":
1356 # Creator expected to call hhc.select(hid) to fill out object
1357 log.debug(
1358 "HoldemOmahaHand.__init__: " + ("DEBUG:") + " " + ("HoldemOmaha hand initialised for %s"), "select()"
1359 )
1360 self.maxseats = 10
1361 else:
1362 log.warning("HoldemOmahaHand.__init__: " + ("Neither HHC nor DB+handID provided"))
1364 def addShownCards(self, cards, player, shown=True, mucked=False, dealt=False, string=None):
1365 if player == self.hero: # we have hero's cards just update shown/mucked
1366 if shown:
1367 self.shown.add(player)
1368 if mucked:
1369 self.mucked.add(player)
1370 else:
1371 if self.gametype["category"] == "aof_omaha":
1372 self.addHoleCards("FLOP", player, open=[], closed=cards, shown=shown, mucked=mucked, dealt=dealt)
1373 elif len(cards) in (2, 3, 4, 6) or self.gametype["category"] in (
1374 "5_omahahi",
1375 "5_omaha8",
1376 "cour_hi",
1377 "cour_hilo",
1378 "fusion",
1379 ): # avoid adding board by mistake (Everleaf problem)
1380 self.addHoleCards("PREFLOP", player, open=[], closed=cards, shown=shown, mucked=mucked, dealt=dealt)
1382 elif len(cards) == 5: # cards holds a winning hand, not hole cards
1383 # filter( lambda x: x not in b, a ) # calcs a - b where a and b are lists
1384 # so diff is set to the winning hand minus the board cards, if we're lucky that leaves the hole cards
1385 diff = [x for x in cards if x not in self.board["FLOP"] + self.board["TURN"] + self.board["RIVER"]]
1386 if len(diff) == 2 and self.gametype["category"] in ("holdem"):
1387 self.addHoleCards("PREFLOP", player, open=[], closed=diff, shown=shown, mucked=mucked, dealt=dealt)
1388 if string is not None:
1389 self.showdownStrings[player] = string
1391 def join_holecards(self, player, asList=False):
1392 holeNo = Card.games[self.gametype["category"]][5][0][1]
1393 hcs = ["0x"] * holeNo
1394 if self.gametype["category"] == "fusion":
1395 for street in self.holeStreets:
1396 if player in list(self.holecards[street].keys()):
1397 if street == "PREFLOP":
1398 if len(self.holecards[street][player][1]) == 1:
1399 continue
1400 for i in 0, 1:
1401 hcs[i] = self.holecards[street][player][1][i]
1402 hcs[i] = hcs[i][0:1].upper() + hcs[i][1:2]
1403 try:
1404 for i in range(2, holeNo):
1405 hcs[i] = self.holecards[street][player][1][i]
1406 hcs[i] = hcs[i][0:1].upper() + hcs[i][1:2]
1407 except IndexError:
1408 log.debug("Why did we get an indexerror?")
1409 elif street == "FLOP":
1410 if len(self.holecards[street][player][1]) == 1:
1411 continue
1412 hcs[2] = self.holecards[street][player][0][0]
1413 hcs[2] = hcs[2][0:1].upper() + hcs[2][1:2]
1414 elif street == "TURN":
1415 if len(self.holecards[street][player][1]) == 1:
1416 continue
1417 hcs[3] = self.holecards[street][player][0][0]
1418 hcs[3] = hcs[3][0:1].upper() + hcs[3][1:2]
1420 else:
1421 for street in self.holeStreets:
1422 if player in list(self.holecards[street].keys()):
1423 if len(self.holecards[street][player][1]) == 1:
1424 continue
1425 for i in 0, 1:
1426 hcs[i] = self.holecards[street][player][1][i]
1427 hcs[i] = hcs[i][0:1].upper() + hcs[i][1:2]
1428 try:
1429 for i in range(2, holeNo):
1430 hcs[i] = self.holecards[street][player][1][i]
1431 hcs[i] = hcs[i][0:1].upper() + hcs[i][1:2]
1432 except IndexError:
1433 log.debug("Why did we get an indexerror?")
1435 if asList:
1436 return hcs
1437 else:
1438 return " ".join(hcs)
1440 def writeHand(self, fh=sys.__stdout__):
1441 # PokerStars format.
1442 super(HoldemOmahaHand, self).writeHand(fh)
1444 players_who_act_preflop = set(
1445 ([x[0] for x in self.actions["PREFLOP"]] + [x[0] for x in self.actions["BLINDSANTES"]])
1446 )
1447 log.debug(self.actions["PREFLOP"])
1448 for player in [x for x in self.players if x[1] in players_who_act_preflop]:
1449 # Only print stacks of players who do something preflop
1450 print(("Seat %s: %s ($%.2f in chips) " % (player[0], player[1], float(player[2]))), file=fh)
1452 if self.actions["BLINDSANTES"]:
1453 log.debug(self.actions["BLINDSANTES"])
1454 for act in self.actions["BLINDSANTES"]:
1455 print(self.actionString(act), file=fh)
1457 print(("*** HOLE CARDS ***"), file=fh)
1458 for player in self.dealt:
1459 print(("Dealt to %s [%s]" % (player, " ".join(self.holecards["PREFLOP"][player][1]))), file=fh)
1460 if self.hero == "":
1461 for player in self.shown.difference(self.dealt):
1462 print(("Dealt to %s [%s]" % (player, " ".join(self.holecards["PREFLOP"][player][1]))), file=fh)
1464 if self.actions["PREFLOP"]:
1465 for act in self.actions["PREFLOP"]:
1466 print(self.actionString(act), file=fh)
1468 if self.board["FLOP"]:
1469 print(("*** FLOP *** [%s]" % (" ".join(self.board["FLOP"]))), file=fh)
1470 if self.actions["FLOP"]:
1471 for act in self.actions["FLOP"]:
1472 print(("*** FLOP *** [%s]" % (" ".join(self.board["FLOP"]))), file=fh)
1474 if self.board["TURN"]:
1475 print(("*** TURN *** [%s] [%s]" % (" ".join(self.board["FLOP"]), " ".join(self.board["TURN"]))), file=fh)
1476 if self.actions["TURN"]:
1477 for act in self.actions["TURN"]:
1478 print(self.actionString(act), file=fh)
1480 if self.board["RIVER"]:
1481 print(
1482 (
1483 "*** RIVER *** [%s] [%s]"
1484 % (" ".join(self.board["FLOP"] + self.board["TURN"]), " ".join(self.board["RIVER"]))
1485 ),
1486 file=fh,
1487 )
1488 if self.actions["RIVER"]:
1489 for act in self.actions["RIVER"]:
1490 print(self.actionString(act), file=fh)
1492 # Some sites don't have a showdown section so we have to figure out if there should be one
1493 # The logic for a showdown is: at the end of river action there are at least two players in the hand
1494 # we probably don't need a showdown section in pseudo stars format for our filtering purposes
1495 if self.shown:
1496 print(("*** SHOW DOWN ***"), file=fh)
1497 for name in self.shown:
1498 # TODO: legacy importer can't handle only one holecard here, make sure there are 2 for holdem, 4 for omaha
1499 # TOOD: If HoldHand subclass supports more than omahahi, omahahilo, holdem, add them here
1500 numOfHoleCardsNeeded = None
1501 if self.gametype["category"] in ("omahahi", "omahahilo"):
1502 numOfHoleCardsNeeded = 4
1503 elif self.gametype["category"] in ("holdem"):
1504 numOfHoleCardsNeeded = 2
1505 if len(self.holecards["PREFLOP"][name]) == numOfHoleCardsNeeded:
1506 print(("%s shows [%s] (a hand...)" % (name, " ".join(self.holecards["PREFLOP"][name][1]))), file=fh)
1508 # Current PS format has the lines:
1509 # Uncalled bet ($111.25) returned to s0rrow
1510 # s0rrow collected $5.15 from side pot
1511 # stervels: shows [Ks Qs] (two pair, Kings and Queens)
1512 # stervels collected $45.35 from main pot
1513 # Immediately before the summary.
1514 # The current importer uses those lines for importing winning rather than the summary
1515 for name in self.pot.returned:
1516 print(("Uncalled bet (%s%s) returned to %s" % (self.sym, self.pot.returned[name], name)), file=fh)
1517 for entry in self.collected:
1518 print(("%s collected %s%s from pot" % (entry[0], self.sym, entry[1])), file=fh)
1520 print(("*** SUMMARY ***"), file=fh)
1521 print("%s | Rake %s%.2f" % (self.pot, self.sym, self.rake), file=fh)
1523 board = []
1524 for street in ["FLOP", "TURN", "RIVER"]:
1525 board += self.board[street]
1526 if board: # sometimes hand ends preflop without a board
1527 print(("Board [%s]" % (" ".join(board))), file=fh)
1529 for player in [x for x in self.players if x[1] in players_who_act_preflop]:
1530 seatnum = player[0]
1531 name = player[1]
1532 if name in self.collectees and name in self.shown:
1533 print(
1534 (
1535 "Seat %d: %s showed [%s] and won (%s%s)"
1536 % (seatnum, name, " ".join(self.holecards["PREFLOP"][name][1]), self.sym, self.collectees[name])
1537 ),
1538 file=fh,
1539 )
1540 elif name in self.collectees:
1541 print(("Seat %d: %s collected (%s%s)" % (seatnum, name, self.sym, self.collectees[name])), file=fh)
1542 elif name in self.folded:
1543 print(("Seat %d: %s folded" % (seatnum, name)), file=fh)
1544 else:
1545 if name in self.shown:
1546 print(
1547 (
1548 "Seat %d: %s showed [%s] and lost with..."
1549 % (seatnum, name, " ".join(self.holecards["PREFLOP"][name][1]))
1550 ),
1551 file=fh,
1552 )
1553 elif name in self.mucked:
1554 print(
1555 ("Seat %d: %s mucked [%s] " % (seatnum, name, " ".join(self.holecards["PREFLOP"][name][1]))),
1556 file=fh,
1557 )
1558 else:
1559 print(("Seat %d: %s mucked" % (seatnum, name)), file=fh)
1561 print("\n\n", file=fh)
1564class DrawHand(Hand):
1565 def __init__(self, config, hhc, sitename, gametype, handText, builtFrom="HHC", handid=None):
1566 self.config = config
1567 if gametype["base"] != "draw":
1568 pass # or indeed don't pass and complain instead
1569 self.streetList = ["BLINDSANTES", "DEAL", "DRAWONE"]
1570 self.allStreets = ["BLINDSANTES", "DEAL", "DRAWONE"]
1571 self.holeStreets = ["DEAL", "DRAWONE"]
1572 self.actionStreets = ["BLINDSANTES", "DEAL", "DRAWONE"]
1573 if gametype["category"] in ["27_3draw", "badugi", "a5_3draw", "badacey", "badeucey", "drawmaha"]:
1574 self.streetList += ["DRAWTWO", "DRAWTHREE"]
1575 self.allStreets += ["DRAWTWO", "DRAWTHREE"]
1576 self.holeStreets += ["DRAWTWO", "DRAWTHREE"]
1577 self.actionStreets += ["DRAWTWO", "DRAWTHREE"]
1578 self.discardStreets = self.holeStreets
1579 self.communityStreets = []
1580 Hand.__init__(self, self.config, sitename, gametype, handText)
1581 self.sb = gametype["sb"]
1582 self.bb = gametype["bb"]
1583 if hasattr(hhc, "in_path"):
1584 self.in_path = hhc.in_path
1585 else:
1586 self.in_path = "database"
1587 # Populate the draw hand.
1588 if builtFrom == "HHC":
1589 hhc.readHandInfo(self)
1590 if self.gametype["type"] == "tour":
1591 self.tablename = "%s %s" % (self.tourNo, self.tablename)
1592 hhc.readPlayerStacks(self)
1593 hhc.compilePlayerRegexs(self)
1594 hhc.markStreets(self)
1595 # markStreets in Draw may match without dealing cards
1596 if self.streets["DEAL"] is None:
1597 log.error("DrawHand.__init__: " + ("Street 'DEAL' is empty. Was hand '%s' cancelled?") % self.handid)
1598 raise FpdbParseError
1599 hhc.readBlinds(self)
1600 hhc.readSTP(self)
1601 hhc.readAntes(self)
1602 hhc.readButton(self)
1603 hhc.readHoleCards(self)
1604 hhc.readShowdownActions(self)
1605 # Read actions in street order
1606 for street in self.streetList:
1607 if self.streets[street]:
1608 hhc.readAction(self, street)
1609 self.pot.markTotal(street)
1610 hhc.readCollectPot(self)
1611 hhc.readShownCards(self)
1612 self.pot.handid = self.handid # This is only required so Pot can throw it in totalPot
1613 self.totalPot() # finalise it (total the pot)
1614 hhc.getRake(self)
1615 if self.maxseats is None:
1616 self.maxseats = hhc.guessMaxSeats(self)
1617 self.sittingOut()
1618 hhc.readTourneyResults(self)
1619 hhc.readOther(self)
1621 elif builtFrom == "DB":
1622 # Creator expected to call hhc.select(hid) to fill out object
1623 self.maxseats = 10
1625 def addShownCards(self, cards, player, shown=True, mucked=False, dealt=False, string=None):
1626 # if player == self.hero: # we have hero's cards just update shown/mucked
1627 # if shown: self.shown.add(player)
1628 # if mucked: self.mucked.add(player)
1629 # else:
1630 # pass
1631 # TODO: Probably better to find the last street with action and add the hole cards to that street
1632 self.addHoleCards(
1633 self.actionStreets[-1], player, open=[], closed=cards, shown=shown, mucked=mucked, dealt=dealt
1634 )
1635 if string is not None:
1636 self.showdownStrings[player] = string
1638 def holecardsAsSet(self, street, player):
1639 """Return holdcards: (oc, nc) as set()"""
1640 (nc, oc) = self.holecards[street][player]
1641 nc = set(nc)
1642 oc = set(oc)
1643 return (nc, oc)
1645 def join_holecards(self, player, asList=False, street=False):
1646 """With asList = True it returns the set cards for a player including down cards if they aren't know"""
1647 handsize = Card.games[self.gametype["category"]][5][0][1]
1648 holecards = ["0x"] * 20
1650 for i, _street in enumerate(self.holeStreets):
1651 if player in list(self.holecards[_street].keys()):
1652 allhole = self.holecards[_street][player][1] + self.holecards[_street][player][0]
1653 allhole = allhole[:handsize]
1654 for c in range(len(allhole)):
1655 idx = c + i * 5
1656 holecards[idx] = allhole[c]
1658 result = []
1659 if street is False:
1660 result = holecards
1661 elif street in self.holeStreets:
1662 if street == "DEAL":
1663 result = holecards[0:5]
1664 elif street == "DRAWONE":
1665 result = holecards[5:10]
1666 elif street == "DRAWTWO":
1667 result = holecards[10:15]
1668 elif street == "DRAWTHREE":
1669 result = holecards[15:20]
1670 return result if asList else " ".join(result)
1672 def writeHand(self, fh=sys.__stdout__):
1673 # PokerStars format.
1674 # HH output should not be translated
1675 super(DrawHand, self).writeHand(fh)
1677 players_who_act_ondeal = set(
1678 ([x[0] for x in self.actions["DEAL"]] + [x[0] for x in self.actions["BLINDSANTES"]])
1679 )
1681 for player in [x for x in self.players if x[1] in players_who_act_ondeal]:
1682 # Only print stacks of players who do something on deal
1683 print((("Seat %s: %s (%s%s in chips) ") % (player[0], player[1], self.sym, player[2])), file=fh)
1685 if "BLINDSANTES" in self.actions:
1686 for act in self.actions["BLINDSANTES"]:
1687 print(("%s: %s %s %s%s" % (act[0], act[1], act[2], self.sym, act[3])), file=fh)
1689 if "DEAL" in self.actions:
1690 print(("*** DEALING HANDS ***"), file=fh)
1691 for player in [x[1] for x in self.players if x[1] in players_who_act_ondeal]:
1692 if "DEAL" in self.holecards:
1693 if player in self.holecards["DEAL"]:
1694 (nc, oc) = self.holecards["DEAL"][player]
1695 print(("Dealt to %s: [%s]") % (player, " ".join(nc)), file=fh)
1696 for act in self.actions["DEAL"]:
1697 print(self.actionString(act, "DEAL"), file=fh)
1699 if "DRAWONE" in self.actions:
1700 print(("*** FIRST DRAW ***"), file=fh)
1701 for act in self.actions["DRAWONE"]:
1702 print(self.actionString(act, "DRAWONE"), file=fh)
1703 if act[0] == self.hero and act[1] == "discards":
1704 (nc, oc) = self.holecardsAsSet("DRAWONE", act[0])
1705 dc = self.discards["DRAWONE"][act[0]]
1706 kc = oc - dc
1707 print((("Dealt to %s [%s] [%s]") % (act[0], " ".join(kc), " ".join(nc))), file=fh)
1709 if "DRAWTWO" in self.actions:
1710 print(("*** SECOND DRAW ***"), file=fh)
1711 for act in self.actions["DRAWTWO"]:
1712 print(self.actionString(act, "DRAWTWO"), file=fh)
1713 if act[0] == self.hero and act[1] == "discards":
1714 (nc, oc) = self.holecardsAsSet("DRAWONE", act[0])
1715 dc = self.discards["DRAWTWO"][act[0]]
1716 kc = oc - dc
1717 print((("Dealt to %s [%s] [%s]") % (act[0], " ".join(kc), " ".join(nc))), file=fh)
1719 if "DRAWTHREE" in self.actions:
1720 print(("*** THIRD DRAW ***"), file=fh)
1721 for act in self.actions["DRAWTHREE"]:
1722 print(self.actionString(act, "DRAWTHREE"), file=fh)
1723 if act[0] == self.hero and act[1] == "discards":
1724 (nc, oc) = self.holecardsAsSet("DRAWONE", act[0])
1725 dc = self.discards["DRAWTHREE"][act[0]]
1726 kc = oc - dc
1727 print((("Dealt to %s [%s] [%s]") % (act[0], " ".join(kc), " ".join(nc))), file=fh)
1729 if "SHOWDOWN" in self.actions:
1730 print(("*** SHOW DOWN ***"), file=fh)
1731 # TODO: Complete SHOWDOWN
1733 # Current PS format has the lines:
1734 # Uncalled bet ($111.25) returned to s0rrow
1735 # s0rrow collected $5.15 from side pot
1736 # stervels: shows [Ks Qs] (two pair, Kings and Queens)
1737 # stervels collected $45.35 from main pot
1738 # Immediately before the summary.
1739 # The current importer uses those lines for importing winning rather than the summary
1740 for name in self.pot.returned:
1741 print(("Uncalled bet (%s%s) returned to %s" % (self.sym, self.pot.returned[name], name)), file=fh)
1742 for entry in self.collected:
1743 print(("%s collected %s%s from pot" % (entry[0], self.sym, entry[1])), file=fh)
1745 print(("*** SUMMARY ***"), file=fh)
1746 print("%s | Rake %s%.2f" % (self.pot, self.sym, self.rake), file=fh)
1747 print("\n\n", file=fh)
1750class StudHand(Hand):
1751 def __init__(self, config, hhc, sitename, gametype, handText, builtFrom="HHC", handid=None):
1752 self.config = config
1753 if gametype["base"] != "stud":
1754 pass # or indeed don't pass and complain instead
1756 self.communityStreets = []
1757 if gametype["category"] == "5_studhi":
1758 self.allStreets = ["BLINDSANTES", "SECOND", "THIRD", "FOURTH", "FIFTH"]
1759 self.actionStreets = ["BLINDSANTES", "SECOND", "THIRD", "FOURTH", "FIFTH"]
1760 self.streetList = [
1761 "BLINDSANTES",
1762 "SECOND",
1763 "THIRD",
1764 "FOURTH",
1765 "FIFTH",
1766 ] # a list of the observed street names in order
1767 self.holeStreets = ["SECOND", "THIRD", "FOURTH", "FIFTH"]
1768 else:
1769 self.allStreets = ["BLINDSANTES", "THIRD", "FOURTH", "FIFTH", "SIXTH", "SEVENTH"]
1770 self.actionStreets = ["BLINDSANTES", "THIRD", "FOURTH", "FIFTH", "SIXTH", "SEVENTH"]
1771 self.streetList = [
1772 "BLINDSANTES",
1773 "THIRD",
1774 "FOURTH",
1775 "FIFTH",
1776 "SIXTH",
1777 "SEVENTH",
1778 ] # a list of the observed street names in order
1779 self.holeStreets = ["THIRD", "FOURTH", "FIFTH", "SIXTH", "SEVENTH"]
1780 self.discardStreets = self.holeStreets
1781 Hand.__init__(self, self.config, sitename, gametype, handText)
1782 self.sb = gametype["sb"]
1783 self.bb = gametype["bb"]
1784 if hasattr(hhc, "in_path"):
1785 self.in_path = hhc.in_path
1786 else:
1787 self.in_path = "database"
1788 # Populate the StudHand
1789 # Generally, we call a 'read' method here, which gets the info according to the particular filter (hhc)
1790 # which then invokes a 'addXXX' callback
1791 if builtFrom == "HHC":
1792 hhc.readHandInfo(self)
1793 if self.gametype["type"] == "tour":
1794 self.tablename = "%s %s" % (self.tourNo, self.tablename)
1795 hhc.readPlayerStacks(self)
1796 hhc.compilePlayerRegexs(self)
1797 hhc.markStreets(self)
1798 hhc.readSTP(self)
1799 hhc.readAntes(self)
1800 hhc.readBringIn(self)
1801 hhc.readHoleCards(self)
1802 hhc.readShowdownActions(self)
1803 # Read actions in street order
1804 for street in self.actionStreets:
1805 if street == "BLINDSANTES":
1806 continue # OMG--sometime someone folds in the ante round
1807 if self.streets[street]:
1808 log.debug(street + self.streets[street])
1809 hhc.readAction(self, street)
1810 self.pot.markTotal(street)
1811 hhc.readCollectPot(self)
1812 hhc.readShownCards(self) # not done yet
1813 self.pot.handid = self.handid # This is only required so Pot can throw it in totalPot
1814 self.totalPot() # finalise it (total the pot)
1815 hhc.getRake(self)
1816 if self.maxseats is None:
1817 self.maxseats = hhc.guessMaxSeats(self)
1818 self.sittingOut()
1819 hhc.readTourneyResults(self)
1820 hhc.readOther(self)
1822 elif builtFrom == "DB":
1823 # Creator expected to call hhc.select(hid) to fill out object
1824 self.maxseats = 10
1826 def addShownCards(self, cards, player, shown=True, mucked=False, dealt=False, string=None):
1827 if player == self.hero: # we have hero's cards just update shown/mucked
1828 if shown:
1829 self.shown.add(player)
1830 if mucked:
1831 self.mucked.add(player)
1832 else:
1833 if self.gametype["category"] == "5_studhi" and len(cards) > 4:
1834 self.addHoleCards("SECOND", player, open=[cards[1]], closed=[cards[0]], shown=shown, mucked=mucked)
1835 self.addHoleCards("THIRD", player, open=[cards[2]], closed=[cards[1]], shown=shown, mucked=mucked)
1836 self.addHoleCards("FOURTH", player, open=[cards[3]], closed=cards[1:2], shown=shown, mucked=mucked)
1837 self.addHoleCards("FIFTH", player, open=[cards[4]], closed=cards[1:3], shown=shown, mucked=mucked)
1838 if len(cards) > 6:
1839 self.addHoleCards("THIRD", player, open=[cards[2]], closed=cards[0:2], shown=shown, mucked=mucked)
1840 self.addHoleCards("FOURTH", player, open=[cards[3]], closed=[cards[2]], shown=shown, mucked=mucked)
1841 self.addHoleCards("FIFTH", player, open=[cards[4]], closed=cards[2:4], shown=shown, mucked=mucked)
1842 self.addHoleCards("SIXTH", player, open=[cards[5]], closed=cards[2:5], shown=shown, mucked=mucked)
1843 self.addHoleCards("SEVENTH", player, open=[], closed=[cards[6]], shown=shown, mucked=mucked)
1844 if string is not None:
1845 self.showdownStrings[player] = string
1847 def addPlayerCards(self, player, street, open=[], closed=[]):
1848 """
1849 Assigns observed cards to a player.
1850 player (string) name of player
1851 street (string) the street name (in streetList)
1852 open list of card bigrams e.g. ['2h','Jc'], dealt face up
1853 closed likewise, but known only to player
1854 """
1855 log.debug("addPlayerCards %s, o%s x%s", player, open, closed)
1856 self.checkPlayerExists(player, "addPlayerCards")
1857 self.holecards[street][player] = (open, closed)
1859 # TODO: def addComplete(self, player, amount):
1860 def addComplete(self, street, player, amountTo):
1861 # assert street=='THIRD'
1862 # This needs to be called instead of addRaiseTo, and it needs to take account of self.lastBet['THIRD'] to determine the raise-by size
1863 """\
1864 Add a complete on [street] by [player] to [amountTo]
1865 """
1866 log.debug(("%s %s completes %s"), street, player, amountTo)
1867 amountTo = amountTo.replace(",", "") # some sites have commas
1868 self.checkPlayerExists(player, "addComplete")
1869 Bp = self.lastBet[street]
1870 Bc = sum(self.bets[street][player])
1871 Rt = Decimal(amountTo)
1872 C = Bp - Bc
1873 Rb = Rt - C
1874 self._addRaise(street, player, C, Rb, Rt, "completes")
1876 def addBringIn(self, player, bringin):
1877 if player is not None:
1878 if self.gametype["category"] == "5_studhi":
1879 street = "SECOND"
1880 else:
1881 street = "THIRD"
1882 log.debug(("Bringin: %s, %s"), player, bringin)
1883 bringin = bringin.replace(",", "") # some sites have commas
1884 self.checkPlayerExists(player, "addBringIn")
1885 bringin = Decimal(bringin)
1886 self.bets[street][player].append(bringin)
1887 self.stacks[player] -= bringin
1888 act = (player, "bringin", bringin, self.stacks[player] == 0)
1889 self.actions[street].append(act)
1890 self.lastBet[street] = bringin
1891 self.pot.addMoney(player, bringin)
1893 def writeHand(self, fh=sys.__stdout__):
1894 # PokerStars format.
1895 # HH output should not be translated
1896 super(StudHand, self).writeHand(fh)
1898 players_who_post_antes = set([x[0] for x in self.actions["BLINDSANTES"]])
1900 for player in [x for x in self.players if x[1] in players_who_post_antes]:
1901 # Only print stacks of players who do something preflop
1902 print(("Seat %s: %s (%s%s in chips)" % (player[0], player[1], self.sym, player[2])), file=fh)
1904 if "BLINDSANTES" in self.actions:
1905 for act in self.actions["BLINDSANTES"]:
1906 print(("%s: posts the ante %s%s" % (act[0], self.sym, act[3])), file=fh)
1908 if "THIRD" in self.actions:
1909 dealt = 0
1910 for player in [x[1] for x in self.players if x[1] in players_who_post_antes]:
1911 if player in self.holecards["THIRD"]:
1912 dealt += 1
1913 if dealt == 1:
1914 print(("*** 3RD STREET ***"), file=fh)
1915 print(self.writeHoleCards("THIRD", player), file=fh)
1916 for act in self.actions["THIRD"]:
1917 # FIXME: Need some logic here for bringin vs completes
1918 print(self.actionString(act), file=fh)
1920 if "FOURTH" in self.actions:
1921 dealt = 0
1922 for player in [x[1] for x in self.players if x[1] in players_who_post_antes]:
1923 if player in self.holecards["FOURTH"]:
1924 dealt += 1
1925 if dealt == 1:
1926 print(("*** 4TH STREET ***"), file=fh)
1927 print(self.writeHoleCards("FOURTH", player), file=fh)
1928 for act in self.actions["FOURTH"]:
1929 print(self.actionString(act), file=fh)
1931 if "FIFTH" in self.actions:
1932 dealt = 0
1933 for player in [x[1] for x in self.players if x[1] in players_who_post_antes]:
1934 if player in self.holecards["FIFTH"]:
1935 dealt += 1
1936 if dealt == 1:
1937 print(("*** 5TH STREET ***"), file=fh)
1938 print(self.writeHoleCards("FIFTH", player), file=fh)
1939 for act in self.actions["FIFTH"]:
1940 print(self.actionString(act), file=fh)
1942 if "SIXTH" in self.actions:
1943 dealt = 0
1944 for player in [x[1] for x in self.players if x[1] in players_who_post_antes]:
1945 if player in self.holecards["SIXTH"]:
1946 dealt += 1
1947 if dealt == 1:
1948 print(("*** 6TH STREET ***"), file=fh)
1949 print(self.writeHoleCards("SIXTH", player), file=fh)
1950 for act in self.actions["SIXTH"]:
1951 print(self.actionString(act), file=fh)
1953 if "SEVENTH" in self.actions:
1954 # OK. It's possible that they're all in at an earlier street, but only closed cards are dealt.
1955 # Then we have no 'dealt to' lines, no action lines, but still 7th street should appear.
1956 # The only way I can see to know whether to print this line is by knowing the state of the hand
1957 # i.e. are all but one players folded; is there an allin showdown; and all that.
1958 print(("*** RIVER ***"), file=fh)
1959 for player in [x[1] for x in self.players if x[1] in players_who_post_antes]:
1960 if player in self.holecards["SEVENTH"]:
1961 if self.writeHoleCards("SEVENTH", player):
1962 print(self.writeHoleCards("SEVENTH", player), file=fh)
1963 for act in self.actions["SEVENTH"]:
1964 print(self.actionString(act), file=fh)
1966 # Some sites don't have a showdown section so we have to figure out if there should be one
1967 # The logic for a showdown is: at the end of river action there are at least two players in the hand
1968 # we probably don't need a showdown section in pseudo stars format for our filtering purposes
1969 if "SHOWDOWN" in self.actions:
1970 print(("*** SHOW DOWN ***"), file=fh)
1971 # TODO: print showdown lines.
1973 # Current PS format has the lines:
1974 # Uncalled bet ($111.25) returned to s0rrow
1975 # s0rrow collected $5.15 from side pot
1976 # stervels: shows [Ks Qs] (two pair, Kings and Queens)
1977 # stervels collected $45.35 from main pot
1978 # Immediately before the summary.
1979 # The current importer uses those lines for importing winning rather than the summary
1980 for name in self.pot.returned:
1981 print(("Uncalled bet (%s%s) returned to %s" % (self.sym, self.pot.returned[name], name)), file=fh)
1982 for entry in self.collected:
1983 print(("%s collected %s%s from pot" % (entry[0], self.sym, entry[1])), file=fh)
1985 print(("*** SUMMARY ***"), file=fh)
1986 print("%s | Rake %s%.2f" % (self.pot, self.sym, self.rake), file=fh)
1987 # TODO: side pots
1989 board = []
1990 for s in list(self.board.values()):
1991 board += s
1992 if board: # sometimes hand ends preflop without a board
1993 print(("Board [%s]" % (" ".join(board))), file=fh)
1995 for player in [x for x in self.players if x[1] in players_who_post_antes]:
1996 seatnum = player[0]
1997 name = player[1]
1998 if name in self.collectees and name in self.shown:
1999 print(
2000 (
2001 "Seat %d: %s showed [%s] and won (%s%s)"
2002 % (seatnum, name, self.join_holecards(name), self.sym, self.collectees[name])
2003 ),
2004 file=fh,
2005 )
2006 elif name in self.collectees:
2007 print(("Seat %d: %s collected (%s%s)" % (seatnum, name, self.sym, self.collectees[name])), file=fh)
2008 elif name in self.shown:
2009 print(("Seat %d: %s showed [%s]" % (seatnum, name, self.join_holecards(name))), file=fh)
2010 elif name in self.mucked:
2011 print(("Seat %d: %s mucked [%s]" % (seatnum, name, self.join_holecards(name))), file=fh)
2012 elif name in self.folded:
2013 print(("Seat %d: %s folded" % (seatnum, name)), file=fh)
2014 else:
2015 print(("Seat %d: %s mucked" % (seatnum, name)), file=fh)
2017 print("\n\n", file=fh)
2019 def writeHoleCards(self, street, player):
2020 hc = "Dealt to %s [" % player
2021 if street == "THIRD":
2022 if player == self.hero:
2023 return (
2024 hc
2025 + " ".join(self.holecards[street][player][1])
2026 + " "
2027 + " ".join(self.holecards[street][player][0])
2028 + "]"
2029 )
2030 else:
2031 return hc + " ".join(self.holecards[street][player][0]) + "]"
2033 if street == "SEVENTH" and player != self.hero:
2034 return # only write 7th st line for hero, LDO
2035 return (
2036 hc + " ".join(self.holecards[street][player][1]) + "] [" + " ".join(self.holecards[street][player][0]) + "]"
2037 )
2039 def join_holecards(self, player, asList=False):
2040 """Function returns a string for the stud writeHand method by default
2041 With asList = True it returns the set cards for a player including down cards if they aren't know"""
2042 holecards = []
2043 for street in self.holeStreets:
2044 if player in self.holecards[street]:
2045 if (self.gametype["category"] == "5_studhi" and street == "SECOND") or (
2046 self.gametype["category"] != "5_studhi" and street == "THIRD"
2047 ):
2048 holecards = holecards + self.holecards[street][player][1] + self.holecards[street][player][0]
2049 elif street == "SEVENTH":
2050 if player == self.hero:
2051 holecards = holecards + self.holecards[street][player][0]
2052 else:
2053 holecards = holecards + self.holecards[street][player][1]
2054 else:
2055 holecards = holecards + self.holecards[street][player][0]
2057 if asList:
2058 if self.gametype["category"] == "5_studhi":
2059 if len(holecards) < 2:
2060 holecards = ["0x"] + holecards
2061 return holecards
2062 else:
2063 if player == self.hero:
2064 if len(holecards) < 3:
2065 holecards = ["0x", "0x"] + holecards
2066 else:
2067 return holecards
2068 elif len(holecards) == 7:
2069 return holecards
2070 elif len(holecards) <= 4:
2071 # Non hero folded before showdown, add first two downcards
2072 holecards = ["0x", "0x"] + holecards
2073 else:
2074 log.warning(
2075 (
2076 "join_holecards: # of holecards should be either < 4, 4 or 7 - 5 and 6 should be impossible for anyone who is not a hero"
2077 )
2078 )
2079 log.warning("join_holecards: holecards(%s): %s", player, holecards)
2080 if holecards == ["0x", "0x"]:
2081 log.warning(("join_holecards: Player '%s' appears not to have been dealt a card"), player)
2082 # If a player is listed but not dealt a card in a cash game this can occur
2083 # Noticed in FTP Razz hand. Return 3 empty cards in this case
2084 holecards = ["0x", "0x", "0x"]
2085 return holecards
2086 else:
2087 return " ".join(holecards)
2090class Pot(object):
2091 def __init__(self):
2092 self.contenders = set()
2093 self.committed = {}
2094 self.streettotals = {}
2095 self.common = {}
2096 self.antes = {}
2097 self.total = None
2098 self.returned = {}
2099 self.sym = "$" # this is the default currency symbol
2100 self.pots = []
2101 self.handid = 0
2102 self.stp = 0
2104 def setSym(self, sym):
2105 self.sym = sym
2107 def addPlayer(self, player):
2108 self.committed[player] = Decimal(0)
2109 self.common[player] = Decimal(0)
2110 self.antes[player] = Decimal(0)
2112 def removePlayer(self, player):
2113 del self.committed[player]
2114 del self.common[player]
2115 del self.antes[player]
2117 def addFold(self, player):
2118 # addFold must be called when a player folds
2119 self.contenders.discard(player)
2121 def addCommonMoney(self, player, amount):
2122 self.common[player] += amount
2124 def addAntes(self, player, amount):
2125 self.antes[player] += amount
2127 def addMoney(self, player, amount):
2128 # addMoney must be called for any actions that put money in the pot, in the order they occur
2129 self.contenders.add(player)
2130 self.committed[player] += amount
2132 def removeMoney(self, player, amount):
2133 self.committed[player] -= amount
2134 self.returned[player] = amount
2136 def setSTP(self, amount):
2137 self.stp = amount
2139 def markTotal(self, street):
2140 self.streettotals[street] = sum(self.committed.values()) + sum(self.common.values()) + self.stp
2142 def getTotalAtStreet(self, street):
2143 if street in self.streettotals:
2144 return self.streettotals[street]
2145 return 0
2147 def end(self):
2148 # Initialize
2149 log.debug("Starting pot calculation...")
2151 self.total = sum(self.committed.values()) + sum(self.common.values()) + self.stp
2153 # Calculating secondary pots
2154 commitsall = sorted([(v, k) for (k, v) in self.committed.items() if v > 0])
2155 try:
2156 while len(commitsall) > 0:
2157 # Filter players still in the running
2158 commitslive = [(v, k) for (v, k) in commitsall if k in self.contenders]
2159 v1 = commitslive[0][0]
2160 # Create a new secondary pot
2161 self.pots += [
2162 (
2163 sum([min(v, v1) for (v, k) in commitsall]),
2164 set(k for (v, k) in commitsall if k in self.contenders),
2165 )
2166 ]
2167 # Updates the remaining bets
2168 commitsall = [((v - v1), k) for (v, k) in commitsall if v - v1 > 0]
2169 except IndexError as e:
2170 log.error("Pot.end(): Major failure while calculating pot: %s", e)
2171 raise FpdbParseError("Error in pot calculation during side pots handling")
2173 # TODO: Gestion du rake (commission)
2174 # Explication de la gestion du rake
2175 # total pot x. main pot y, side pot z. | rake r
2176 # et y+z+r = x
2177 # Exemple:
2178 # Total pot $124.30 Main pot $98.90. Side pot $23.40. | Rake $2
2180 def __str__(self):
2181 if self.sym is None:
2182 self.sym = "C"
2183 if self.total is None:
2184 # NB if I'm sure end() is idempotent, call it here.
2185 log.error(("Error in printing Hand object"))
2186 raise FpdbParseError
2188 ret = "Total pot %s%.2f" % (self.sym, self.total)
2189 if len(self.pots) < 2:
2190 return ret
2191 ret += " Main pot %s%.2f" % (self.sym, self.pots[0][0])
2192 return ret + "".join([(" Side pot %s%.2f." % (self.sym, self.pots[x][0])) for x in range(1, len(self.pots))])
2195def hand_factory(hand_id, config, db_connection):
2196 # a factory function to discover the base type of the hand
2197 # and to return a populated class instance of the correct hand
2199 log.debug(f"get info from db for hand {hand_id}")
2200 gameinfo = db_connection.get_gameinfo_from_hid(hand_id)
2202 if gameinfo is None:
2203 log.error(f"No game info found for hand ID {hand_id}")
2204 return None # Return None or handle the error appropriately
2206 log.debug(f"gameinfo {gameinfo} for hand {hand_id}")
2208 if gameinfo["base"] == "hold":
2209 hand_instance = HoldemOmahaHand(
2210 config=config,
2211 hhc=None,
2212 sitename=gameinfo["sitename"],
2213 gametype=gameinfo,
2214 handText=None,
2215 builtFrom="DB",
2216 handid=hand_id,
2217 )
2218 elif gameinfo["base"] == "stud":
2219 hand_instance = StudHand(
2220 config=config,
2221 hhc=None,
2222 sitename=gameinfo["sitename"],
2223 gametype=gameinfo,
2224 handText=None,
2225 builtFrom="DB",
2226 handid=hand_id,
2227 )
2228 elif gameinfo["base"] == "draw":
2229 hand_instance = DrawHand(
2230 config=config,
2231 hhc=None,
2232 sitename=gameinfo["sitename"],
2233 gametype=gameinfo,
2234 handText=None,
2235 builtFrom="DB",
2236 handid=hand_id,
2237 )
2238 else:
2239 log.error(f"Unknown game base type: {gameinfo['base']} for hand {hand_id}")
2240 return None # Handle unexpected game types
2242 log.debug(f"selecting info from db for hand {hand_id}")
2243 hand_instance.select(db_connection, hand_id)
2244 hand_instance.handid_selected = hand_id # hand_instance does not supply this, create it here
2245 log.debug(f"exiting hand_factory for hand {hand_id}")
2247 return hand_instance