Coverage for HUD_main.pyw: 78%

378 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-18 00:10 +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 

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

274 found = False 

275 

276 enabled_sites = self.config.get_supported_sites() 

277 if not enabled_sites: 

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

279 self.db_connection.connection.rollback() 

280 self.destroy() 

281 return 

282 

283 aux_disabled_sites = [] 

284 for i in enabled_sites: 

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

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

287 aux_disabled_sites.append(i) 

288 

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

290 

291 if not found: 

292 for site in enabled_sites: 

293 log.debug("not found ... site in enabled_site") 

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

295 site_id = result[0][0] 

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

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

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

299 found = True 

300 else: 

301 self.hero_ids[site_id] = -1 

302 

303 if new_hand_id != "": 

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

305 if new_hand_id in self.cache: 

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

307 table_info = self.cache[new_hand_id] 

308 else: 

309 log.debug(f"Data not found in cache for hand_id: {new_hand_id}") 

310 try: 

311 table_info = self.db_connection.get_table_info(new_hand_id) 

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

313 except Exception as e: 

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

315 return 

316 

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

318 

319 if fast: 

320 return 

321 

322 if site_name in aux_disabled_sites: 

323 return 

324 if site_name not in enabled_sites: 

325 return 

326 

327 # Generating the temporary key 

328 if type == "tour": 

329 try: 

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

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

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

333 # else: 

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

335 

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

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

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

339 except ValueError: 

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

341 else: 

342 temp_key = table_name 

343 

344 # Managing table changes for tournaments 

345 if type == "tour": 

346 if temp_key in self.hud_dict: 

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

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

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

350 return 

351 else: 

352 for k in self.hud_dict: 

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

354 if k.startswith(tour_number): 

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

356 continue 

357 

358 # Detection of max_seats and poker_game changes 

359 if temp_key in self.hud_dict: 

360 with contextlib.suppress(Exception): 

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

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

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

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

365 self.kill_hud("activate", temp_key) 

366 while temp_key in self.hud_dict: 

367 time.sleep(0.5) 

368 max = newmax 

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

370 

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

372 with contextlib.suppress(Exception): 

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

374 self.kill_hud("activate", temp_key) 

375 while temp_key in self.hud_dict: 

376 time.sleep(0.5) 

377 

378 # Updating or creating the HUD 

379 if temp_key in self.hud_dict: 

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

381 self.db_connection.init_hud_stat_vars( 

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

383 ) 

384 stat_dict = self.db_connection.get_stats_from_hand( 

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

386 ) 

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

388 

389 try: 

390 self.hud_dict[temp_key].stat_dict = stat_dict 

391 except KeyError: 

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

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

394 return 

395 

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

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

398 aw.update_data(new_hand_id, self.db_connection) 

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

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

401 else: 

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

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

404 stat_dict = self.db_connection.get_stats_from_hand( 

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

406 ) 

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

408 

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

410 if not hero_found: 

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

412 return 

413 

414 cards = self.get_cards(new_hand_id, poker_game) 

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

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

417 if tablewindow.number is None: 

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

419 if type == "tour": 

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

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

422 return 

423 elif tablewindow.number in self.blacklist: 

424 return 

425 else: 

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

427 tablewindow.key = temp_key 

428 tablewindow.max = max 

429 tablewindow.site = site_name 

430 if hasattr(tablewindow, "number"): 

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

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

433 else: 

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

435 return 

436 

437 def get_cards(self, new_hand_id, poker_game): 

438 cards = self.db_connection.get_cards(new_hand_id) 

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

440 comm_cards = self.db_connection.get_common_cards(new_hand_id) 

441 cards["common"] = comm_cards["common"] 

442 return cards 

443 

444 def idle_move(self, hud): 

445 try: 

446 hud.move_table_position() 

447 for aw in hud.aux_windows: 

448 aw.move_windows() 

449 except Exception: 

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

451 

452 def idle_resize(self, hud): 

453 try: 

454 hud.resize_windows() 

455 for aw in hud.aux_windows: 

456 aw.resize_windows() 

457 except Exception: 

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

459 

460 def idle_kill(self, table): 

461 try: 

462 if table in self.hud_dict: 

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

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

465 self.hud_dict[table].kill() 

466 del self.hud_dict[table] 

467 self.main_window.resize(1, 1) 

468 except Exception: 

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

470 

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

472 try: 

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

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

475 self.vb.addWidget(newlabel) 

476 

477 self.hud_dict[temp_key].tablehudlabel = newlabel 

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

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

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

481 m.create() 

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

483 m.update_gui(new_hand_id) 

484 

485 except Exception: 

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

487 

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

489 try: 

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

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

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

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

494 aw.update_gui(new_hand_id) 

495 except Exception: 

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

497 

498 

499if __name__ == "__main__": 

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

501 

502 app = QApplication([]) 

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

504 

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

506 

507 app.exec_()