def _prepare(self): if self.share_dir is None: raise Exception("Cant locate share directory") theme_name = self.settings.value("theme", "default") self.theme = Theme(join(self.share_dir, "themes", theme_name), None) self.theme.enable_sound = self.settings.value("enable_sound", type=bool) self.game = Game(self.server_url) self.poll_timer = self.startTimer(500) self.setup_fields_on_poll = False
def _start_server(self): server_running = Game.check_server(self.server_url) use_local_server = self.settings.value("use_local_server", type=bool) self.local_server_used = False if server_running: if use_local_server: logging.info( _("Server appears to be already running, do not start a local server" )) else: if use_local_server: self.splashscreen.showMessage(_("Starting local server...")) QApplication.processEvents() server_path = self.settings.value("local_server_path") logging.info(_("Running local server: {}").format(server_path)) server = subprocess.Popen(server_path, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) server.poll() if server.returncode is not None and server.returncode != 0: output = server.stdout.read() message = _( "Could not start local server; exit code: {}; message:\n{}" ).format(server.returncode, output.decode('utf-8')) logging.error(message) QMessageBox.critical(self, _("Exception"), message) else: self.local_server_used = True
def process_message(self, message): if "move" in message: move = Move.fromJson(message["move"]) self.message.emit(OtherSideMove(self, move)) self._new_board = Game.parse_board(message["board"]) src_field = self.index_by_label[move.from_field] dst_field = self.get_move_end_field(move) start_position = self.get_field_center(src_field) piece = self.fields[src_field].piece if piece is None: raise Exception("No piece at {}!".format(src_field)) if self.invert_colors: piece = piece.inverted() self.move_animation.start(src_field, dst_field, move, start_position, piece, process_result=False) my_side = 'First' if self.game.user_side == FIRST else 'Second' self.my_turn = True #self.my_turn = message["to_side"] == my_side elif "undo" in message: self.message.emit(UndoMessage()) self._board = Game.parse_board(message["board"]) elif "hint" in message: moves = [Move.fromJson(m) for m in message["hint"]] self.message.emit(AiHintMessage(moves)) elif "result" in message: result = message["result"] self.message.emit(GameResultMessage(result)) elif "draw" in message: self.message.emit(DrawRequestedMessage()) elif "draw_accepted" in message: result = message["draw_accepted"] self.message.emit(DrawResponseMessage(result)) elif "message" in message: text = message["message"] level = message["level"] self.server_log.emit(level, text)
class Checkers(QMainWindow): def __init__(self, share_dir): QMainWindow.__init__(self) self.share_dir = share_dir self.setWindowTitle(_("HCheckers client")) self.settings = QSettings("hcheckers", "hcheckers") self._board_setup_mode = False self._game_active = False self._connection_failed = False self._poll_try_number = 0 self.splashscreen = None self._show_splashcreen(_("Starting HCheckers...")) self.server_url = self.settings.value("server_url", DEFAULT_SERVER_URL) self._start_server() self._prepare() self._gui_setup() self._setup_actions() self._default_new_game() def get_board_setup_mode(self): return self._board_setup_mode def set_board_setup_mode(self, mode): self._board_setup_mode = mode self.run_action.setEnabled(mode) self.put_first_action.setEnabled(mode) self.put_second_action.setEnabled(mode) self.erase_action.setEnabled(mode) board_setup_mode = property(get_board_setup_mode, set_board_setup_mode) def get_my_turn(self): return self.board.my_turn def set_my_turn(self, value): self.board.my_turn = value if value: self.statusBar().showMessage(_("Your turn.")) self.board.text_message = None #self.board.repaint() else: self.statusBar().showMessage( _("Awaiting a turn from another side.")) #self.board.text_message = _("Waiting for another side turn") my_turn = property(get_my_turn, set_my_turn) def get_game_active(self): return self._game_active def set_game_active(self, value): self._game_active = value self.board.locked = not value game_active = property(get_game_active, set_game_active) def _start_server(self): server_running = Game.check_server(self.server_url) use_local_server = self.settings.value("use_local_server", type=bool) self.local_server_used = False if server_running: if use_local_server: logging.info( _("Server appears to be already running, do not start a local server" )) else: if use_local_server: self.splashscreen.showMessage(_("Starting local server...")) QApplication.processEvents() server_path = self.settings.value("local_server_path") logging.info(_("Running local server: {}").format(server_path)) server = subprocess.Popen(server_path, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) server.poll() if server.returncode is not None and server.returncode != 0: output = server.stdout.read() message = _( "Could not start local server; exit code: {}; message:\n{}" ).format(server.returncode, output.decode('utf-8')) logging.error(message) QMessageBox.critical(self, _("Exception"), message) else: self.local_server_used = True def _prepare(self): if self.share_dir is None: raise Exception("Cant locate share directory") theme_name = self.settings.value("theme", "default") self.theme = Theme(join(self.share_dir, "themes", theme_name), None) self.theme.enable_sound = self.settings.value("enable_sound", type=bool) self.game = Game(self.server_url) self.poll_timer = self.startTimer(500) self.setup_fields_on_poll = False def _icon(self, name): return QIcon(join(self.share_dir, "icons", name)) @handling_error def _gui_setup(self): widget = QWidget(self) layout = QVBoxLayout() self.board = Board(self.theme, self.settings, self.game, self) self.board.message.connect(self._on_board_message) self.board.field_clicked.connect(self._on_field_clicked) #self.board.show() self.toolbar = QToolBar(self) self.message = QLabel(self) layout.addWidget(self.toolbar) layout.addWidget(self.message) layout.addWidget(self.board, stretch=1) widget.setLayout(layout) self.setCentralWidget(widget) self.status_info = QLabel(self) self.statusBar().addPermanentWidget(self.status_info) self.rules_info = QLabel(self) self.rules_info.setFrameStyle(QFrame.Sunken | QFrame.Panel) #self.rules_info.setLineWidth(3) self.statusBar().addPermanentWidget(self.rules_info) self.opponent_info = QLabel(self) self.opponent_info.setFrameStyle(QFrame.Sunken | QFrame.Panel) #self.opponent_info.setLineWidth(3) self.statusBar().addPermanentWidget(self.opponent_info) self.history = HistoryWidget(self.game, self.board, self) self.history_dock = QDockWidget(_("History"), self) self.history_dock.setAllowedAreas(Qt.AllDockWidgetAreas) self.history_dock.setWidget(self.history) self.history_dock.setObjectName("history") self.addDockWidget(Qt.RightDockWidgetArea, self.history_dock) self.log = QListWidget(self) self.log.setContextMenuPolicy(Qt.CustomContextMenu) self.log.customContextMenuRequested.connect(self._on_log_context_menu) self.log_dock = QDockWidget(_("Log"), self) self.log_dock.setAllowedAreas(Qt.AllDockWidgetAreas) self.log_dock.setWidget(self.log) self.log_dock.setObjectName("log") self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock) console_handler = logging.getLogger().handlers[0] logging.getLogger().removeHandler(console_handler) log_handler = UiLogHandler(self.log) level = self.settings.value("log_level", logging.INFO, type=int) logging.getLogger().setLevel(level) logging.getLogger().addHandler(log_handler) for logger in lowered_loggers: logging.getLogger(logger).addFilter( LogFilter(lowered_regexps=lowered_regexps)) self.board.server_log.connect(self._on_server_log) geometry = self.settings.value("UI/geometry") if geometry is not None: self.restoreGeometry(geometry) state = self.settings.value("UI/windowState") if state is not None: self.restoreState(state) def _create_action(self, icon, title, menu, handler=None, group=None, toggle=False, toolbar=True, key=None): if group is None: parent = self else: parent = group action = QAction(title, parent) if icon is not None: action.setIcon(icon) if key is not None: action.setShortcut(key) if toggle: action.setCheckable(True) if toolbar: self.toolbar.addAction(action) menu.addAction(action) if handler: action.triggered.connect(handler) return action def _setup_actions(self): menu = self.menuBar().addMenu(_("&Game")) self._create_action(QIcon.fromTheme("document-new"), _("&New Game"), menu, self._on_new_game, key="Ctrl+N") self._create_action(QIcon.fromTheme("document-open"), _("&Open Game..."), menu, self._on_open_game, key="Ctrl+O") self._create_action(QIcon.fromTheme("document-save"), _("&Save Position"), menu, self._on_save_game, key="Ctrl+S") self._create_action(QIcon.fromTheme("edit-undo"), _("&Undo"), menu, self._on_undo, key="Ctrl+Z") self.request_draw_action = self._create_action( self._icon("draw_offer.svg"), _("Offer a &draw"), menu, self._on_draw_rq) self.capitulate_action = self._create_action(self._icon("handsup.svg"), _("Capitulate"), menu, self._on_capitulate) menu.addSeparator() self.clear_log_action = self._create_action( QIcon.fromTheme("edit-clear"), _("&Clear log"), menu, self._on_clear_log, toolbar=False) self.copy_log_action = self._create_action( QIcon.fromTheme("edit-copy"), _("Copy selected log record"), menu, self._on_copy_log, toolbar=False) self.save_log_action = self._create_action( QIcon.fromTheme("document-save"), _("Save &log..."), menu, self._on_save_log, toolbar=False) menu.addSeparator() self.toolbar.addSeparator() self.run_action = self._create_action( QIcon.fromTheme("media-playback-start"), _("Start &Game"), menu, self._on_run_game, key="Ctrl+R") menu.addSeparator() self._create_action(QIcon.fromTheme("preferences-system"), _("Se&ttings"), menu, self._on_settings, toolbar=False) menu.addSeparator() self._create_action(QIcon.fromTheme("application-exit"), _("E&xit"), menu, self._on_exit, toolbar=False, key="Ctrl+Q") menu = self.menuBar().addMenu(_("&Position")) setup = QActionGroup(self) setup.setExclusive(True) self.put_first_action = self._create_action(self._icon("manwhite.svg"), _("Put &white piece"), menu, group=setup, toggle=True) self.put_second_action = self._create_action( self._icon("manblack.svg"), _("Put &black piece"), menu, group=setup, toggle=True) self.erase_action = self._create_action(QIcon.fromTheme("list-remove"), _("&Remove piece"), menu, group=setup, toggle=True) self.board_setup_mode = False menu.addSeparator() self.toolbar.addSeparator() menu = self.menuBar().addMenu(_("&View")) self.flip_action = self._create_action( QIcon.fromTheme("object-flip-vertical"), _("&Flip board"), menu, self._on_flip_board, toggle=True, key="Ctrl+T") flip = self.settings.value("flip_board", False, type=bool) self.flip_action.setChecked(flip) self._set_flip_board(flip) menu.addAction(self.history_dock.toggleViewAction()) menu.addAction(self.log_dock.toggleViewAction()) @handling_error def _on_run_game(self, checked=None): self.board_setup_mode = False board = self.board.json() self.game.start_new_game( self.game_settings.user_name, rules=self.game_settings.rules, user_turn_first=self.game_settings.user_turn_first, ai=self.game_settings.ai, board=board) self.board.text_message = None self.board.fields_setup() @handling_error def _on_field_clicked(self, row, col): if not self.board_setup_mode: return field = self.board.fields[(row, col)] if not field.usable: logging.debug("You cant put piece at this field") return first = self.put_first_action.isChecked() second = self.put_second_action.isChecked() erase = self.erase_action.isChecked() if not first and not second and not erase: return if first: side = FIRST elif second: side = SECOND piece = field.piece if not erase: if piece and piece.side == side: if piece.kind == MAN: piece.kind = KING else: piece.kind = MAN else: piece = Piece(MAN, side) else: piece = None self.board.fields[(row, col)].piece = piece @handling_error def _default_new_game(self): self.splashscreen.finish(self) if len(sys.argv) == 2: path = sys.argv[1] if path.endswith(".pdn"): mask = PDN_MASK elif path.endswith(".fen"): mask = FEN_MASK else: mask = None else: path, mask = None, None self._on_new_game(show_exit=True, open_file=(path, mask)) @handling_error def _on_exit(self, *args): self.close() def _screen_size(self): rect = QApplication.desktop().availableGeometry(self) return min(rect.width(), rect.height()) def _splashscreen_size(self): screen_size = self._screen_size() return screen_size / 2 def _show_splashcreen(self, message=None): splash_size = self._splashscreen_size() splash_pix = self._icon("splashscreen.svg").pixmap( QSize(splash_size, splash_size)) self.splashscreen = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) self.splashscreen.show() QApplication.processEvents() if message is not None: self.splashscreen.showMessage(message) QApplication.processEvents() def _new_game(self, dialog): # Show splashcreen after user pressed Ok in the "new game" dialog self._show_splashcreen(_("Starting new game...")) if self.game.is_active(): self.game.capitulate() self.board.text_message = None self.game_active = True self.game.game_id = None self.request_draw_action.setEnabled(True) self.capitulate_action.setEnabled(True) self.game_settings = game = dialog.get_settings() if game.action == START_AI_GAME: if game.board_setup: self.board.empty() self.board_setup_mode = True self.game.rules = game.rules else: self.game.start_new_game( game.user_name, rules=game.rules, user_turn_first=game.user_turn_first, ai=game.ai, fen_path=game.fen_path, pdn_path=game.pdn_path, previous_board_game=game.previous_board_game) state = self.game.get_state() my_side = 'First' if self.game.user_side == FIRST else 'Second' self.my_turn = state["side"] == my_side self.rules_info.setText( _("Rules: {}").format(rules_dict[game.rules])) self.opponent_info.setText(_("AI: {}").format(game.ai.title)) self.status_info.setText("") elif game.action == START_HUMAN_GAME: game_id = self.game.new_game(game.rules) logging.info(_("New game ID: {}").format(game_id)) if game.user_turn_first: self.game.register_user(game.user_name, FIRST) else: self.game.register_user(game.user_name, SECOND) self.setup_fields_on_poll = True self.rules_info.setText(_("Rules: {}").format(game.rules)) self.opponent_info.setText("") self.status_info.setText("") self.game_active = False message = _("Waiting for another side to join the game.") self.statusBar().showMessage(message) self.board.text_message = message elif game.action == JOIN_HUMAN_GAME: self.game.game_id = dialog.lobby.get_game_id() self.game.user_side = side = dialog.lobby.get_free_side() self.game.rules = dialog.lobby.get_rules() #used_name = dialog.lobby.get_used_name() self.game.register_user(game.user_name, side) self.game.run_game() self.setup_fields_on_poll = True self.my_turn = side == FIRST self.rules_info.setText(_("Rules: {}").format(game.rules)) self.opponent_info.setText("") self.status_info.setText("") size, invert, notation = self.game.get_notation(game.rules) self.board.invert_colors = invert self.board.topology = self.game.get_topology(game.rules) self.board.set_notation(size, notation) self.board.theme = self.board.theme #self.board.repaint() self.history.fill() @handling_error def _on_new_game(self, checked=None, show_exit=False, open_file=(None, None)): dialog = NewGameDialog(self.settings, self.game, self.share_dir, show_exit, open_file=open_file, parent=self) result = dialog.exec_() if result == QDialog.Accepted: self._new_game(dialog) if self.splashscreen: self.splashscreen.finish(self) if result == EXIT: print("Exit!") #QApplication.quit() QTimer.singleShot(0, lambda: self.close()) @handling_error def _on_open_game(self, checked=None): path, mask = select_game_file(self) if path: dialog = NewGameDialog(self.settings, self.game, self.share_dir, show_exit=False, open_file=(path, mask), parent=self) result = dialog.exec_() if result == QDialog.Accepted: self._new_game(dialog) if self.splashscreen: self.splashscreen.finish(self) @handling_error def _on_save_game(self, checked=None): (path, mask) = QFileDialog.getSaveFileName(self.board, _("Save file"), ".", FEN_MASK + ";;" + PDN_MASK) if path: if mask == FEN_MASK: fen = self.game.get_fen() with open(path, 'w') as f: f.write(fen) elif mask == PDN_MASK: pdn = self.game.get_pdn() with open(path, 'w') as f: f.write(pdn) @handling_error def _on_undo(self, checked=None): try: prev_board = self.game.undo() self.board.fields_setup(prev_board) self.board.repaint() self.history.fill() except RequestError as e: error = e.rs.json().get("error", None) if error == "NothingToUndo": logging.warning(_("Nothing to undo.")) else: raise e @handling_error def _on_draw_rq(self, checked=None): messages = self.game.request_draw() for message in messages: self.board.process_message(message) self.request_draw_action.setEnabled(False) self.capitulate_action.setEnabled(False) @handling_error def _on_accept_draw(self, checked=None): messages = self.game.accept_draw(True) for message in messages: self.board.process_message(message) @handling_error def _on_decline_draw(self, checked=None): messages = self.game.accept_draw(False) for message in messages: self.board.process_message(message) @handling_error def _on_capitulate(self, checked=None): messages = self.game.capitulate() for message in messages: self.board.process_message(message) #self.my_turn = False @handling_error def get_result_str(self, result): first, second = self.game.get_colors(self.game.rules) if result == 'FirstWin': return _("{} win").format(first) elif result == 'SecondWin': return _("{} win").format(second) else: return _("Draw") def _on_board_message(self, message): if isinstance(message, GameResultMessage): self.game_active = False result = self.get_result_str(message.result) result_text = _("Game result: {}").format(result) self.status_info.setText(result_text) self.board.setCursor(Qt.ArrowCursor) game_over = _("Game over.") self.statusBar().showMessage(game_over) self.board.text_message = game_over + " " + result_text self.history.fill() self.request_draw_action.setEnabled(False) self.capitulate_action.setEnabled(False) elif isinstance(message, OtherSideMove): self.message.setText(str(message)) self.history.fill() self.my_turn = True self.board.text_message = None self.board.repaint() elif isinstance(message, WaitingMove): text = str(message) self.statusBar().showMessage(text) self.board.text_message = text elif isinstance(message, DrawRequestedMessage): ok = QMessageBox.question( self, _("Accept the draw?"), _("Another side have offered you a draw. Do you wish to accept it?" )) if ok == QMessageBox.Yes: self._on_accept_draw() else: self._on_decline_draw() elif isinstance(message, DrawResponseMessage): text = str(message) self.message.setText(text) self.board.text_message = text self.board.repaint() if not message.result: self.request_draw_action.setEnabled(True) self.capitulate_action.setEnabled(True) def _on_server_log(self, level, message): if level == "DEBUG": logging.debug(message) elif level == "INFO": logging.info(message) elif level == "WARNING": logging.warning(message) elif level == "ERROR": logging.error(message) # item = QListWidgetItem(self.log) # item.setText(message) # icon = None # if level == "DEBUG": # icon = QIcon.fromTheme("document-properties") # elif level == "INFO": # icon = QIcon.fromTheme("dialog-information") # elif level == "ERROR": # icon = QIcon.fromTheme("dialog-error") # elif level == "WARNING": # icon = QIcon.fromTheme("dialog-warning") # if icon is not None: # item.setIcon(icon) # self.log.update() # self.log.scrollToBottom() def _log_context_menu(self): menu = QMenu(self) menu.addAction(self.clear_log_action) menu.addAction(self.copy_log_action) menu.addAction(self.save_log_action) return menu def _on_log_context_menu(self, pos): menu = self._log_context_menu() menu.exec_(self.log.mapToGlobal(pos)) def _on_clear_log(self, checked): self.log.clear() logging.info(_("Log has been cleared.")) def _on_save_log(self, checked): text = "" for row in range(self.log.count()): text = text + self.log.item(row).text() + "\n" (path, mask) = QFileDialog.getSaveFileName(self, _("Save file"), ".", LOG_MASK) if path: with open(path, 'w') as f: f.write(text) #.encode("utf-8")) def _on_copy_log(self, checked): items = self.log.selectedItems() if not items: return text = items[0].text() QApplication.clipboard().setText(text) def _set_flip_board(self, value): self.board.flip = value self.settings.setValue("flip_board", self.board.flip) def _on_flip_board(self): self._set_flip_board(self.flip_action.isChecked()) def _on_settings(self): dialog = SettingsDialog(self.settings, self.share_dir, self) result = dialog.exec_() if result == QDialog.Accepted: self.board.show_possible_moves = dialog.get_show_possible_moves() self.board.show_notation = dialog.get_show_notation() self.board.theme = dialog.get_theme() self.board.theme.enable_sound = dialog.get_enable_sound() level = self.settings.value("log_level", logging.INFO, type=int) logging.getLogger().setLevel(level) self.settings.sync() logging.info(_("Settings have been updated.")) def _handle_game_error(self, rs): try: json = rs.json() except: json = None message_format = _( "Unexpected response received from the server.\nRequest URL: {}\nResponse code: {}\nResponse message: {}" ) if json is None: message = message_format.format(rs.url, rs.status_code, rs.text) else: err_msg = json.get("error", "Unspecified") if err_msg == "no such move": move = json.get("move", "?") #board = Game.parse_board(json["board"]) #board = self.board #possible = json.get("possible", []) #possible = [board.show_move(Move.fromJson(m)) for m in possible] message = _("No such move: {}").format(move) elif err_msg == "invalid game status": expected = json.get("expected", "?") actual = json.get("actual", "?") message = _( "Status of current game is unsuitable for this operation. Game status is {}; required status is {}" ).format(expected, actual) else: message = message_format.format(rs.url, rs.status_code, err_msg) QMessageBox.critical(self, _("Exception"), message) logging.exception(message) def _handle_connection_error(self, url, e): message = _( "An exception occured while connecting to the server.\nRequest URL: {}\nException text: {}" ).format(url, e) QMessageBox.critical(self, _("Exception"), message) logging.exception(message) self._connection_failed = True def closeEvent(self, ev): if self.game.is_active(): try: self.game.capitulate() except Exception as e: logging.exception(e) print(e) use_local_server = self.settings.value("use_local_server", type=bool) if use_local_server and self.local_server_used: try: self.game.shutdown() except RequestError as e: self._handle_game_error(e.rs) except Exception as e: logging.exception(e) print(e) self.settings.setValue("UI/geometry", self.saveGeometry()) self.settings.setValue("UI/windowState", self.saveState()) QMainWindow.closeEvent(self, ev) @handling_error def timerEvent(self, e): if e.timerId() != self.poll_timer: return if self._connection_failed: self._poll_try_number = self._poll_try_number + 1 if self._poll_try_number < 10: return if self.setup_fields_on_poll and not self.game_active: state = self.game.get_state() if state["status"] == 'Running': self.game_active = True my_side = 'First' if self.game.user_side == FIRST else 'Second' self.my_turn = state['side'] == my_side #self.statusBar().clearMessage() if not self.game.is_active(): return self._poll_try_number = 0 board, messages = self.game.poll() for message in messages: self.board.process_message(message) if "move" in message: self.my_turn = True if self.setup_fields_on_poll: self.board.fields_setup(board)