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