Coverage for GuiTourHandViewer.py: 0%

278 statements  

« 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 

9 

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) 

22 

23from io import StringIO 

24import GuiReplayer 

25 

26 

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 

34 

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

36 

37 self.setup_filters() 

38 

39 scroll = QScrollArea() 

40 scroll.setWidget(self.filters) 

41 

42 self.handsFrame = QFrame() 

43 self.handsVBox = QVBoxLayout() 

44 self.handsFrame.setLayout(self.handsVBox) 

45 

46 self.addWidget(scroll) 

47 self.addWidget(self.handsFrame) 

48 self.setStretchFactor(0, 0) 

49 self.setStretchFactor(1, 1) 

50 

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

52 self.cardImages = self.init_card_images() 

53 

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) 

79 

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 ) 

101 

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) 

108 

109 self.view.resizeColumnsToContents() 

110 self.view.setSortingEnabled(True) 

111 

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) 

136 

137 # update games for default hero and site 

138 heroes = self.filters.getHeroes() 

139 sites = self.filters.getSites() 

140 

141 default_hero = next(iter(heroes.values())) if heroes else None 

142 default_site = next(iter(sites)) if sites else None 

143 

144 if default_hero and default_site: 

145 self.filters.update_games_for_hero(default_hero, default_site) 

146 

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) 

150 

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 

160 

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) 

164 

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 """ 

175 

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})" 

180 

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})" 

185 

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})" 

190 

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})" 

195 

196 # print(f"Buy-ins sélectionnés (incluant les frais) : {selected_buyins}") 

197 

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

199 

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] 

206 

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} 

225 

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 

239 

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

253 

254 progress.setValue(idx + 1) 

255 if idx % 10 == 0: 

256 QCoreApplication.processEvents() 

257 self.view.resizeColumnsToContents() 

258 self.view.resizeColumnsToContents() 

259 

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

281 

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

286 

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

312 

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

317 

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 ] 

351 

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) 

363 

364 def copyHandToClipboard(self, checkState, hand): 

365 handText = StringIO() 

366 hand.writeHand(handText) 

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

368 

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

379 

380 def filter_cards_cb(self, card): 

381 if hasattr(self, "hands"): 

382 self.filterModel.invalidateFilter() 

383 

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

389 

390 if "0x" in hcs: # if cards are unknown return True 

391 return True 

392 

393 gt = self.model.data(self.model.index(rownum, self.colnum["Game"])) 

394 

395 if gt not in ("holdem", "omahahi", "omahahilo"): 

396 return True 

397 

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) 

403 

404 # Debug output to trace unexpected keys 

405 if abbr not in card_filter: 

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

407 

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

409 

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) 

415 

416 def importhand(self, handid=1): 

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

418 

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 

425 

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) 

438 

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 

446 

447 

448class TourHandViewer(GuiHandViewer): 

449 def __init__(self, config, querylist, mainwin): 

450 super().__init__(config, querylist, mainwin) 

451 

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) 

478 

479 # update games for default hero and site 

480 heroes = self.filters.getHeroes() 

481 sites = self.filters.getSites() 

482 

483 default_hero = next(iter(heroes.values())) if heroes else None 

484 default_site = next(iter(sites)) if sites else None 

485 

486 if default_hero and default_site: 

487 self.filters.update_games_for_hero(default_hero, default_site) 

488 

489 

490if __name__ == "__main__": 

491 config = Configuration.Config() 

492 

493 settings = {} 

494 

495 settings.update(config.get_db_parameters()) 

496 settings.update(config.get_import_parameters()) 

497 settings.update(config.get_default_paths()) 

498 

499 from PyQt5.QtWidgets import QMainWindow 

500 

501 app = QApplication([]) 

502 sql = SQL.Sql(db_server=settings["db-server"]) 

503 main_window = QMainWindow() 

504 

505 # create tour viewer 

506 tour_viewer = TourHandViewer(config, sql, main_window) 

507 

508 main_window.setCentralWidget(tour_viewer) 

509 

510 main_window.show() 

511 main_window.resize(1400, 800) 

512 app.exec_()