Coverage for HUD_main.pyw: 78%

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

34 

35 

36 

37class ZMQWorker(QThread): 

38 error_occurred = pyqtSignal(str) 

39 

40 def __init__(self, zmq_receiver): 

41 super().__init__() 

42 self.zmq_receiver = zmq_receiver 

43 self.is_running = True 

44 

45 def run(self): 

46 while self.is_running: 

47 try: 

48 self.zmq_receiver.process_message() 

49 except Exception as e: 

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

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

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

53 

54 def stop(self): 

55 self.is_running = False 

56 self.wait() 

57 

58 

59class ZMQReceiver(QObject): 

60 message_received = pyqtSignal(str) 

61 

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

63 super().__init__(parent) 

64 self.context = zmq.Context() 

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

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

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

68 

69 # Heartbeat configuration 

70 self.poller = zmq.Poller() 

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

72 

73 def process_message(self): 

74 try: 

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

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

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

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

79 self.message_received.emit(hand_id) 

80 else: 

81 # Heartbeat 

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

83 except zmq.ZMQError as e: 

84 if e.errno == zmq.EAGAIN: 

85 pass # No message available 

86 else: 

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

88 

89 def close(self): 

90 self.socket.close() 

91 self.context.term() 

92 log.info("ZMQ receiver closed") 

93 

94class HUD_main(QObject): 

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

96 def __init__(self, options, db_name='fpdb'): 

97 self.options = options 

98 QObject.__init__(self) 

99 self.db_name = db_name 

100 Configuration.set_logfile(u"HUD-log.txt") 

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

102 

103 # Selecting the right module for the OS 

104 if self.config.os_family == 'Linux': 

105 import XTables as Tables 

106 elif self.config.os_family == 'Mac': 

107 import OSXTables as Tables 

108 elif self.config.os_family in ('XP', 'Win7'): 

109 import WinTables as Tables 

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

111 self.Tables = Tables # Assign Tables to self.Tables 

112 

113 # Configuration du logging 

114 if not options.errorsToConsole: 

115 fileName = os.path.join(self.config.dir_log, u'HUD-errors.txt') 

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

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

118 errorFile = codecs.open(fileName, 'w', 'utf-8') 

119 sys.stderr = errorFile 

120 log.info("HUD_main starting") 

121 

122 try: 

123 # Connecting to the database 

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

125 

126 # HUD dictionary and parameters 

127 self.hud_dict = {} 

128 self.blacklist = [] 

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

130 self.deck = Deck.Deck(self.config, deck_type=self.hud_params["deck_type"], card_back=self.hud_params["card_back"], 

131 width=self.hud_params['card_wd'], height=self.hud_params['card_ht']) 

132 

133 # Cache initialization 

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

135 

136 # Initialisation ZMQ avec QThread 

137 self.zmq_receiver = ZMQReceiver(parent=self) 

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

139 self.zmq_worker = ZMQWorker(self.zmq_receiver) 

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

141 self.zmq_worker.start() 

142 

143 # Main window 

144 self.init_main_window() 

145 

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

147 except Exception as e: 

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

149 raise 

150 

151 def handle_worker_error(self, error_message): 

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

153 

154 def init_main_window(self): 

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

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

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

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

159 self.main_window.move(x, y) 

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

161 self.vb = QVBoxLayout() 

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

163 self.main_window.setLayout(self.vb) 

164 self.label = QLabel('Closing this window will exit from the HUD.') 

165 self.main_window.closeEvent = self.close_event_handler 

166 self.vb.addWidget(self.label) 

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

168 cards = os.path.join(self.config.graphics_path, 'tribal.jpg') 

169 if cards: 

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

171 

172 # Timer for periodically checking tables 

173 self.check_tables_timer = QTimer(self) 

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

175 self.check_tables_timer.start(800) 

176 self.main_window.show() 

177 

178 def close_event_handler(self, event): 

179 self.destroy() 

180 event.accept() 

181 

182 def handle_message(self, hand_id): 

183 # This method will be called in the main thread 

184 self.read_stdin(hand_id) 

185 

186 def destroy(self, *args): 

187 if hasattr(self, 'zmq_receiver'): 

188 self.zmq_receiver.close() 

189 if hasattr(self, 'zmq_worker'): 

190 self.zmq_worker.stop() 

191 log.info("Quitting normally") 

192 QCoreApplication.quit() 

193 

194 def check_tables(self): 

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

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

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

198 status = hud.table.check_table() 

199 if status == "client_destroyed": 

200 self.client_destroyed(None, hud) 

201 elif status == "client_moved": 

202 self.client_moved(None, hud) 

203 elif status == "client_resized": 

204 self.client_resized(None, hud) 

205 

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

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

208 for aw in hud.aux_windows: 

209 if not hasattr(aw, 'm_windows'): 

210 continue 

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

212 if w.isVisible(): 

213 hud.table.topify(w) 

214 

215 def client_moved(self, widget, hud): 

216 log.debug("Client moved event") 

217 self.idle_move(hud) 

218 

219 def client_resized(self, widget, hud): 

220 log.debug("Client resized event") 

221 self.idle_resize(hud) 

222 

223 def client_destroyed(self, widget, hud): 

224 log.debug("Client destroyed event") 

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

226 

227 def table_title_changed(self, widget, hud): 

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

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

230 

231 def table_is_stale(self, hud): 

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

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

234 

235 

236 

237 def kill_hud(self, event, table): 

238 log.debug("kill_hud event") 

239 self.idle_kill(table) 

240 

241 def blacklist_hud(self, event, table): 

242 log.debug("blacklist_hud event") 

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

244 self.idle_kill(table) 

245 

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

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

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

249 self.hud_dict[temp_key].table_name = temp_key 

250 self.hud_dict[temp_key].stat_dict = stat_dict 

251 self.hud_dict[temp_key].cards = cards 

252 self.hud_dict[temp_key].max = max 

253 

254 table.hud = self.hud_dict[temp_key] 

255 

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

257 

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

259 aw.update_data(new_hand_id, self.db_connection) 

260 

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

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

263 

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

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

266 self.idle_update(new_hand_id, table_name, config) 

267 

268 def read_stdin(self, new_hand_id): 

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

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

271 

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 print("No enabled sites found") 

280 self.db_connection.connection.rollback() 

281 self.destroy() 

282 return 

283 

284 aux_disabled_sites = [] 

285 for i in enabled_sites: 

286 if not self.config.get_site_parameters(i)['aux_enabled']: 

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

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

289 aux_disabled_sites.append(i) 

290 

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

292 

293 if not found: 

294 for site in enabled_sites: 

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

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

297 site_id = result[0][0] 

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

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

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

301 found = True 

302 else: 

303 self.hero_ids[site_id] = -1 

304 

305 if new_hand_id != "": 

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

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

308 if new_hand_id in self.cache: 

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

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

311 table_info = self.cache[new_hand_id] 

312 else: 

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

314 try: 

315 table_info = self.db_connection.get_table_info(new_hand_id) 

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

317 except Exception as e: 

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

319 print("Database error while processing hand") 

320 return 

321 

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

323 

324 if fast: 

325 return 

326 

327 if site_name in aux_disabled_sites: 

328 return 

329 if site_name not in enabled_sites: 

330 return 

331 

332 # Generating the temporary key 

333 if type == "tour": 

334 try: 

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

336 if len(table_name) >= 2 and table_name[-2].endswith(','): 

337 parts = table_name.split(',', 1) 

338 else: 

339 parts = table_name.split(' ', 1) 

340 

341 tab_number = tab_number.rsplit(' ', 1)[-1] 

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

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

344 except ValueError: 

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

346 else: 

347 temp_key = table_name 

348 

349 # Managing table changes for tournaments 

350 if type == "tour": 

351 if temp_key in self.hud_dict: 

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

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

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

355 return 

356 else: 

357 for k in self.hud_dict: 

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

359 if k.startswith(tour_number): 

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

361 continue 

362 

363 # Detection of max_seats and poker_game changes 

364 if temp_key in self.hud_dict: 

365 with contextlib.suppress(Exception): 

366 newmax = self.hud_dict[temp_key].hud_params['new_max_seats'] 

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

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

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

370 self.kill_hud("activate", temp_key) 

371 while temp_key in self.hud_dict: time.sleep(0.5) 

372 max = newmax 

373 self.hud_dict[temp_key].hud_params['new_max_seats'] = None 

374 

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

376 with contextlib.suppress(Exception): 

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

378 self.kill_hud("activate", temp_key) 

379 while temp_key in self.hud_dict: time.sleep(0.5) 

380 

381 # Updating or creating the HUD 

382 if temp_key in self.hud_dict: 

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

384 self.db_connection.init_hud_stat_vars(self.hud_dict[temp_key].hud_params['hud_days'], 

385 self.hud_dict[temp_key].hud_params['h_hud_days']) 

386 stat_dict = self.db_connection.get_stats_from_hand(new_hand_id, type, self.hud_dict[temp_key].hud_params, 

387 self.hero_ids[site_id], num_seats) 

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

389 

390 try: 

391 self.hud_dict[temp_key].stat_dict = stat_dict 

392 except KeyError: 

393 log.error(f'hud_dict[{temp_key}] was not found') 

394 log.error('will not send hand') 

395 return 

396 

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

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

399 aw.update_data(new_hand_id, self.db_connection) 

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

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

402 else: 

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

404 self.db_connection.init_hud_stat_vars(self.hud_params['hud_days'], self.hud_params['h_hud_days']) 

405 stat_dict = self.db_connection.get_stats_from_hand(new_hand_id, type, self.hud_params, 

406 self.hero_ids[site_id], num_seats) 

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

408 

409 hero_found = any( 

410 stat_dict[key]['screen_name'] == self.hero[site_id] 

411 for key in stat_dict 

412 ) 

413 if not hero_found: 

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

415 return 

416 

417 cards = self.get_cards(new_hand_id, poker_game) 

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

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

420 if tablewindow.number is None: 

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

422 if type == "tour": 

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

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

425 return 

426 elif tablewindow.number in self.blacklist: 

427 return 

428 else: 

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

430 tablewindow.key = temp_key 

431 tablewindow.max = max 

432 tablewindow.site = site_name 

433 if hasattr(tablewindow, 'number'): 

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

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

436 else: 

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

438 return 

439 

440 def get_cards(self, new_hand_id, poker_game): 

441 cards = self.db_connection.get_cards(new_hand_id) 

442 if poker_game in ['holdem', 'omahahi', 'omahahilo']: 

443 comm_cards = self.db_connection.get_common_cards(new_hand_id) 

444 cards['common'] = comm_cards['common'] 

445 return cards 

446 

447 def idle_move(self, hud): 

448 try: 

449 hud.move_table_position() 

450 for aw in hud.aux_windows: 

451 aw.move_windows() 

452 except Exception: 

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

454 

455 def idle_resize(self, hud): 

456 try: 

457 hud.resize_windows() 

458 for aw in hud.aux_windows: 

459 aw.resize_windows() 

460 except Exception: 

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

462 

463 def idle_kill(self, table): 

464 try: 

465 if table in self.hud_dict: 

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

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

468 self.hud_dict[table].kill() 

469 del self.hud_dict[table] 

470 self.main_window.resize(1, 1) 

471 except Exception: 

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

473 

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

475 try: 

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

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

478 self.vb.addWidget(newlabel) 

479 

480 self.hud_dict[temp_key].tablehudlabel = newlabel 

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

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

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

484 m.create() 

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

486 m.update_gui(new_hand_id) 

487 

488 except Exception: 

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

490 

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

492 try: 

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

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

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

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

497 aw.update_gui(new_hand_id) 

498 except Exception: 

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

500 

501if __name__ == "__main__": 

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

503 

504 app = QApplication([]) 

505 apply_stylesheet(app, theme='dark_purple.xml') 

506 

507 

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

509 

510 

511 app.exec_()