Coverage for GuiHandViewer.py: 0%
238 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-27 18:50 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-27 18:50 +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 (QApplication, QFrame, QMenu,
39 QProgressDialog, QScrollArea, QSplitter,
40 QTableView, QVBoxLayout)
42from io import StringIO
44import GuiReplayer
47class GuiHandViewer(QSplitter):
48 def __init__(self, config, querylist, mainwin):
49 QSplitter.__init__(self, mainwin)
50 self.config = config
51 self.main_window = mainwin
52 self.sql = querylist
53 self.replayer = None
55 self.db = Database.Database(self.config, sql=self.sql)
57 filters_display = {"Heroes": True,
58 "Sites": True,
59 "Games": True,
60 "Currencies": False,
61 "Limits": True,
62 "LimitSep": True,
63 "LimitType": True,
64 "Positions": True,
65 "Type": True,
66 "Seats": False,
67 "SeatSep": False,
68 "Dates": True,
69 "Cards": False,
70 "Groups": False,
71 "GroupsAll": False,
72 "Button1": True,
73 "Button2": False
74 }
76 self.filters = Filters.Filters(self.db, display=filters_display)
77 self.filters.registerButton1Name("Load Hands")
78 self.filters.registerButton1Callback(self.loadHands)
79 self.filters.registerCardsCallback(self.filter_cards_cb)
81 scroll = QScrollArea()
82 scroll.setWidget(self.filters)
84 self.handsFrame = QFrame()
85 self.handsVBox = QVBoxLayout()
86 self.handsFrame.setLayout(self.handsVBox)
88 self.addWidget(scroll)
89 self.addWidget(self.handsFrame)
90 self.setStretchFactor(0, 0)
91 self.setStretchFactor(1, 1)
93 self.deck_instance = Deck.Deck(self.config, height=42, width=30)
94 self.cardImages = self.init_card_images()
96 # !Dict of colnames and their column idx in the model/ListStore
97 self.colnum = {
98 'Stakes': 0,
99 'Players': 1,
100 'Pos': 2,
101 'Street0': 3,
102 'Action0': 4,
103 'Street1-4': 5,
104 'Action1-4': 6,
105 'Won': 7,
106 'Bet': 8,
107 'Net': 9,
108 'Game': 10,
109 'HandId': 11,
110 'Total Pot': 12,
111 'Rake': 13,
112 'SiteHandNo': 14
113 }
114 self.view = QTableView()
115 self.view.setSelectionBehavior(QTableView.SelectRows)
116 self.handsVBox.addWidget(self.view)
117 self.model = QStandardItemModel(0, len(self.colnum), self.view)
118 self.filterModel = QSortFilterProxyModel()
119 self.filterModel.setSourceModel(self.model)
120 self.filterModel.setSortRole(Qt.UserRole)
122 self.view.setModel(self.filterModel)
123 self.view.verticalHeader().hide()
124 self.model.setHorizontalHeaderLabels(
125 ['Stakes', 'Nb Players', 'Position', 'Hands', 'Preflop Action', 'Board', 'Postflop Action',
126 'Won', 'Bet', 'Net', 'Game', 'HandId', 'Total Pot', 'Rake', 'SiteHandId'])
128 self.view.doubleClicked.connect(self.row_activated)
129 self.view.contextMenuEvent = self.contextMenu
130 self.filterModel.rowsInserted.connect(
131 lambda index, start, end: [self.view.resizeRowToContents(r) for r in range(start, end + 1)])
132 self.filterModel.filterAcceptsRow = lambda row, sourceParent: self.is_row_in_card_filter(row)
134 self.view.resizeColumnsToContents()
135 self.view.setSortingEnabled(True)
137 def init_card_images(self):
138 suits = ('s', 'h', 'd', 'c')
139 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
141 card_images = [0] * 53
142 for j in range(0, 13):
143 for i in range(0, 4):
144 loc = Card.cardFromValueSuit(ranks[j], suits[i])
145 card_image = self.deck_instance.card(suits[i], ranks[j])
146 card_images[loc] = card_image
147 back_image = self.deck_instance.back()
148 card_images[0] = back_image
149 return card_images
151 def loadHands(self, checkState):
152 hand_ids = self.get_hand_ids_from_date_range(self.filters.getDates()[0], self.filters.getDates()[1])
153 # ! print(hand_ids)
154 self.reload_hands(hand_ids)
156 def get_hand_ids_from_date_range(self, start, end):
157 q = self.db.sql.query['handsInRangeSessionFilter']
158 q = q.replace('<datetest>', "between '" + start + "' and '" + end + "'")
160 # Apply filters
161 q = self.filters.replace_placeholders_with_filter_values(q)
163 # debug
164 #print("Requête SQL filtrée :", q)
166 c = self.db.get_cursor()
167 c.execute(q)
168 return [r[0] for r in c.fetchall()]
173 def rankedhand(self, hand, game):
174 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,
175 'K': 13, 'A': 14}
176 suits = {'x': 0, 's': 1, 'c': 2, 'd': 3, 'h': 4}
178 if game == 'holdem':
179 card1 = ranks[hand[0]]
180 card2 = ranks[hand[3]]
181 suit1 = suits[hand[1]]
182 suit2 = suits[hand[4]]
183 if card1 < card2:
184 (card1, card2) = (card2, card1)
185 (suit1, suit2) = (suit2, suit1)
186 if suit1 == suit2:
187 suit1 += 4
188 return card1 * 14 * 14 + card2 * 14 + suit1
189 else:
190 return 0
192 def reload_hands(self, handids):
193 self.hands = {}
194 self.model.removeRows(0, self.model.rowCount())
195 if len(handids) == 0:
196 return
197 progress = QProgressDialog("Loading hands", "Abort", 0, len(handids), self)
198 progress.setValue(0)
199 progress.show()
200 for idx, handid in enumerate(handids):
201 if progress.wasCanceled():
202 break
203 self.hands[handid] = self.importhand(handid)
204 self.addHandRow(handid, self.hands[handid])
206 progress.setValue(idx + 1)
207 if idx % 10 == 0:
208 QCoreApplication.processEvents()
209 self.view.resizeColumnsToContents()
210 self.view.resizeColumnsToContents()
212 def addHandRow(self, handid, hand):
213 hero = self.filters.getHeroes()[hand.sitename]
214 won = 0
215 nbplayers = len(hand.players)
216 # ! print("max seaT: ", hand.maxseats)
217 if hero in list(hand.collectees.keys()):
218 won = hand.collectees[hero]
219 bet = 0
220 if hero in list(hand.pot.committed.keys()):
221 bet = hand.pot.committed[hero]
222 net = won - bet
223 pos = hand.get_player_position(hero)
224 gt = hand.gametype['category']
225 row = []
226 totalpot = hand.totalpot
227 rake = hand.rake
228 sitehandid = hand.handid
229 if hand.gametype['base'] == 'hold':
230 board = []
231 board.extend(hand.board['FLOP'])
232 board.extend(hand.board['TURN'])
233 board.extend(hand.board['RIVER'])
235 pre_actions = hand.get_actions_short(hero, 'PREFLOP')
236 post_actions = ''
237 if 'F' not in pre_actions: # if player hasen't folded preflop
238 post_actions = hand.get_actions_short_streets(hero, 'FLOP', 'TURN', 'RIVER')
240 row = [hand.getStakesAsString(), str(nbplayers), pos, hand.join_holecards(hero), pre_actions, ' '.join(board), post_actions,
241 str(won), str(bet),
242 str(net), gt, str(handid), str(totalpot), str(rake), str(sitehandid)]
243 elif hand.gametype['base'] == 'stud':
244 third = " ".join(hand.holecards['THIRD'][hero][0]) + " " + " ".join(hand.holecards['THIRD'][hero][1])
245 # ugh - fix the stud join_holecards function so we can retrieve sanely
246 later_streets = []
247 later_streets.extend(hand.holecards['FOURTH'][hero][0])
248 later_streets.extend(hand.holecards['FIFTH'][hero][0])
249 later_streets.extend(hand.holecards['SIXTH'][hero][0])
250 later_streets.extend(hand.holecards['SEVENTH'][hero][0])
252 pre_actions = hand.get_actions_short(hero, 'THIRD')
253 post_actions = ''
254 if 'F' not in pre_actions:
255 post_actions = hand.get_actions_short_streets(hero, 'FOURTH', 'FIFTH', 'SIXTH', 'SEVENTH')
257 row = [hand.getStakesAsString(), str(nbplayers), pos, third, pre_actions, ' '.join(later_streets), post_actions, str(won),
258 str(bet), str(net),
259 gt, str(handid), str(totalpot), str(rake)]
260 elif hand.gametype['base'] == 'draw':
261 row = [hand.getStakesAsString(), str(nbplayers), pos, hand.join_holecards(hero, street='DEAL'),
262 hand.get_actions_short(hero, 'DEAL'), None, None,
263 str(won), str(bet), str(net), gt, str(handid), str(totalpot), str(rake)]
265 modelrow = [QStandardItem(r) for r in row]
266 for index, item in enumerate(modelrow):
267 item.setEditable(False)
268 if index in (self.colnum['Street0'], self.colnum['Street1-4']):
269 cards = item.data(Qt.DisplayRole)
270 item.setData(self.render_cards(cards), Qt.DecorationRole)
271 item.setData("", Qt.DisplayRole)
272 item.setData(cards, Qt.UserRole + 1)
273 if index in (self.colnum['Bet'], self.colnum['Net'], self.colnum['Won']):
274 item.setData(float(item.data(Qt.DisplayRole)), Qt.UserRole)
275 self.model.appendRow(modelrow)
277 def copyHandToClipboard(self, checkState, hand):
278 handText = StringIO()
279 hand.writeHand(handText)
280 QApplication.clipboard().setText(handText.getvalue())
282 def contextMenu(self, event):
283 index = self.view.currentIndex()
284 if index.row() < 0:
285 return
286 hand = self.hands[int(index.sibling(index.row(), self.colnum['HandId']).data())]
287 m = QMenu()
288 copyAction = m.addAction('Copy to clipboard')
289 copyAction.triggered.connect(partial(self.copyHandToClipboard, hand=hand))
290 m.move(event.globalPos())
291 m.exec_()
293 def filter_cards_cb(self, card):
294 if hasattr(self, 'hands'):
295 self.filterModel.invalidateFilter()
297 def is_row_in_card_filter(self, rownum):
298 """ Returns true if the cards of the given row are in the card filter """
299 # Does work but all cards that should NOT be displayed have to be clicked.
300 card_filter = self.filters.getCards()
301 hcs = self.model.data(self.model.index(rownum, self.colnum['Street0']), Qt.UserRole + 1).split(' ')
303 if '0x' in hcs: # if cards are unknown return True
304 return True
306 gt = self.model.data(self.model.index(rownum, self.colnum['Game']))
308 if gt not in ('holdem', 'omahahi', 'omahahilo'):
309 return True
311 # Holdem: Compare the real start cards to the selected filter (ie. AhKh = AKs)
312 value1 = Card.card_map[hcs[0][0]]
313 value2 = Card.card_map[hcs[1][0]]
314 idx = Card.twoStartCards(value1, hcs[0][1], value2, hcs[1][1])
315 abbr = Card.twoStartCardString(idx)
317 # Debug output to trace unexpected keys
318 if abbr not in card_filter:
319 print(f"Unexpected key in card filter: {abbr}")
321 return card_filter.get(abbr, True) # Default to True if key is not found
324 def row_activated(self, index):
325 handlist = list(sorted(self.hands.keys()))
326 # ! print('handlist:')
327 # ! print(handlist)
328 self.replayer = GuiReplayer.GuiReplayer(self.config, self.sql, self.main_window, handlist)
329 index = handlist.index(int(index.sibling(index.row(), self.colnum['HandId']).data()))
330 # ! print('index:')
331 # ! print(index)
332 self.replayer.play_hand(index)
334 def importhand(self, handid=1):
335 h = Hand.hand_factory(handid, self.config, self.db)
337 # Safely get the hero for this hand's sitename
338 heroes = self.filters.getHeroes()
339 h.hero = heroes.get(h.sitename, None)
340 if h.hero is None:
341 print(f"No hero found for site {h.sitename}")
342 return h
344 def render_cards(self, cardstring):
345 card_width = 30
346 card_height = 42
347 if cardstring is None or cardstring == '':
348 cardstring = "0x"
349 cardstring = cardstring.replace("'", "")
350 cardstring = cardstring.replace("[", "")
351 cardstring = cardstring.replace("]", "")
352 cardstring = cardstring.replace("'", "")
353 cardstring = cardstring.replace(",", "")
354 cards = [Card.encodeCard(c) for c in cardstring.split(' ')]
355 n_cards = len(cards)
357 pixbuf = QPixmap(card_width * n_cards, card_height)
358 painter = QPainter(pixbuf)
359 x = 0 # x coord where the next card starts in pixbuf
360 for card in cards:
361 painter.drawPixmap(x, 0, self.cardImages[card])
362 x += card_width
363 return pixbuf
366if __name__ == "__main__":
367 config = Configuration.Config()
369 settings = {}
371 settings.update(config.get_db_parameters())
372 settings.update(config.get_import_parameters())
373 settings.update(config.get_default_paths())
375 from PyQt5.QtWidgets import QMainWindow
377 app = QApplication([])
378 sql = SQL.Sql(db_server=settings['db-server'])
379 main_window = QMainWindow()
380 i = GuiHandViewer(config, sql, main_window)
381 main_window.setCentralWidget(i)
382 main_window.show()
383 main_window.resize(1400, 800)
384 app.exec_()