Coverage for HUD_main.pyw: 78%

384 statements  

« 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 -*- 

3"""Hud_main.py 

4 

5Main for FreePokerTools HUD. 

6""" 

7 

8import codecs 

9import contextlib 

10import sys 

11import os 

12import time 

13import logging 

14import zmq 

15from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, Qt, QTimer 

16from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget 

17from PyQt5.QtGui import QIcon 

18from qt_material import apply_stylesheet 

19 

20import Configuration 

21import Database 

22import Hud 

23import Options 

24import Deck 

25 

26# Add a cache for frequently accessed data 

27from cachetools import TTLCache 

28 

29# Logging configuration 

30logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") 

31log = logging.getLogger("hud") 

32 

33 

34class ZMQWorker(QThread): 

35 error_occurred = pyqtSignal(str) 

36 

37 def __init__(self, zmq_receiver): 

38 super().__init__() 

39 self.zmq_receiver = zmq_receiver 

40 self.is_running = True 

41 

42 def run(self): 

43 while self.is_running: 

44 try: 

45 self.zmq_receiver.process_message() 

46 except Exception as e: 

47 log.error(f"Error in ZMQWorker: {e}") 

48 self.error_occurred.emit(str(e)) 

49 time.sleep(0.01) # Short delay to avoid excessive CPU usage 

50 

51 def stop(self): 

52 self.is_running = False 

53 self.wait() 

54 

55 

56class ZMQReceiver(QObject): 

57 message_received = pyqtSignal(str) 

58 

59 def __init__(self, port="5555", parent=None): 

60 super().__init__(parent) 

61 self.context = zmq.Context() 

62 self.socket = self.context.socket(zmq.PULL) 

63 self.socket.connect(f"tcp://127.0.0.1:{port}") 

64 log.info(f"ZMQ receiver connected on port {port}") 

65 

66 # Heartbeat configuration 

67 self.poller = zmq.Poller() 

68 self.poller.register(self.socket, zmq.POLLIN) 

69 

70 def process_message(self): 

71 try: 

72 socks = dict(self.poller.poll(1000)) # Timeout 1 seconde 

73 if self.socket in socks and socks[self.socket] == zmq.POLLIN: 

74 hand_id = self.socket.recv_string(zmq.NOBLOCK) 

75 log.debug(f"Received hand ID: {hand_id}") 

76 self.message_received.emit(hand_id) 

77 else: 

78 # Heartbeat 

79 log.debug("Heartbeat: No message received") 

80 except zmq.ZMQError as e: 

81 if e.errno == zmq.EAGAIN: 

82 pass # No message available 

83 else: 

84 log.error(f"ZMQ error: {e}") 

85 

86 def close(self): 

87 self.socket.close() 

88 self.context.term() 

89 log.info("ZMQ receiver closed") 

90 

91 

92class HUD_main(QObject): 

93 """A main() object to own both the socket thread and the gui.""" 

94 

95 def __init__(self, options, db_name="fpdb"): 

96 self.options = options 

97 QObject.__init__(self) 

98 self.db_name = db_name 

99 Configuration.set_logfile("HUD-log.txt") 

100 self.config = Configuration.Config(file=options.config, dbname=options.dbname) 

101 

102 # Selecting the right module for the OS 

103 if self.config.os_family == "Linux": 

104 import XTables as Tables 

105 elif self.config.os_family == "Mac": 

106 import OSXTables as Tables 

107 elif self.config.os_family in ("XP", "Win7"): 

108 import WinTables as Tables 

109 log.info(f"HUD_main starting: Using db name = {db_name}") 

110 self.Tables = Tables # Assign Tables to self.Tables 

111 

112 # Configuration du logging 

113 if not options.errorsToConsole: 

114 fileName = os.path.join(self.config.dir_log, "HUD-errors.txt") 

115 log.info(f"Note: error output is being diverted to {fileName}.") 

116 log.info("Any major error will be reported there *only*.") 

117 errorFile = codecs.open(fileName, "w", "utf-8") 

118 sys.stderr = errorFile 

119 log.info("HUD_main starting") 

120 

121 try: 

122 # Connecting to the database 

123 self.db_connection = Database.Database(self.config) 

124 

125 # HUD dictionary and parameters 

126 self.hud_dict = {} 

127 self.blacklist = [] 

128 self.hud_params = self.config.get_hud_ui_parameters() 

129 self.deck = Deck.Deck( 

130 self.config, 

131 deck_type=self.hud_params["deck_type"], 

132 card_back=self.hud_params["card_back"], 

133 width=self.hud_params["card_wd"], 

134 height=self.hud_params["card_ht"], 

135 ) 

136 

137 # Cache initialization 

138 self.cache = TTLCache(maxsize=1000, ttl=300) # Cache of 1000 elements with a TTL of 5 minutes 

139 

140 # Initialisation ZMQ avec QThread 

141 self.zmq_receiver = ZMQReceiver(parent=self) 

142 self.zmq_receiver.message_received.connect(self.handle_message) 

143 self.zmq_worker = ZMQWorker(self.zmq_receiver) 

144 self.zmq_worker.error_occurred.connect(self.handle_worker_error) 

145 self.zmq_worker.start() 

146 

147 # Main window 

148 self.init_main_window() 

149 

150 log.debug("Main window initialized and shown.") 

151 except Exception as e: 

152 log.error(f"Error during HUD_main initialization: {e}") 

153 raise 

154 

155 def handle_worker_error(self, error_message): 

156 log.error(f"ZMQWorker encountered an error: {error_message}") 

157 

158 def init_main_window(self): 

159 self.main_window = QWidget(None, Qt.Dialog | Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint) 

160 if self.options.xloc is not None or self.options.yloc is not None: 

161 x = int(self.options.xloc) if self.options.xloc is not None else self.main_window.x() 

162 y = int(self.options.yloc) if self.options.yloc is not None else self.main_window.y() 

163 self.main_window.move(x, y) 

164 self.main_window.destroyed.connect(self.destroy) 

165 self.vb = QVBoxLayout() 

166 self.vb.setContentsMargins(2, 0, 2, 0) 

167 self.main_window.setLayout(self.vb) 

168 self.label = QLabel("Closing this window will exit from the HUD.") 

169 self.main_window.closeEvent = self.close_event_handler 

170 self.vb.addWidget(self.label) 

171 self.main_window.setWindowTitle("HUD Main Window") 

172 cards = os.path.join(self.config.graphics_path, "tribal.jpg") 

173 if cards: 

174 self.main_window.setWindowIcon(QIcon(cards)) 

175 

176 # Timer for periodically checking tables 

177 self.check_tables_timer = QTimer(self) 

178 self.check_tables_timer.timeout.connect(self.check_tables) 

179 self.check_tables_timer.start(800) 

180 self.main_window.show() 

181 

182 def close_event_handler(self, event): 

183 self.destroy() 

184 event.accept() 

185 

186 def handle_message(self, hand_id): 

187 # This method will be called in the main thread 

188 self.read_stdin(hand_id) 

189 

190 def destroy(self, *args): 

191 if hasattr(self, "zmq_receiver"): 

192 self.zmq_receiver.close() 

193 if hasattr(self, "zmq_worker"): 

194 self.zmq_worker.stop() 

195 log.info("Quitting normally") 

196 QCoreApplication.quit() 

197 

198 def check_tables(self): 

199 if len(self.hud_dict) == 0: 

200 log.info("Waiting for hands ...") 

201 for tablename, hud in list(self.hud_dict.items()): 

202 status = hud.table.check_table() 

203 if status == "client_destroyed": 

204 self.client_destroyed(None, hud) 

205 elif status == "client_moved": 

206 self.client_moved(None, hud) 

207 elif status == "client_resized": 

208 self.client_resized(None, hud) 

209 

210 if self.config.os_family == "Mac": 

211 for hud in self.hud_dict.values(): 

212 for aw in hud.aux_windows: 

213 if not hasattr(aw, "m_windows"): 

214 continue 

215 for w in aw.m_windows.values(): 

216 if w.isVisible(): 

217 hud.table.topify(w) 

218 

219 def client_moved(self, widget, hud): 

220 log.debug("Client moved event") 

221 self.idle_move(hud) 

222 

223 def client_resized(self, widget, hud): 

224 log.debug("Client resized event") 

225 self.idle_resize(hud) 

226 

227 def client_destroyed(self, widget, hud): 

228 log.debug("Client destroyed event") 

229 self.kill_hud(None, hud.table.key) 

230 

231 def table_title_changed(self, widget, hud): 

232 log.debug("Table title changed, killing current HUD") 

233 self.kill_hud(None, hud.table.key) 

234 

235 def table_is_stale(self, hud): 

236 log.debug("Moved to a new table, killing current HUD") 

237 self.kill_hud(None, hud.table.key) 

238 

239 def kill_hud(self, event, table): 

240 log.debug("kill_hud event") 

241 self.idle_kill(table) 

242 

243 def blacklist_hud(self, event, table): 

244 log.debug("blacklist_hud event") 

245 self.blacklist.append(self.hud_dict[table].tablenumber) 

246 self.idle_kill(table) 

247 

248 def create_HUD(self, new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards): 

249 log.debug(f"Creating HUD for table {temp_key} and hand {new_hand_id}") 

250 self.hud_dict[temp_key] = Hud.Hud(self, table, max, poker_game, type, self.config) 

251 self.hud_dict[temp_key].table_name = temp_key 

252 self.hud_dict[temp_key].stat_dict = stat_dict 

253 self.hud_dict[temp_key].cards = cards 

254 self.hud_dict[temp_key].max = max 

255 

256 table.hud = self.hud_dict[temp_key] 

257 

258 self.hud_dict[temp_key].hud_params["new_max_seats"] = None # trigger for seat layout change 

259 

260 for aw in self.hud_dict[temp_key].aux_windows: 

261 aw.update_data(new_hand_id, self.db_connection) 

262 

263 self.idle_create(new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards) 

264 log.debug(f"HUD for table {temp_key} created successfully.") 

265 

266 def update_HUD(self, new_hand_id, table_name, config): 

267 log.debug(f"Updating HUD for table {table_name} and hand {new_hand_id}") 

268 self.idle_update(new_hand_id, table_name, config) 

269 

270 def read_stdin(self, new_hand_id): 

271 log.debug(f"Processing new hand id: {new_hand_id}") 

272 print(f"Entering read_stdin with hand_id: {new_hand_id}") 

273 

274 self.hero, self.hero_ids = {}, {} 

275 found = False 

276 

277 enabled_sites = self.config.get_supported_sites() 

278 if not enabled_sites: 

279 log.error("No enabled sites found") 

280 print("No enabled sites found") 

281 self.db_connection.connection.rollback() 

282 self.destroy() 

283 return 

284 

285 aux_disabled_sites = [] 

286 for i in enabled_sites: 

287 if not self.config.get_site_parameters(i)["aux_enabled"]: 

288 log.info(f"Aux disabled for site {i}") 

289 print(f"Aux disabled for site {i}") 

290 aux_disabled_sites.append(i) 

291 

292 self.db_connection.connection.rollback() # Libérer le verrou de l'itération précédente 

293 

294 if not found: 

295 for site in enabled_sites: 

296 print("not found ... site in enabled_site") 

297 if result := self.db_connection.get_site_id(site): 

298 site_id = result[0][0] 

299 self.hero[site_id] = self.config.supported_sites[site].screen_name 

300 self.hero_ids[site_id] = self.db_connection.get_player_id(self.config, site, self.hero[site_id]) 

301 if self.hero_ids[site_id] is not None: 

302 found = True 

303 else: 

304 self.hero_ids[site_id] = -1 

305 

306 if new_hand_id != "": 

307 log.debug("HUD_main.read_stdin: Hand processing starting.") 

308 print("HUD_main.read_stdin: Hand processing starting.") 

309 if new_hand_id in self.cache: 

310 log.debug(f"Using cached data for hand {new_hand_id}") 

311 print(f"Data found in cache for hand_id: {new_hand_id}") 

312 table_info = self.cache[new_hand_id] 

313 else: 

314 print(f"Data not found in cache for hand_id: {new_hand_id}") 

315 try: 

316 table_info = self.db_connection.get_table_info(new_hand_id) 

317 self.cache[new_hand_id] = table_info # Mise en cache des informations 

318 except Exception as e: 

319 log.error(f"Database error while processing hand {new_hand_id}: {e}", exc_info=True) 

320 print("Database error while processing hand") 

321 return 

322 

323 (table_name, max, poker_game, type, fast, site_id, site_name, num_seats, tour_number, tab_number) = table_info 

324 

325 if fast: 

326 return 

327 

328 if site_name in aux_disabled_sites: 

329 return 

330 if site_name not in enabled_sites: 

331 return 

332 

333 # Generating the temporary key 

334 if type == "tour": 

335 try: 

336 log.debug("creating temp_key for tour") 

337 # if len(table_name) >= 2 and table_name[-2].endswith(","): 

338 # parts = table_name.split(",", 1) 

339 # else: 

340 # parts = table_name.split(" ", 1) 

341 

342 tab_number = tab_number.rsplit(" ", 1)[-1] 

343 temp_key = f"{tour_number} Table {tab_number}" 

344 log.debug(f"temp_key {temp_key}") 

345 except ValueError: 

346 log.error("Both tab_number and table_name not working") 

347 else: 

348 temp_key = table_name 

349 

350 # Managing table changes for tournaments 

351 if type == "tour": 

352 if temp_key in self.hud_dict: 

353 if self.hud_dict[temp_key].table.has_table_title_changed(self.hud_dict[temp_key]): 

354 log.debug("table has been renamed") 

355 self.table_is_stale(self.hud_dict[temp_key]) 

356 return 

357 else: 

358 for k in self.hud_dict: 

359 log.debug("check if the tournament number is in the hud_dict under a different table") 

360 if k.startswith(tour_number): 

361 self.table_is_stale(self.hud_dict[k]) 

362 continue 

363 

364 # Detection of max_seats and poker_game changes 

365 if temp_key in self.hud_dict: 

366 with contextlib.suppress(Exception): 

367 newmax = self.hud_dict[temp_key].hud_params["new_max_seats"] 

368 log.debug(f"newmax {newmax}") 

369 if newmax and self.hud_dict[temp_key].max != newmax: 

370 log.debug("going to kill_hud due to max seats change") 

371 self.kill_hud("activate", temp_key) 

372 while temp_key in self.hud_dict: 

373 time.sleep(0.5) 

374 max = newmax 

375 self.hud_dict[temp_key].hud_params["new_max_seats"] = None 

376 

377 if self.hud_dict[temp_key].poker_game != poker_game: 

378 with contextlib.suppress(Exception): 

379 log.debug("going to kill_hud due to poker game change") 

380 self.kill_hud("activate", temp_key) 

381 while temp_key in self.hud_dict: 

382 time.sleep(0.5) 

383 

384 # Updating or creating the HUD 

385 if temp_key in self.hud_dict: 

386 log.debug(f"update hud for hand {new_hand_id}") 

387 self.db_connection.init_hud_stat_vars( 

388 self.hud_dict[temp_key].hud_params["hud_days"], self.hud_dict[temp_key].hud_params["h_hud_days"] 

389 ) 

390 stat_dict = self.db_connection.get_stats_from_hand( 

391 new_hand_id, type, self.hud_dict[temp_key].hud_params, self.hero_ids[site_id], num_seats 

392 ) 

393 log.debug(f"got stats for hand {new_hand_id}") 

394 

395 try: 

396 self.hud_dict[temp_key].stat_dict = stat_dict 

397 except KeyError: 

398 log.error(f"hud_dict[{temp_key}] was not found") 

399 log.error("will not send hand") 

400 return 

401 

402 self.hud_dict[temp_key].cards = self.get_cards(new_hand_id, poker_game) 

403 for aw in self.hud_dict[temp_key].aux_windows: 

404 aw.update_data(new_hand_id, self.db_connection) 

405 self.update_HUD(new_hand_id, temp_key, self.config) 

406 log.debug(f"hud updated for table {temp_key} and hand {new_hand_id}") 

407 else: 

408 log.debug(f"create new hud for hand {new_hand_id}") 

409 self.db_connection.init_hud_stat_vars(self.hud_params["hud_days"], self.hud_params["h_hud_days"]) 

410 stat_dict = self.db_connection.get_stats_from_hand( 

411 new_hand_id, type, self.hud_params, self.hero_ids[site_id], num_seats 

412 ) 

413 log.debug(f"got stats for hand {new_hand_id}") 

414 

415 hero_found = any(stat_dict[key]["screen_name"] == self.hero[site_id] for key in stat_dict) 

416 if not hero_found: 

417 log.info("HUD not created yet, because hero is not seated for this hand") 

418 return 

419 

420 cards = self.get_cards(new_hand_id, poker_game) 

421 table_kwargs = dict(table_name=table_name, tournament=tour_number, table_number=tab_number) 

422 tablewindow = self.Tables.Table(self.config, site_name, **table_kwargs) 

423 if tablewindow.number is None: 

424 log.debug("tablewindow.number is none") 

425 if type == "tour": 

426 table_name = f"{tour_number} {tab_number}" 

427 log.error(f"HUD create: table name {table_name} not found, skipping.") 

428 return 

429 elif tablewindow.number in self.blacklist: 

430 return 

431 else: 

432 log.debug("tablewindow.number is not none") 

433 tablewindow.key = temp_key 

434 tablewindow.max = max 

435 tablewindow.site = site_name 

436 if hasattr(tablewindow, "number"): 

437 log.debug("table window still exists") 

438 self.create_HUD(new_hand_id, tablewindow, temp_key, max, poker_game, type, stat_dict, cards) 

439 else: 

440 log.error(f'Table "{table_name}" no longer exists') 

441 return 

442 

443 def get_cards(self, new_hand_id, poker_game): 

444 cards = self.db_connection.get_cards(new_hand_id) 

445 if poker_game in ["holdem", "omahahi", "omahahilo"]: 

446 comm_cards = self.db_connection.get_common_cards(new_hand_id) 

447 cards["common"] = comm_cards["common"] 

448 return cards 

449 

450 def idle_move(self, hud): 

451 try: 

452 hud.move_table_position() 

453 for aw in hud.aux_windows: 

454 aw.move_windows() 

455 except Exception: 

456 log.exception(f"Error moving HUD for table: {hud.table.title}.") 

457 

458 def idle_resize(self, hud): 

459 try: 

460 hud.resize_windows() 

461 for aw in hud.aux_windows: 

462 aw.resize_windows() 

463 except Exception: 

464 log.exception(f"Error resizing HUD for table: {hud.table.title}.") 

465 

466 def idle_kill(self, table): 

467 try: 

468 if table in self.hud_dict: 

469 self.vb.removeWidget(self.hud_dict[table].tablehudlabel) 

470 self.hud_dict[table].tablehudlabel.setParent(None) 

471 self.hud_dict[table].kill() 

472 del self.hud_dict[table] 

473 self.main_window.resize(1, 1) 

474 except Exception: 

475 log.exception(f"Error killing HUD for table: {table}.") 

476 

477 def idle_create(self, new_hand_id, table, temp_key, max, poker_game, type, stat_dict, cards): 

478 try: 

479 newlabel = QLabel(f"{table.site} - {temp_key}") 

480 log.debug(f"adding label {newlabel.text()}") 

481 self.vb.addWidget(newlabel) 

482 

483 self.hud_dict[temp_key].tablehudlabel = newlabel 

484 self.hud_dict[temp_key].tablenumber = table.number 

485 self.hud_dict[temp_key].create(new_hand_id, self.config, stat_dict) 

486 for m in self.hud_dict[temp_key].aux_windows: 

487 m.create() 

488 log.debug(f"idle_create new_hand_id {new_hand_id}") 

489 m.update_gui(new_hand_id) 

490 

491 except Exception: 

492 log.exception(f"Error creating HUD for hand {new_hand_id}.") 

493 

494 def idle_update(self, new_hand_id, table_name, config): 

495 try: 

496 log.debug(f"idle_update entered for {table_name} {new_hand_id}") 

497 self.hud_dict[table_name].update(new_hand_id, config) 

498 log.debug(f"idle_update update_gui {new_hand_id}") 

499 for aw in self.hud_dict[table_name].aux_windows: 

500 aw.update_gui(new_hand_id) 

501 except Exception: 

502 log.exception(f"Error updating HUD for hand {new_hand_id}.") 

503 

504 

505if __name__ == "__main__": 

506 (options, argv) = Options.fpdb_options() 

507 

508 app = QApplication([]) 

509 apply_stylesheet(app, theme="dark_purple.xml") 

510 

511 hm = HUD_main(options, db_name=options.dbname) 

512 

513 app.exec_()