Coverage for GuiHandViewer.py: 0%

238 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-28 16:41 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3 

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# 

18 

19 

20# This code once was in GuiReplayer.py and was split up in this and the former by zarturo. 

21 

22 

23# import L10n 

24# _ = L10n.get_translation() 

25 

26from functools import partial 

27 

28import Hand 

29import Card 

30import Configuration 

31import Database 

32import SQL 

33import Filters 

34import Deck 

35 

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) 

41 

42from io import StringIO 

43 

44import GuiReplayer 

45 

46 

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 

54 

55 self.db = Database.Database(self.config, sql=self.sql) 

56 

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 } 

75 

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) 

80 

81 scroll = QScrollArea() 

82 scroll.setWidget(self.filters) 

83 

84 self.handsFrame = QFrame() 

85 self.handsVBox = QVBoxLayout() 

86 self.handsFrame.setLayout(self.handsVBox) 

87 

88 self.addWidget(scroll) 

89 self.addWidget(self.handsFrame) 

90 self.setStretchFactor(0, 0) 

91 self.setStretchFactor(1, 1) 

92 

93 self.deck_instance = Deck.Deck(self.config, height=42, width=30) 

94 self.cardImages = self.init_card_images() 

95 

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) 

121 

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']) 

127 

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) 

133 

134 self.view.resizeColumnsToContents() 

135 self.view.setSortingEnabled(True) 

136 

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) 

140 

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 

150 

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) 

155 

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 + "'") 

159 

160 # Apply filters 

161 q = self.filters.replace_placeholders_with_filter_values(q) 

162 

163 # debug 

164 #print("Requête SQL filtrée :", q) 

165 

166 c = self.db.get_cursor() 

167 c.execute(q) 

168 return [r[0] for r in c.fetchall()] 

169 

170 

171 

172 

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} 

177 

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 

191 

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]) 

205 

206 progress.setValue(idx + 1) 

207 if idx % 10 == 0: 

208 QCoreApplication.processEvents() 

209 self.view.resizeColumnsToContents() 

210 self.view.resizeColumnsToContents() 

211 

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']) 

234 

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') 

239 

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]) 

251 

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') 

256 

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)] 

264 

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) 

276 

277 def copyHandToClipboard(self, checkState, hand): 

278 handText = StringIO() 

279 hand.writeHand(handText) 

280 QApplication.clipboard().setText(handText.getvalue()) 

281 

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_() 

292 

293 def filter_cards_cb(self, card): 

294 if hasattr(self, 'hands'): 

295 self.filterModel.invalidateFilter() 

296 

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(' ') 

302 

303 if '0x' in hcs: # if cards are unknown return True 

304 return True 

305 

306 gt = self.model.data(self.model.index(rownum, self.colnum['Game'])) 

307 

308 if gt not in ('holdem', 'omahahi', 'omahahilo'): 

309 return True 

310 

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) 

316 

317 # Debug output to trace unexpected keys 

318 if abbr not in card_filter: 

319 print(f"Unexpected key in card filter: {abbr}") 

320 

321 return card_filter.get(abbr, True) # Default to True if key is not found 

322 

323 

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) 

333 

334 def importhand(self, handid=1): 

335 h = Hand.hand_factory(handid, self.config, self.db) 

336 

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 

343 

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) 

356 

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 

364 

365 

366if __name__ == "__main__": 

367 config = Configuration.Config() 

368 

369 settings = {} 

370 

371 settings.update(config.get_db_parameters()) 

372 settings.update(config.get_import_parameters()) 

373 settings.update(config.get_default_paths()) 

374 

375 from PyQt5.QtWidgets import QMainWindow 

376 

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_()