Coverage for GuiHandViewer.py: 0%
238 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
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
4# Copyright 2010-2011 Maxime Grandchamp
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.
17#
20# This code once was in GuiReplayer.py and was split up in this and the former by zarturo.
23# import L10n
24# _ = L10n.get_translation()
26from functools import partial
28import Hand
29import Card
30import Configuration
31import Database
32import SQL
33import Filters
34import Deck
36from PyQt5.QtCore import QCoreApplication, QSortFilterProxyModel, Qt
37from PyQt5.QtGui import QPainter, QPixmap, QStandardItem, QStandardItemModel
38from PyQt5.QtWidgets import (
39 QApplication,
40 QFrame,
41 QMenu,
42 QProgressDialog,
43 QScrollArea,
44 QSplitter,
45 QTableView,
46 QVBoxLayout,
47)
49from io import StringIO
51import GuiReplayer
54class GuiHandViewer(QSplitter):
55 def __init__(self, config, querylist, mainwin):
56 QSplitter.__init__(self, mainwin)
57 self.config = config
58 self.main_window = mainwin
59 self.sql = querylist
60 self.replayer = None
62 self.db = Database.Database(self.config, sql=self.sql)
64 filters_display = {
65 "Heroes": True,
66 "Sites": True,
67 "Games": True,
68 "Currencies": False,
69 "Limits": True,
70 "LimitSep": True,
71 "LimitType": True,
72 "Positions": True,
73 "Type": True,
74 "Seats": False,
75 "SeatSep": False,
76 "Dates": True,
77 "Cards": False,
78 "Groups": False,
79 "GroupsAll": False,
80 "Button1": True,
81 "Button2": False,
82 }
84 self.filters = Filters.Filters(self.db, display=filters_display)
85 self.filters.registerButton1Name("Load Hands")
86 self.filters.registerButton1Callback(self.loadHands)
87 self.filters.registerCardsCallback(self.filter_cards_cb)
89 scroll = QScrollArea()
90 scroll.setWidget(self.filters)
92 self.handsFrame = QFrame()
93 self.handsVBox = QVBoxLayout()
94 self.handsFrame.setLayout(self.handsVBox)
96 self.addWidget(scroll)
97 self.addWidget(self.handsFrame)
98 self.setStretchFactor(0, 0)
99 self.setStretchFactor(1, 1)
101 self.deck_instance = Deck.Deck(self.config, height=42, width=30)
102 self.cardImages = self.init_card_images()
104 # !Dict of colnames and their column idx in the model/ListStore
105 self.colnum = {
106 "Stakes": 0,
107 "Players": 1,
108 "Pos": 2,
109 "Street0": 3,
110 "Action0": 4,
111 "Street1-4": 5,
112 "Action1-4": 6,
113 "Won": 7,
114 "Bet": 8,
115 "Net": 9,
116 "Game": 10,
117 "HandId": 11,
118 "Total Pot": 12,
119 "Rake": 13,
120 "SiteHandNo": 14,
121 }
122 self.view = QTableView()
123 self.view.setSelectionBehavior(QTableView.SelectRows)
124 self.handsVBox.addWidget(self.view)
125 self.model = QStandardItemModel(0, len(self.colnum), self.view)
126 self.filterModel = QSortFilterProxyModel()
127 self.filterModel.setSourceModel(self.model)
128 self.filterModel.setSortRole(Qt.UserRole)
130 self.view.setModel(self.filterModel)
131 self.view.verticalHeader().hide()
132 self.model.setHorizontalHeaderLabels(
133 [
134 "Stakes",
135 "Nb Players",
136 "Position",
137 "Hands",
138 "Preflop Action",
139 "Board",
140 "Postflop Action",
141 "Won",
142 "Bet",
143 "Net",
144 "Game",
145 "HandId",
146 "Total Pot",
147 "Rake",
148 "SiteHandId",
149 ]
150 )
152 self.view.doubleClicked.connect(self.row_activated)
153 self.view.contextMenuEvent = self.contextMenu
154 self.filterModel.rowsInserted.connect(
155 lambda index, start, end: [self.view.resizeRowToContents(r) for r in range(start, end + 1)]
156 )
157 self.filterModel.filterAcceptsRow = lambda row, sourceParent: self.is_row_in_card_filter(row)
159 self.view.resizeColumnsToContents()
160 self.view.setSortingEnabled(True)
162 def init_card_images(self):
163 suits = ("s", "h", "d", "c")
164 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
166 card_images = [0] * 53
167 for j in range(0, 13):
168 for i in range(0, 4):
169 loc = Card.cardFromValueSuit(ranks[j], suits[i])
170 card_image = self.deck_instance.card(suits[i], ranks[j])
171 card_images[loc] = card_image
172 back_image = self.deck_instance.back()
173 card_images[0] = back_image
174 return card_images
176 def loadHands(self, checkState):
177 hand_ids = self.get_hand_ids_from_date_range(self.filters.getDates()[0], self.filters.getDates()[1])
178 # ! print(hand_ids)
179 self.reload_hands(hand_ids)
181 def get_hand_ids_from_date_range(self, start, end):
182 q = self.db.sql.query["handsInRangeSessionFilter"]
183 q = q.replace("<datetest>", "between '" + start + "' and '" + end + "'")
185 # Apply filters
186 q = self.filters.replace_placeholders_with_filter_values(q)
188 # debug
189 # print("Requête SQL filtrée :", q)
191 c = self.db.get_cursor()
192 c.execute(q)
193 return [r[0] for r in c.fetchall()]
195 def rankedhand(self, hand, game):
196 ranks = {
197 "0": 0,
198 "2": 2,
199 "3": 3,
200 "4": 4,
201 "5": 5,
202 "6": 6,
203 "7": 7,
204 "8": 8,
205 "9": 9,
206 "T": 10,
207 "J": 11,
208 "Q": 12,
209 "K": 13,
210 "A": 14,
211 }
212 suits = {"x": 0, "s": 1, "c": 2, "d": 3, "h": 4}
214 if game == "holdem":
215 card1 = ranks[hand[0]]
216 card2 = ranks[hand[3]]
217 suit1 = suits[hand[1]]
218 suit2 = suits[hand[4]]
219 if card1 < card2:
220 (card1, card2) = (card2, card1)
221 (suit1, suit2) = (suit2, suit1)
222 if suit1 == suit2:
223 suit1 += 4
224 return card1 * 14 * 14 + card2 * 14 + suit1
225 else:
226 return 0
228 def reload_hands(self, handids):
229 self.hands = {}
230 self.model.removeRows(0, self.model.rowCount())
231 if len(handids) == 0:
232 return
233 progress = QProgressDialog("Loading hands", "Abort", 0, len(handids), self)
234 progress.setValue(0)
235 progress.show()
236 for idx, handid in enumerate(handids):
237 if progress.wasCanceled():
238 break
239 self.hands[handid] = self.importhand(handid)
240 self.addHandRow(handid, self.hands[handid])
242 progress.setValue(idx + 1)
243 if idx % 10 == 0:
244 QCoreApplication.processEvents()
245 self.view.resizeColumnsToContents()
246 self.view.resizeColumnsToContents()
248 def addHandRow(self, handid, hand):
249 hero = self.filters.getHeroes()[hand.sitename]
250 won = 0
251 nbplayers = len(hand.players)
252 # ! print("max seaT: ", hand.maxseats)
253 if hero in list(hand.collectees.keys()):
254 won = hand.collectees[hero]
255 bet = 0
256 if hero in list(hand.pot.committed.keys()):
257 bet = hand.pot.committed[hero]
258 net = won - bet
259 pos = hand.get_player_position(hero)
260 gt = hand.gametype["category"]
261 row = []
262 totalpot = hand.totalpot
263 rake = hand.rake
264 sitehandid = hand.handid
265 if hand.gametype["base"] == "hold":
266 board = []
267 board.extend(hand.board["FLOP"])
268 board.extend(hand.board["TURN"])
269 board.extend(hand.board["RIVER"])
271 pre_actions = hand.get_actions_short(hero, "PREFLOP")
272 post_actions = ""
273 if "F" not in pre_actions: # if player hasen't folded preflop
274 post_actions = hand.get_actions_short_streets(hero, "FLOP", "TURN", "RIVER")
276 row = [
277 hand.getStakesAsString(),
278 str(nbplayers),
279 pos,
280 hand.join_holecards(hero),
281 pre_actions,
282 " ".join(board),
283 post_actions,
284 str(won),
285 str(bet),
286 str(net),
287 gt,
288 str(handid),
289 str(totalpot),
290 str(rake),
291 str(sitehandid),
292 ]
293 elif hand.gametype["base"] == "stud":
294 third = " ".join(hand.holecards["THIRD"][hero][0]) + " " + " ".join(hand.holecards["THIRD"][hero][1])
295 # ugh - fix the stud join_holecards function so we can retrieve sanely
296 later_streets = []
297 later_streets.extend(hand.holecards["FOURTH"][hero][0])
298 later_streets.extend(hand.holecards["FIFTH"][hero][0])
299 later_streets.extend(hand.holecards["SIXTH"][hero][0])
300 later_streets.extend(hand.holecards["SEVENTH"][hero][0])
302 pre_actions = hand.get_actions_short(hero, "THIRD")
303 post_actions = ""
304 if "F" not in pre_actions:
305 post_actions = hand.get_actions_short_streets(hero, "FOURTH", "FIFTH", "SIXTH", "SEVENTH")
307 row = [
308 hand.getStakesAsString(),
309 str(nbplayers),
310 pos,
311 third,
312 pre_actions,
313 " ".join(later_streets),
314 post_actions,
315 str(won),
316 str(bet),
317 str(net),
318 gt,
319 str(handid),
320 str(totalpot),
321 str(rake),
322 ]
323 elif hand.gametype["base"] == "draw":
324 row = [
325 hand.getStakesAsString(),
326 str(nbplayers),
327 pos,
328 hand.join_holecards(hero, street="DEAL"),
329 hand.get_actions_short(hero, "DEAL"),
330 None,
331 None,
332 str(won),
333 str(bet),
334 str(net),
335 gt,
336 str(handid),
337 str(totalpot),
338 str(rake),
339 ]
341 modelrow = [QStandardItem(r) for r in row]
342 for index, item in enumerate(modelrow):
343 item.setEditable(False)
344 if index in (self.colnum["Street0"], self.colnum["Street1-4"]):
345 cards = item.data(Qt.DisplayRole)
346 item.setData(self.render_cards(cards), Qt.DecorationRole)
347 item.setData("", Qt.DisplayRole)
348 item.setData(cards, Qt.UserRole + 1)
349 if index in (self.colnum["Bet"], self.colnum["Net"], self.colnum["Won"]):
350 item.setData(float(item.data(Qt.DisplayRole)), Qt.UserRole)
351 self.model.appendRow(modelrow)
353 def copyHandToClipboard(self, checkState, hand):
354 handText = StringIO()
355 hand.writeHand(handText)
356 QApplication.clipboard().setText(handText.getvalue())
358 def contextMenu(self, event):
359 index = self.view.currentIndex()
360 if index.row() < 0:
361 return
362 hand = self.hands[int(index.sibling(index.row(), self.colnum["HandId"]).data())]
363 m = QMenu()
364 copyAction = m.addAction("Copy to clipboard")
365 copyAction.triggered.connect(partial(self.copyHandToClipboard, hand=hand))
366 m.move(event.globalPos())
367 m.exec_()
369 def filter_cards_cb(self, card):
370 if hasattr(self, "hands"):
371 self.filterModel.invalidateFilter()
373 def is_row_in_card_filter(self, rownum):
374 """Returns true if the cards of the given row are in the card filter"""
375 # Does work but all cards that should NOT be displayed have to be clicked.
376 card_filter = self.filters.getCards()
377 hcs = self.model.data(self.model.index(rownum, self.colnum["Street0"]), Qt.UserRole + 1).split(" ")
379 if "0x" in hcs: # if cards are unknown return True
380 return True
382 gt = self.model.data(self.model.index(rownum, self.colnum["Game"]))
384 if gt not in ("holdem", "omahahi", "omahahilo"):
385 return True
387 # Holdem: Compare the real start cards to the selected filter (ie. AhKh = AKs)
388 value1 = Card.card_map[hcs[0][0]]
389 value2 = Card.card_map[hcs[1][0]]
390 idx = Card.twoStartCards(value1, hcs[0][1], value2, hcs[1][1])
391 abbr = Card.twoStartCardString(idx)
393 # Debug output to trace unexpected keys
394 if abbr not in card_filter:
395 print(f"Unexpected key in card filter: {abbr}")
397 return card_filter.get(abbr, True) # Default to True if key is not found
399 def row_activated(self, index):
400 handlist = list(sorted(self.hands.keys()))
401 # ! print('handlist:')
402 # ! print(handlist)
403 self.replayer = GuiReplayer.GuiReplayer(self.config, self.sql, self.main_window, handlist)
404 index = handlist.index(int(index.sibling(index.row(), self.colnum["HandId"]).data()))
405 # ! print('index:')
406 # ! print(index)
407 self.replayer.play_hand(index)
409 def importhand(self, handid=1):
410 h = Hand.hand_factory(handid, self.config, self.db)
412 # Safely get the hero for this hand's sitename
413 heroes = self.filters.getHeroes()
414 h.hero = heroes.get(h.sitename, None)
415 if h.hero is None:
416 print(f"No hero found for site {h.sitename}")
417 return h
419 def render_cards(self, cardstring):
420 card_width = 30
421 card_height = 42
422 if cardstring is None or cardstring == "":
423 cardstring = "0x"
424 cardstring = cardstring.replace("'", "")
425 cardstring = cardstring.replace("[", "")
426 cardstring = cardstring.replace("]", "")
427 cardstring = cardstring.replace("'", "")
428 cardstring = cardstring.replace(",", "")
429 cards = [Card.encodeCard(c) for c in cardstring.split(" ")]
430 n_cards = len(cards)
432 pixbuf = QPixmap(card_width * n_cards, card_height)
433 painter = QPainter(pixbuf)
434 x = 0 # x coord where the next card starts in pixbuf
435 for card in cards:
436 painter.drawPixmap(x, 0, self.cardImages[card])
437 x += card_width
438 return pixbuf
441if __name__ == "__main__":
442 config = Configuration.Config()
444 settings = {}
446 settings.update(config.get_db_parameters())
447 settings.update(config.get_import_parameters())
448 settings.update(config.get_default_paths())
450 from PyQt5.QtWidgets import QMainWindow
452 app = QApplication([])
453 sql = SQL.Sql(db_server=settings["db-server"])
454 main_window = QMainWindow()
455 i = GuiHandViewer(config, sql, main_window)
456 main_window.setCentralWidget(i)
457 main_window.show()
458 main_window.resize(1400, 800)
459 app.exec_()