Coverage for GuiTourHandViewer.py: 0%
278 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-14 11:07 +0000
1from functools import partial
2import Hand
3import Card
4import Configuration
5import Database
6import SQL
7import Filters
8import Deck
10from PyQt5.QtCore import QCoreApplication, QSortFilterProxyModel, Qt
11from PyQt5.QtGui import QPainter, QPixmap, QStandardItem, QStandardItemModel
12from PyQt5.QtWidgets import (
13 QApplication,
14 QFrame,
15 QMenu,
16 QProgressDialog,
17 QScrollArea,
18 QSplitter,
19 QTableView,
20 QVBoxLayout,
21)
23from io import StringIO
24import GuiReplayer
27class GuiHandViewer(QSplitter):
28 def __init__(self, config, querylist, mainwin):
29 QSplitter.__init__(self, mainwin)
30 self.config = config
31 self.main_window = mainwin
32 self.sql = querylist
33 self.replayer = None
35 self.db = Database.Database(self.config, sql=self.sql)
37 self.setup_filters()
39 scroll = QScrollArea()
40 scroll.setWidget(self.filters)
42 self.handsFrame = QFrame()
43 self.handsVBox = QVBoxLayout()
44 self.handsFrame.setLayout(self.handsVBox)
46 self.addWidget(scroll)
47 self.addWidget(self.handsFrame)
48 self.setStretchFactor(0, 0)
49 self.setStretchFactor(1, 1)
51 self.deck_instance = Deck.Deck(self.config, height=42, width=30)
52 self.cardImages = self.init_card_images()
54 # !Dict of colnames and their column idx in the model/ListStore
55 self.colnum = {
56 "Stakes": 0,
57 "Players": 1,
58 "Pos": 2,
59 "Street0": 3,
60 "Action0": 4,
61 "Street1-4": 5,
62 "Action1-4": 6,
63 "Won": 7,
64 "Bet": 8,
65 "Net": 9,
66 "Game": 10,
67 "HandId": 11,
68 "Total Pot": 12,
69 "Rake": 13,
70 "SiteHandNo": 14,
71 }
72 self.view = QTableView()
73 self.view.setSelectionBehavior(QTableView.SelectRows)
74 self.handsVBox.addWidget(self.view)
75 self.model = QStandardItemModel(0, len(self.colnum), self.view)
76 self.filterModel = QSortFilterProxyModel()
77 self.filterModel.setSourceModel(self.model)
78 self.filterModel.setSortRole(Qt.UserRole)
80 self.view.setModel(self.filterModel)
81 self.view.verticalHeader().hide()
82 self.model.setHorizontalHeaderLabels(
83 [
84 "Stakes",
85 "Nb Players",
86 "Position",
87 "Hands",
88 "Preflop Action",
89 "Board",
90 "Postflop Action",
91 "Won",
92 "Bet",
93 "Net",
94 "Game",
95 "HandId",
96 "Total Pot",
97 "Rake",
98 "SiteHandId",
99 ]
100 )
102 self.view.doubleClicked.connect(self.row_activated)
103 self.view.contextMenuEvent = self.contextMenu
104 self.filterModel.rowsInserted.connect(
105 lambda index, start, end: [self.view.resizeRowToContents(r) for r in range(start, end + 1)]
106 )
107 self.filterModel.filterAcceptsRow = lambda row, sourceParent: self.is_row_in_card_filter(row)
109 self.view.resizeColumnsToContents()
110 self.view.setSortingEnabled(True)
112 def setup_filters(self):
113 filters_display = {
114 "Heroes": True,
115 "Sites": True,
116 "Games": True,
117 "Currencies": False,
118 "Limits": True,
119 "LimitSep": True,
120 "LimitType": True,
121 "Positions": True,
122 "Type": True,
123 "Seats": False,
124 "SeatSep": False,
125 "Dates": True,
126 "Cards": False,
127 "Groups": False,
128 "GroupsAll": False,
129 "Button1": True,
130 "Button2": False,
131 }
132 self.filters = Filters.Filters(self.db, display=filters_display)
133 self.filters.registerButton1Name("Load Hands")
134 self.filters.registerButton1Callback(self.loadHands)
135 self.filters.registerCardsCallback(self.filter_cards_cb)
137 # update games for default hero and site
138 heroes = self.filters.getHeroes()
139 sites = self.filters.getSites()
141 default_hero = next(iter(heroes.values())) if heroes else None
142 default_site = next(iter(sites)) if sites else None
144 if default_hero and default_site:
145 self.filters.update_games_for_hero(default_hero, default_site)
147 def init_card_images(self):
148 suits = ("s", "h", "d", "c")
149 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
151 card_images = [0] * 53
152 for j in range(0, 13):
153 for i in range(0, 4):
154 loc = Card.cardFromValueSuit(ranks[j], suits[i])
155 card_image = self.deck_instance.card(suits[i], ranks[j])
156 card_images[loc] = card_image
157 back_image = self.deck_instance.back()
158 card_images[0] = back_image
159 return card_images
161 def loadHands(self, checkState):
162 hand_ids = self.get_hand_ids_from_date_range(self.filters.getDates()[0], self.filters.getDates()[1])
163 self.reload_hands(hand_ids)
165 def get_hand_ids_from_date_range(self, start, end):
166 q = """
167 SELECT DISTINCT h.id, h.startTime, tt.buyin, tt.fee, p.name, tt.siteId, tt.category
168 FROM Hands h
169 JOIN Tourneys t ON h.tourneyId = t.id
170 JOIN TourneyTypes tt ON t.tourneyTypeId = tt.id
171 JOIN TourneysPlayers tp ON t.id = tp.tourneyId
172 JOIN Players p ON tp.playerId = p.id
173 WHERE h.startTime BETWEEN ? AND ?
174 """
176 hero_filter = self.filters.getHeroes()
177 if hero_filter:
178 hero_names = ", ".join(f"'{h}'" for h in hero_filter.values())
179 q += f" AND p.name IN ({hero_names})"
181 site_filter = self.filters.getSites()
182 if site_filter:
183 site_ids = ", ".join(str(self.filters.siteid[s]) for s in site_filter)
184 q += f" AND tt.siteId IN ({site_ids})"
186 category_filter = self.filters.getGames()
187 if category_filter:
188 categories = ", ".join(f"'{c}'" for c in category_filter)
189 q += f" AND tt.category IN ({categories})"
191 selected_buyins = self.filters.getBuyIn()
192 if selected_buyins:
193 buyins_str = ", ".join(map(str, selected_buyins))
194 q += f" AND (tt.buyin + tt.fee) IN ({buyins_str})"
196 # print(f"Buy-ins sélectionnés (incluant les frais) : {selected_buyins}")
198 # print("Requête SQL filtrée :", q)
200 c = self.db.get_cursor()
201 c.execute(q, (start, end))
202 results = c.fetchall()
203 for row in results[:10]: # show 10 first results
204 print(row)
205 return [r[0] for r in results]
207 def rankedhand(self, hand, game):
208 ranks = {
209 "0": 0,
210 "2": 2,
211 "3": 3,
212 "4": 4,
213 "5": 5,
214 "6": 6,
215 "7": 7,
216 "8": 8,
217 "9": 9,
218 "T": 10,
219 "J": 11,
220 "Q": 12,
221 "K": 13,
222 "A": 14,
223 }
224 suits = {"x": 0, "s": 1, "c": 2, "d": 3, "h": 4}
226 if game == "holdem":
227 card1 = ranks[hand[0]]
228 card2 = ranks[hand[3]]
229 suit1 = suits[hand[1]]
230 suit2 = suits[hand[4]]
231 if card1 < card2:
232 (card1, card2) = (card2, card1)
233 (suit1, suit2) = (suit2, suit1)
234 if suit1 == suit2:
235 suit1 += 4
236 return card1 * 14 * 14 + card2 * 14 + suit1
237 else:
238 return 0
240 def reload_hands(self, handids):
241 self.hands = {}
242 self.model.removeRows(0, self.model.rowCount())
243 if len(handids) == 0:
244 return
245 progress = QProgressDialog("Loading hands", "Abort", 0, len(handids), self)
246 progress.setValue(0)
247 progress.show()
248 for idx, handid in enumerate(handids):
249 if progress.wasCanceled():
250 break
251 self.hands[handid] = self.importhand(handid)
252 self.addHandRow(handid, self.hands[handid])
254 progress.setValue(idx + 1)
255 if idx % 10 == 0:
256 QCoreApplication.processEvents()
257 self.view.resizeColumnsToContents()
258 self.view.resizeColumnsToContents()
260 def addHandRow(self, handid, hand):
261 hero = self.filters.getHeroes()[hand.sitename]
262 won = 0
263 nbplayers = len(hand.players)
264 if hero in list(hand.collectees.keys()):
265 won = hand.collectees[hero]
266 bet = 0
267 if hero in list(hand.pot.committed.keys()):
268 bet = hand.pot.committed[hero]
269 net = won - bet
270 pos = hand.get_player_position(hero)
271 gt = hand.gametype["category"]
272 row = []
273 totalpot = hand.totalpot
274 rake = hand.rake
275 sitehandid = hand.handid
276 if hand.gametype["base"] == "hold":
277 board = []
278 board.extend(hand.board["FLOP"])
279 board.extend(hand.board["TURN"])
280 board.extend(hand.board["RIVER"])
282 pre_actions = hand.get_actions_short(hero, "PREFLOP")
283 post_actions = ""
284 if "F" not in pre_actions: # if player hasen't folded preflop
285 post_actions = hand.get_actions_short_streets(hero, "FLOP", "TURN", "RIVER")
287 row = [
288 hand.getStakesAsString(),
289 str(nbplayers),
290 pos,
291 hand.join_holecards(hero),
292 pre_actions,
293 " ".join(board),
294 post_actions,
295 str(won),
296 str(bet),
297 str(net),
298 gt,
299 str(handid),
300 str(totalpot),
301 str(rake),
302 str(sitehandid),
303 ]
304 elif hand.gametype["base"] == "stud":
305 third = " ".join(hand.holecards["THIRD"][hero][0]) + " " + " ".join(hand.holecards["THIRD"][hero][1])
306 # ugh - fix the stud join_holecards function so we can retrieve sanely
307 later_streets = []
308 later_streets.extend(hand.holecards["FOURTH"][hero][0])
309 later_streets.extend(hand.holecards["FIFTH"][hero][0])
310 later_streets.extend(hand.holecards["SIXTH"][hero][0])
311 later_streets.extend(hand.holecards["SEVENTH"][hero][0])
313 pre_actions = hand.get_actions_short(hero, "THIRD")
314 post_actions = ""
315 if "F" not in pre_actions:
316 post_actions = hand.get_actions_short_streets(hero, "FOURTH", "FIFTH", "SIXTH", "SEVENTH")
318 row = [
319 hand.getStakesAsString(),
320 str(nbplayers),
321 pos,
322 third,
323 pre_actions,
324 " ".join(later_streets),
325 post_actions,
326 str(won),
327 str(bet),
328 str(net),
329 gt,
330 str(handid),
331 str(totalpot),
332 str(rake),
333 ]
334 elif hand.gametype["base"] == "draw":
335 row = [
336 hand.getStakesAsString(),
337 str(nbplayers),
338 pos,
339 hand.join_holecards(hero, street="DEAL"),
340 hand.get_actions_short(hero, "DEAL"),
341 None,
342 None,
343 str(won),
344 str(bet),
345 str(net),
346 gt,
347 str(handid),
348 str(totalpot),
349 str(rake),
350 ]
352 modelrow = [QStandardItem(r) for r in row]
353 for index, item in enumerate(modelrow):
354 item.setEditable(False)
355 if index in (self.colnum["Street0"], self.colnum["Street1-4"]):
356 cards = item.data(Qt.DisplayRole)
357 item.setData(self.render_cards(cards), Qt.DecorationRole)
358 item.setData("", Qt.DisplayRole)
359 item.setData(cards, Qt.UserRole + 1)
360 if index in (self.colnum["Bet"], self.colnum["Net"], self.colnum["Won"]):
361 item.setData(float(item.data(Qt.DisplayRole)), Qt.UserRole)
362 self.model.appendRow(modelrow)
364 def copyHandToClipboard(self, checkState, hand):
365 handText = StringIO()
366 hand.writeHand(handText)
367 QApplication.clipboard().setText(handText.getvalue())
369 def contextMenu(self, event):
370 index = self.view.currentIndex()
371 if index.row() < 0:
372 return
373 hand = self.hands[int(index.sibling(index.row(), self.colnum["HandId"]).data())]
374 m = QMenu()
375 copyAction = m.addAction("Copy to clipboard")
376 copyAction.triggered.connect(partial(self.copyHandToClipboard, hand=hand))
377 m.move(event.globalPos())
378 m.exec_()
380 def filter_cards_cb(self, card):
381 if hasattr(self, "hands"):
382 self.filterModel.invalidateFilter()
384 def is_row_in_card_filter(self, rownum):
385 """Returns true if the cards of the given row are in the card filter"""
386 # Does work but all cards that should NOT be displayed have to be clicked.
387 card_filter = self.filters.getCards()
388 hcs = self.model.data(self.model.index(rownum, self.colnum["Street0"]), Qt.UserRole + 1).split(" ")
390 if "0x" in hcs: # if cards are unknown return True
391 return True
393 gt = self.model.data(self.model.index(rownum, self.colnum["Game"]))
395 if gt not in ("holdem", "omahahi", "omahahilo"):
396 return True
398 # Holdem: Compare the real start cards to the selected filter (ie. AhKh = AKs)
399 value1 = Card.card_map[hcs[0][0]]
400 value2 = Card.card_map[hcs[1][0]]
401 idx = Card.twoStartCards(value1, hcs[0][1], value2, hcs[1][1])
402 abbr = Card.twoStartCardString(idx)
404 # Debug output to trace unexpected keys
405 if abbr not in card_filter:
406 print(f"Unexpected key in card filter: {abbr}")
408 return card_filter.get(abbr, True) # Default to True if key is not found
410 def row_activated(self, index):
411 handlist = list(sorted(self.hands.keys()))
412 self.replayer = GuiReplayer.GuiReplayer(self.config, self.sql, self.main_window, handlist)
413 index = handlist.index(int(index.sibling(index.row(), self.colnum["HandId"]).data()))
414 self.replayer.play_hand(index)
416 def importhand(self, handid=1):
417 h = Hand.hand_factory(handid, self.config, self.db)
419 # Safely get the hero for this hand's sitename
420 heroes = self.filters.getHeroes()
421 h.hero = heroes.get(h.sitename, None)
422 if h.hero is None:
423 print(f"No hero found for site {h.sitename}")
424 return h
426 def render_cards(self, cardstring):
427 card_width = 30
428 card_height = 42
429 if cardstring is None or cardstring == "":
430 cardstring = "0x"
431 cardstring = cardstring.replace("'", "")
432 cardstring = cardstring.replace("[", "")
433 cardstring = cardstring.replace("]", "")
434 cardstring = cardstring.replace("'", "")
435 cardstring.replace(",", "")
436 cards = [Card.encodeCard(c) for c in cardstring.split(" ")]
437 n_cards = len(cards)
439 pixbuf = QPixmap(card_width * n_cards, card_height)
440 painter = QPainter(pixbuf)
441 x = 0 # x coord where the next card starts in pixbuf
442 for card in cards:
443 painter.drawPixmap(x, 0, self.cardImages[card])
444 x += card_width
445 return pixbuf
448class TourHandViewer(GuiHandViewer):
449 def __init__(self, config, querylist, mainwin):
450 super().__init__(config, querylist, mainwin)
452 def setup_filters(self):
453 filters_display = {
454 "Heroes": True,
455 "Sites": True,
456 "Games": False, # cash game
457 "Tourney": True,
458 "TourneyCat": True,
459 "TourneyLim": True,
460 "TourneyBuyin": True,
461 "Currencies": True,
462 "Limits": False,
463 "LimitSep": True,
464 "LimitType": True,
465 "Type": True,
466 "UseType": "tour",
467 "Seats": False,
468 "SeatSep": True,
469 "Dates": True,
470 "Groups": False,
471 "Button1": True,
472 "Button2": False,
473 }
474 self.filters = Filters.Filters(self.db, display=filters_display)
475 self.filters.registerButton1Name("Load Hands")
476 self.filters.registerButton1Callback(self.loadHands)
477 self.filters.registerCardsCallback(self.filter_cards_cb)
479 # update games for default hero and site
480 heroes = self.filters.getHeroes()
481 sites = self.filters.getSites()
483 default_hero = next(iter(heroes.values())) if heroes else None
484 default_site = next(iter(sites)) if sites else None
486 if default_hero and default_site:
487 self.filters.update_games_for_hero(default_hero, default_site)
490if __name__ == "__main__":
491 config = Configuration.Config()
493 settings = {}
495 settings.update(config.get_db_parameters())
496 settings.update(config.get_import_parameters())
497 settings.update(config.get_default_paths())
499 from PyQt5.QtWidgets import QMainWindow
501 app = QApplication([])
502 sql = SQL.Sql(db_server=settings["db-server"])
503 main_window = QMainWindow()
505 # create tour viewer
506 tour_viewer = TourHandViewer(config, sql, main_window)
508 main_window.setCentralWidget(tour_viewer)
510 main_window.show()
511 main_window.resize(1400, 800)
512 app.exec_()