Exemple #1
0
    def transfer_time(time_list):
        def num(ts):
            try:
                return int(ts)
            except ValueError:
                return 1

        if len(time_list) == 1:
            secs = num(time_list[0])
            time_control = TimeControl(TimeMode.FIXED, seconds_per_move=secs)
            text = dgttranslate.text('B00_tc_fixed', '{:2d}'.format(secs))
        elif len(time_list) == 2:
            mins = num(time_list[0])
            finc = num(time_list[1])
            if finc == 0:
                time_control = TimeControl(TimeMode.BLITZ,
                                           minutes_per_game=mins)
                text = dgttranslate.text('B00_tc_blitz', '{:2d}'.format(mins))
            else:
                time_control = TimeControl(TimeMode.FISCHER,
                                           minutes_per_game=mins,
                                           fischer_increment=finc)
                text = dgttranslate.text('B00_tc_fisch',
                                         '{:2d} {:2d}'.format(mins, finc))
        else:
            time_control = TimeControl(TimeMode.BLITZ, minutes_per_game=5)
            text = dgttranslate.text('B00_tc_blitz', ' 5')
        return time_control, text
Exemple #2
0
 def _process_startup_info(self, message):
     self.play_mode = message.info['play_mode']
     self.dgtmenu.set_mode(message.info['interaction_mode'])
     self.dgtmenu.set_book(message.info['book_index'])
     self.dgtmenu.all_books = message.info['books']
     tc_init = message.info['tc_init']
     timectrl = self.time_control = TimeControl(**tc_init)
     self.dgtmenu.set_time_mode(timectrl.mode)
     # try to find the index from the given time_control (timectrl)
     # if user gave a non-existent timectrl value update map & list
     index = 0
     isnew = True
     if timectrl.mode == TimeMode.FIXED:
         for val in self.dgtmenu.tc_fixed_map.values():
             if val == timectrl:
                 self.dgtmenu.set_time_fixed(index)
                 isnew = False
                 break
             index += 1
         if isnew:
             self.dgtmenu.tc_fixed_map.update({('', timectrl)})
             self.dgtmenu.tc_fixed_list.append(timectrl.get_list_text())
             self.dgtmenu.set_time_fixed(index)
     elif timectrl.mode == TimeMode.BLITZ:
         for val in self.dgtmenu.tc_blitz_map.values():
             if val == timectrl:
                 self.dgtmenu.set_time_blitz(index)
                 isnew = False
                 break
             index += 1
         if isnew:
             self.dgtmenu.tc_blitz_map.update({('', timectrl)})
             self.dgtmenu.tc_blitz_list.append(timectrl.get_list_text())
             self.dgtmenu.set_time_blitz(index)
     elif timectrl.mode == TimeMode.FISCHER:
         for val in self.dgtmenu.tc_fisch_map.values():
             if val == timectrl:
                 self.dgtmenu.set_time_fisch(index)
                 isnew = False
                 break
             index += 1
         if isnew:
             self.dgtmenu.tc_fisch_map.update({('', timectrl)})
             self.dgtmenu.tc_fisch_list.append(timectrl.get_list_text())
             self.dgtmenu.set_time_fisch(index)
def process_file(filepath, database):
    with open(filepath, "r") as f:
        player1 = None
        player2 = None
        variant = None
        tc = None
        for line in f:
            line = line.strip()
            if len(line) == 0:
                continue
            if line.startswith("[White "):
                player1 = line.split("\"")[1].lower()
            elif line.startswith("[Black "):
                player2 = line.split("\"")[1].lower()
            elif line.startswith("[Variant "):
                variant = line.split("\"")[1]
            elif line.startswith("[TimeControl "):
                tc = TimeControl(line.split("\"")[1])
            elif not line.startswith("["):
                if player1 is None or player2 is None or variant is None or tc is None:
                    raise Exception("bug")
                update_player(player1, tc.score, database, variant)
                update_player(player2, tc.score, database, variant)
Exemple #4
0
def main():
    #Command line argument parsing
    parser = configargparse.ArgParser(default_config_files=[os.path.join(os.path.dirname(__file__), "picochess.ini")])
    parser.add_argument("-e", "--engine", type=str, help="UCI engine executable path", required=True)
    parser.add_argument("-d", "--dgt-port", type=str, help="enable dgt board on the given serial port such as /dev/ttyUSB0")
    parser.add_argument("-leds", "--enable-dgt-board-leds", action='store_true', help="enable dgt board leds")
    parser.add_argument("-hs", "--hash-size", type=int, help="hashtable size in MB (default:64)", default=64)
    parser.add_argument("-t", "--threads", type=int, help="number of engine threads (default:1)", default=1)
    parser.add_argument("-l", "--log-level", choices=['notset', 'debug', 'info', 'warning', 'error', 'critical'], default='warning', help="logging level")
    parser.add_argument("-lf", "--log-file", type=str, help="log to the given file")
    parser.add_argument("-r", "--remote", type=str, help="remote server running the engine")
    parser.add_argument("-u", "--user", type=str, help="remote user on server running the engine")
    parser.add_argument("-p", "--password", type=str, help="password for the remote user")
    parser.add_argument("-sk", "--server-key", type=str, help="key file used to connect to the remote server")
    parser.add_argument("-pgn", "--pgn-file", type=str, help="pgn file used to store the games", default='games.pgn')
    parser.add_argument("-ar", "--auto-reboot", action='store_true', help="reboot system after update")
    parser.add_argument("-web", "--web-server", dest="web_server_port", nargs="?", const=80, type=int, metavar="PORT", help="launch web server")
    parser.add_argument("-mail", "--email", type=str, help="email used to send pgn files", default=None)
    parser.add_argument("-mail_s", "--smtp_server", type=str, help="Adress of email server", default=None)
    parser.add_argument("-mail_u", "--smtp_user", type=str, help="Username for email server", default=None)
    parser.add_argument("-mail_p", "--smtp_pass", type=str, help="Password for email server", default=None)
    parser.add_argument("-mail_enc", "--smtp_encryption", action='store_true', help="use ssl encryption connection to smtp-Server")
    parser.add_argument("-mk", "--mailgun-key", type=str, help="key used to send emails via Mailgun Webservice", default=None)
    parser.add_argument("-uci", "--uci-option", type=str, help="pass an UCI option to the engine (name;value)", default=None)
    parser.add_argument("-dgt3000", "--dgt-3000-clock", action='store_true', help="use dgt 3000 clock")
    parser.add_argument("-nobeep", "--disable-dgt-clock-beep", action='store_true', help="disable beeps on the dgt clock")
    parser.add_argument("-uvoice", "--user-voice", type=str, help="voice for user", default=None)
    parser.add_argument("-cvoice", "--computer-voice", type=str, help="voice for computer", default=None)
    args = parser.parse_args()
    # Enable logging
    logging.basicConfig(filename=args.log_file, level=getattr(logging, args.log_level.upper()),
                        format='%(asctime)s.%(msecs)d %(levelname)s %(module)s - %(funcName)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")

    # Update
    update_picochess(args.auto_reboot)

    # Load UCI engine
    engine = uci.Engine(args.engine, hostname=args.remote, username=args.user, key_file=args.server_key, password=args.password)

    logging.debug('Loaded engine [%s]', engine.get().name)
    logging.debug('Supported options [%s]', engine.get().options)
    if 'Hash' in engine.get().options:
        engine.set_option("Hash", args.hash_size)
    if 'Threads' in engine.get().options:  # Stockfish
        engine.set_option("Threads", args.threads)
    if 'Core Threads' in engine.get().options:  # Hiarcs
        engine.set_option("Core Threads", args.threads)
    if args.uci_option:
        for uci_option in args.uci_option.strip('"').split(";"):
            uci_parameter = uci_option.strip().split('=')
            engine.set_option(uci_parameter[0], uci_parameter[1])

    # Connect to DGT board
    if args.dgt_port:
        logging.debug("Starting picochess with DGT board on [%s]", args.dgt_port)
        dgt.DGTBoard(args.dgt_port, args.enable_dgt_board_leds, args.dgt_3000_clock, not args.disable_dgt_clock_beep).start()
    else:
        logging.warning("No DGT board port provided")
        # Enable keyboard input and terminal display
        KeyboardInput().start()
        TerminalDisplay().start()

    # Save to PGN
    PgnDisplay(args.pgn_file, email=args.email, fromINIMailGun_Key=args.mailgun_key,
                        fromIniSmtp_Server=args.smtp_server, fromINISmtp_User=args.smtp_user,
                        fromINISmtp_Pass=args.smtp_pass, fromINISmtp_Enc=args.smtp_encryption).start() 

    # Create ChessTalker for speech output
    talker = None
    if(args.user_voice or args.computer_voice):
        logging.debug("Initializing ChessTalker [%s, %s]", str(args.user_voice), str(args.computer_voice))
        talker = chesstalker.chesstalker.ChessTalker(args.user_voice, args.computer_voice)
        talker.start()
    else:
        logging.debug("ChessTalker disabled")

    # Launch web server
    if(args.web_server_port):
        WebServer(args.web_server_port).start()

    def compute_legal_fens(g):
        """
        Computes a list of legal FENs for the given game.
        Also stores the initial position in the 'root' attribute.
        :param g: The game
        :return: A list of legal FENs, and the root FEN
        """
        class FenList(list):
            def __init__(self, *args):
                list.__init__(self, *args)
                self.root = ''

        fens = FenList()
        for move in g.legal_moves:
            g.push(move)
            fens.append(g.fen().split(' ')[0])
            g.pop()
        fens.root = g.fen().split(' ')[0]
        return fens

    def think(time):
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        def send_book_move(move):
            g=copy.deepcopy(game)
            g.push(move)
            book_ponder = weighted_choice(book, g)
            Observable.fire(Event.BEST_MOVE, move=move, ponder=book_ponder)
            Observable.fire(Event.SCORE, score='book', mate=None)

        # global book_thread
        book_move = weighted_choice(book, game)
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time)
        time.run(game.turn)
        if book_move:
            Display.show(Message.BOOK_MOVE, move=book_move.uci())
            send_book_move(book_move)

            # No need for one more thread at this point given slightly slower picochess, can bring back if needed
            # book_thread = threading.Timer(2, send_book_move, [book_move])
            # book_thread.start()
        else:
            # book_thread = None
            engine.set_position(game)
            engine.go(time.uci())
            Display.show(Message.SEARCH_STARTED)

    def analyse():
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        engine.set_position(game)
        engine.ponder()
        Display.show(Message.SEARCH_STARTED)

    def observe(time):
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time)
        # logging.debug("Starting clock")
        time.run(game.turn)

        engine.set_position(game)
        engine.ponder()
        Display.show(Message.SEARCH_STARTED)

    def stop_thinking():
        """
        Stop current search or book thread.
        :return:
        """
        #if book_thread:
        #    book_thread.cancel()
        #else:
        engine.stop()

    def check_game_state(game, play_mode):
        """
        Check if the game has ended or not ; it also sends Message to Displays if the game has ended.
        :param game:
        :return: True is the game continues, False if it has ended
        """
        custom_fen = game.custom_fen if hasattr(game, 'custom_fen') else None
        if game.is_stalemate():
            Display.show(Message.GAME_ENDS, result=GameResult.STALEMATE, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
            return False
        if game.is_insufficient_material():
            Display.show(Message.GAME_ENDS, result=GameResult.INSUFFICIENT_MATERIAL, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
            return False
        if game.is_seventyfive_moves():
            Display.show(Message.GAME_ENDS, result=GameResult.SEVENTYFIVE_MOVES, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
            return False
        if game.is_fivefold_repetition():
            Display.show(Message.GAME_ENDS, result=GameResult.FIVEFOLD_REPETITION, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
            return False
        if game.is_game_over():
            Display.show(Message.GAME_ENDS, result=GameResult.MATE, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
            return False
        return True

    game = chess.Board()  # Create the current game
    legal_fens = compute_legal_fens(game)  # Compute the legal FENs
    book = chess.polyglot.open_reader(get_opening_books()[8][1])  # Default opening book
    interaction_mode = Mode.GAME   # Interaction mode
    play_mode = GameMode.PLAY_WHITE

    # book_thread = None  # The thread that will fire book moves
    time_control = TimeControl(ClockMode.BLITZ, minutes_per_game=5)

    #Send the engine's UCI options to all Displays
    Display.show(Message.UCI_OPTION_LIST, options=engine.get().options)
    Display.show(Message.SYSTEM_INFO, info={"version": version, "location": get_location(),
                                            "books": get_opening_books(), "ip": get_ip()})

    #Event loop
    while True:
        event = event_queue.get()
        logging.debug('Received event in event loop : %s', event)
        for case in switch(event):
            if case(Event.FEN):  # User sets a new position, convert it to a move if it is legal
                if event.fen in legal_fens:
                    # Check if we have to undo a previous move (sliding)
                    if interaction_mode == Mode.GAME:
                        if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.BLACK) or (play_mode == GameMode.PLAY_BLACK and game.turn == chess.WHITE):
                            stop_thinking()
                            if game.move_stack:
                                game.pop()
                    legal_moves = list(game.legal_moves)
                    Observable.fire(Event.USER_MOVE, move=legal_moves[legal_fens.index(event.fen)])
                elif event.fen == game.fen().split(' ')[0]:  # Player had done the computer move on the board
                    if check_game_state(game, play_mode):
                        Display.show(Message.COMPUTER_MOVE_DONE_ON_BOARD)
                        if time_control.mode != ClockMode.FIXED_TIME:
                            Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time_control)
                            # logging.debug("Starting player clock")
                            time_control.run(game.turn)
                elif event.fen == legal_fens.root:  # Allow user to take his move back while the engine is searching
                    stop_thinking()
                    game.pop()
                    if interaction_mode == Mode.ANALYSIS:
                        analyse()
                    if interaction_mode == Mode.OBSERVE:
                        observe(time_control)
                    Display.show(Message.USER_TAKE_BACK)
                else:  # Check if this a a previous legal position and allow user to restart from this position
                    game_history = copy.deepcopy(game)
                    while game_history.move_stack:
                        game_history.pop()
                        if (play_mode == GameMode.PLAY_WHITE and game_history.turn == chess.WHITE) \
                            or (play_mode == GameMode.PLAY_BLACK and game_history.turn == chess.BLACK) \
                            or (interaction_mode == Mode.OBSERVE) or (interaction_mode == Mode.ANALYSIS):
                            if game_history.fen().split(' ')[0] == event.fen:
                                logging.debug("Undoing game until FEN :" + event.fen)
                                stop_thinking()
                                while len(game_history.move_stack) < len(game.move_stack):
                                    game.pop()
                                if interaction_mode == Mode.ANALYSIS:
                                    analyse()
                                if interaction_mode == Mode.OBSERVE:
                                    observe(time_control)
                                Display.show(Message.USER_TAKE_BACK)
                                legal_fens = compute_legal_fens(game)
                                break
                break

            if case(Event.USER_MOVE):  # User sends a new move
                move = event.move
                logging.debug('User move [%s]', move)
                if move not in game.legal_moves:
                    logging.warning('Illegal move [%s]', move)
                # Check if we are in play mode and it is player's turn
                elif interaction_mode == Mode.GAME:
                    if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.WHITE) or \
                            (play_mode == GameMode.PLAY_BLACK and game.turn == chess.BLACK):
                        time_control.stop()
                        # logging.debug("Stopping player clock")
                        game.push(move)
                        if check_game_state(game, play_mode):
                            think(time_control)
                            Display.show(Message.USER_MOVE, move=move, game=copy.deepcopy(game))
                elif interaction_mode == Mode.OBSERVE:
                    stop_thinking()
                    time_control.stop()
                    fen = game.fen()
                    game.push(move)
                    if check_game_state(game, play_mode):
                        observe(time_control)
                        Display.show(Message.REVIEW_MODE_MOVE, move=move, fen=fen, game=copy.deepcopy(game), mode=interaction_mode)
                        legal_fens = compute_legal_fens(game)
                elif (interaction_mode == Mode.ANALYSIS) or (interaction_mode == Mode.KIBITZ):
                    stop_thinking()
                    # time_control.stop()
                    fen = game.fen()
                    game.push(move)
                    if check_game_state(game, play_mode):
                        analyse()
                        Display.show(Message.REVIEW_MODE_MOVE, move=move, fen=fen, game=copy.deepcopy(game), mode=interaction_mode)
                        legal_fens = compute_legal_fens(game)
                break

            if case(Event.LEVEL):  # User sets a new level
                level = event.level
                logging.debug("Setting engine to level %i", level)
                engine.set_level(level)
                break

            if case(Event.SETUP_POSITION): # User sets up a position
                logging.debug("Setting up custom fen: {0}".format(event.fen))

                game = chess.Board(event.fen)
                game.custom_fen = event.fen
                legal_fens = compute_legal_fens(game)
                time_control.stop()
                time_control.reset()
                Display.show(Message.START_NEW_GAME)
                if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.BLACK) or (play_mode == GameMode.PLAY_BLACK and game.turn == chess.WHITE):
                    think(time_control)
                break

            if case(Event.NEW_GAME):  # User starts a new game
                if game.move_stack:
                    logging.debug("Starting a new game")
                    if not game.is_game_over():
                        custom_fen = game.custom_fen if hasattr(game, 'custom_fen') else None
                        Display.show(Message.GAME_ENDS, result=GameResult.ABORT, moves=list(game.move_stack), color=game.turn, mode=play_mode, custom_fen=custom_fen)
                    game = chess.Board()
                    legal_fens = compute_legal_fens(game)
                    time_control.stop()
                    time_control.reset()
                    Display.show(Message.START_NEW_GAME)
                    if interaction_mode == Mode.ANALYSIS:
                        play_mode = GameMode.PLAY_WHITE
                if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.BLACK) or (play_mode == GameMode.PLAY_BLACK and game.turn == chess.WHITE):
                    think(time_control)
                break

            if case(Event.OPENING_BOOK):
                logging.debug("Changing opening book [%s]", get_opening_books()[event.book_index][1])
                book = chess.polyglot.open_reader(get_opening_books()[event.book_index][1])
                break

            if case(Event.BEST_MOVE):
                move = event.move
                ponder = event.ponder
                # Check if we are in play mode and it is computer's turn
                if interaction_mode == Mode.GAME:
                    if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.BLACK) or (play_mode == GameMode.PLAY_BLACK and game.turn == chess.WHITE):
                        time_control.stop()
                        fen = game.fen()
                        game.push(move)
                        Display.show(Message.COMPUTER_MOVE, move=move, ponder=ponder, fen=fen, game=copy.deepcopy(game), time_control=time_control)
                        # if check_game_state(game, interaction_mode):
                        legal_fens = compute_legal_fens(game)
                break

            if case(Event.NEW_PV):
                if (interaction_mode == Mode.ANALYSIS) or (interaction_mode == Mode.OBSERVE) or (interaction_mode == Mode.KIBITZ):
                    Display.show(Message.NEW_PV, pv=event.pv, interaction_mode=interaction_mode, fen=game.fen())
                break

            if case(Event.SCORE):
                Display.show(Message.SCORE, score=event.score, mate=event.mate, interaction_mode=interaction_mode)
                break

            if case(Event.SET_MODE):
                Display.show(Message.INTERACTION_MODE, mode=event.mode)  # Useful for pgn display device
                interaction_mode = event.mode
                break

            if case(Event.SET_PLAYMODE):
                Display.show(Message.PLAY_MODE, mode=event.mode)  # Useful for pgn display device
                play_mode = event.mode
                break

            if case(Event.CHANGE_PLAYMODE):
                if play_mode == GameMode.PLAY_WHITE:
                    play_mode = GameMode.PLAY_BLACK
                else:
                    play_mode = GameMode.PLAY_WHITE
                Display.show(Message.INTERACTION_MODE, mode=play_mode)
                if (play_mode == GameMode.PLAY_WHITE and game.turn == chess.BLACK) or (play_mode == GameMode.PLAY_BLACK and game.turn == chess.WHITE):
                    if check_game_state(game, play_mode):
                        think(time_control)
                break

            if case(Event.SET_TIME_CONTROL):
                time_control = event.time_control
                break

            if case(Event.OUT_OF_TIME):
                stop_thinking()
                custom_fen = game.custom_fen if hasattr(game, 'custom_fen') else None
                Display.show(Message.GAME_ENDS, result=GameResult.TIME_CONTROL, moves=list(game.move_stack), color=event.color, mode=play_mode, custom_fen=custom_fen)
                break

            if case(Event.UCI_OPTION_SET):
                engine.set_option(event.name, event.value)
                break

            if case(Event.SHUTDOWN):
                if talker:
                    talker.say_event(event)
                shutdown()
                break

            if case():  # Default
                logging.warning("Event not handled : [%s]", event)
Exemple #5
0
def main():

    def engine_startup():
        if 'Hash' in engine.get().options:
            engine.option("Hash", args.hash_size)
        if 'Threads' in engine.get().options:  # Stockfish
            engine.option("Threads", args.threads)
        if 'Core Threads' in engine.get().options:  # Hiarcs
            engine.option("Core Threads", args.threads)
        if args.uci_option:
            for uci_option in args.uci_option.strip('"').split(";"):
                uci_parameter = uci_option.strip().split('=')
                engine.option(uci_parameter[0], uci_parameter[1])
        # send the options to the engine
        engine.send()
        # Log the engine info
        logging.debug('Loaded engine [%s]', engine_name)
        logging.debug('Supported options [%s]', engine.get().options)

    def display_system_info():
        if args.enable_internet:
            place = get_location()
            addr = get_ip()
        else:
            place = "?"
            addr = "?"
        DisplayMsg.show(Message.SYSTEM_INFO(info={"version": version, "location": place,
                                                  "books": get_opening_books(), "ip": addr,
                                                  "engine_name": engine_name, "user_name": user_name
                                                  }))

    def compute_legal_fens(g):
        """
        Computes a list of legal FENs for the given game.
        Also stores the initial position in the 'root' attribute.
        :param g: The game
        :return: A list of legal FENs, and the root FEN
        """

        class FenList(list):
            def __init__(self, *args):
                list.__init__(self, *args)
                self.root = ''

        fens = FenList()
        for move in g.legal_moves:
            g.push(move)
            fens.append(g.board_fen())
            g.pop()
        fens.root = g.board_fen()
        return fens

    def probe_tablebase(game):
        if not gaviota:
            return None
        score = gaviota.probe_dtm(game)
        if score is not None:
            Observable.fire(Event.NEW_SCORE(score='tb', mate=score))
        return score

    def think_callback():
        tc = time_control
        tc.start(game.turn)

        book_move = searchmoves.book(bookreader, game)
        if book_move:
            Observable.fire(Event.NEW_SCORE(score='book', mate=None))
            Observable.fire(Event.BEST_MOVE(result=book_move, inbook=True))
        else:
            probe_tablebase(game)
            engine.position(copy.deepcopy(game))
            uci_dict = tc.uci()
            uci_dict['searchmoves'] = searchmoves.all(game)
            engine.go(uci_dict)

    def think(game, tc):
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        DisplayMsg.show(Message.RUN_CLOCK(turn=game.turn, time_control=tc, callback=think_callback))

    def analyse(game):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        probe_tablebase(game)
        engine.position(copy.deepcopy(game))
        engine.ponder()

    def observe_callback():
        tc = time_control
        tc.start(game.turn)
        analyse(game)

    def observe(game, tc):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        DisplayMsg.show(Message.RUN_CLOCK(turn=game.turn, time_control=tc, callback=observe_callback))

    def stop_search():
        """
        Stop current search.
        :return:
        """
        engine.stop()

    def stop_clock():
        nonlocal time_control
        time_control.stop()
        DisplayMsg.show(Message.STOP_CLOCK())

    def stop_search_and_clock():
        stop_clock()
        stop_search()

    def check_game_state(game, play_mode):
        """
        Check if the game has ended or not ; it also sends Message to Displays if the game has ended.
        :param game:
        :return: True is the game continues, False if it has ended
        """
        result = None
        if game.is_stalemate():
            result = GameResult.STALEMATE
        if game.is_insufficient_material():
            result = GameResult.INSUFFICIENT_MATERIAL
        if game.is_seventyfive_moves():
            result = GameResult.SEVENTYFIVE_MOVES
        if game.is_fivefold_repetition():
            result = GameResult.FIVEFOLD_REPETITION
        if game.is_checkmate():
            result = GameResult.MATE

        if result is None:
            return True
        else:
            custom_fen = getattr(game, 'custom_fen', None)
            DisplayMsg.show(Message.GAME_ENDS(result=result, play_mode=play_mode, game=copy.deepcopy(game), custom_fen=custom_fen))
            return False

    def timecontrol_callback():
        time_control.start(game.turn)

    def process_fen(fen, legal_fens):
        nonlocal last_computer_fen
        if fen in legal_fens:
            # Check if we have to undo a previous move (sliding)
            if interaction_mode == Mode.NORMAL:
                if (play_mode == PlayMode.USER_WHITE and game.turn == chess.BLACK) or \
                        (play_mode == PlayMode.USER_BLACK and game.turn == chess.WHITE):
                    stop_search()
                    if game.move_stack:
                        game.pop()
            legal_moves = list(game.legal_moves)
            time_control.add_inc(game.turn)
            Observable.fire(Event.USER_MOVE(move=legal_moves[legal_fens.index(fen)]))
        elif fen == last_computer_fen:  # Player had done the computer move on the board
            last_computer_fen = None
            if check_game_state(game, play_mode) and ((interaction_mode == Mode.NORMAL) or (interaction_mode == Mode.REMOTE)):
                # finally reset all alternative moves see: handle_move()
                nonlocal searchmoves
                searchmoves.reset()
                time_control.add_inc(not game.turn)
                DisplayMsg.show(Message.COMPUTER_MOVE_DONE_ON_BOARD())
                if time_control.mode != TimeMode.FIXED:
                    DisplayMsg.show(Message.RUN_CLOCK(turn=game.turn, time_control=time_control, callback=timecontrol_callback))
                    # time_control.start(game.turn)
        else:  # Check if this a a previous legal position and allow user to restart from this position
            game_history = copy.deepcopy(game)
            while game_history.move_stack:
                game_history.pop()
                if (play_mode == PlayMode.USER_WHITE and game_history.turn == chess.WHITE) \
                        or (play_mode == PlayMode.USER_BLACK and game_history.turn == chess.BLACK) \
                        or (interaction_mode == Mode.OBSERVE) or (interaction_mode == Mode.KIBITZ) \
                        or (interaction_mode == Mode.REMOTE) or (interaction_mode == Mode.ANALYSIS):
                    if game_history.board_fen() == fen:
                        logging.debug("Legal Fens root       : " + str(legal_fens.root))
                        logging.debug("Current game FEN      : " + str(game.fen()))
                        logging.debug("Undoing game until FEN: " + fen)
                        stop_search()
                        while len(game_history.move_stack) < len(game.move_stack):
                            game.pop()
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                        DisplayMsg.show(Message.USER_TAKE_BACK())
                        legal_fens = compute_legal_fens(game)
                        break
        return legal_fens

    def set_wait_state():
        if interaction_mode == Mode.NORMAL:
            nonlocal play_mode
            play_mode = PlayMode.USER_WHITE if game.turn == chess.WHITE else PlayMode.USER_BLACK

    def handle_move(result, game):
        move = result.bestmove
        fen = game.fen()
        game.push(move)
        nonlocal last_computer_fen
        nonlocal searchmoves
        last_computer_fen = None
        if interaction_mode == Mode.NORMAL:
            stop_clock()
            # If UserMove: reset all alternative moves
            # If ComputerMove: disallow this move, and finally reset all if DONE_ON_BOARD event @see: process_fen()
            if (play_mode == PlayMode.USER_WHITE and game.turn == chess.WHITE)\
                    or (play_mode == PlayMode.USER_BLACK and game.turn == chess.BLACK):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                text = Message.COMPUTER_MOVE(result=result, fen=fen, game=game.copy(), time_control=time_control)
                DisplayMsg.show(text)
            else:
                searchmoves.reset()
                DisplayMsg.show(Message.USER_MOVE(move=move, game=game.copy()))
                if check_game_state(game, play_mode):
                    think(game, time_control)

        elif interaction_mode == Mode.REMOTE:
            stop_search_and_clock()
            # If UserMove: reset all alternative moves
            # If Remote Move: same process as for computer move above
            if (play_mode == PlayMode.USER_WHITE and game.turn == chess.WHITE)\
                    or (play_mode == PlayMode.USER_BLACK and game.turn == chess.BLACK):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                text = Message.COMPUTER_MOVE(result=result, fen=fen, game=game.copy(), time_control=time_control)
                DisplayMsg.show(text)
            else:
                searchmoves.reset()
                DisplayMsg.show(Message.USER_MOVE(move=move, game=game.copy()))
                if check_game_state(game, play_mode):
                    observe(game, time_control)

        elif interaction_mode == Mode.OBSERVE:
            stop_search_and_clock()
            DisplayMsg.show(Message.REVIEW_MOVE(move=move, fen=fen, game=game.copy(), mode=interaction_mode))
            if check_game_state(game, play_mode):
                observe(game, time_control)

        elif interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
            stop_search()
            DisplayMsg.show(Message.REVIEW_MOVE(move=move, fen=fen, game=game.copy(), mode=interaction_mode))
            if check_game_state(game, play_mode):
                analyse(game)

        return game

    # Enable garbage collection - needed for engine swapping as objects orphaned
    gc.enable()

    # Command line argument parsing
    parser = configargparse.ArgParser(default_config_files=[os.path.join(os.path.dirname(__file__), "picochess.ini")])
    parser.add_argument("-e", "--engine", type=str, help="UCI engine executable path", default=None)
    parser.add_argument("-d", "--dgt-port", type=str,
                        help="enable dgt board on the given serial port such as /dev/ttyUSB0")
    parser.add_argument("-b", "--book", type=str, help="Opening book - full name of book in 'books' folder",
                        default='h-varied.bin')
    parser.add_argument("-g", "--enable-gaviota", action='store_true', help="enable gavoita tablebase probing")
    parser.add_argument("-leds", "--enable-revelation-leds", action='store_true', help="enable Revelation leds")
    parser.add_argument("-hs", "--hash-size", type=int, help="hashtable size in MB (default:64)", default=64)
    parser.add_argument("-t", "--threads", type=int, help="number of engine threads (default:1)", default=1)
    parser.add_argument("-l", "--log-level", choices=['notset', 'debug', 'info', 'warning', 'error', 'critical'],
                        default='warning', help="logging level")
    parser.add_argument("-lf", "--log-file", type=str, help="log to the given file")
    parser.add_argument("-r", "--remote", type=str, help="remote server running the engine")
    parser.add_argument("-u", "--user", type=str, help="remote user on server running the engine")
    parser.add_argument("-p", "--password", type=str, help="password for the remote user")
    parser.add_argument("-sk", "--server-key", type=str, help="key file used to connect to the remote server")
    parser.add_argument("-pgn", "--pgn-file", type=str, help="pgn file used to store the games", default='games.pgn')
    parser.add_argument("-pgn_u", "--pgn-user", type=str, help="user name for the pgn file", default=None)
    parser.add_argument("-ar", "--auto-reboot", action='store_true', help="reboot system after update")
    parser.add_argument("-web", "--web-server", dest="web_server_port", nargs="?", const=80, type=int, metavar="PORT",
                        help="launch web server")
    parser.add_argument("-mail", "--email", type=str, help="email used to send pgn files", default=None)
    parser.add_argument("-mail_s", "--smtp_server", type=str, help="Adress of email server", default=None)
    parser.add_argument("-mail_u", "--smtp_user", type=str, help="Username for email server", default=None)
    parser.add_argument("-mail_p", "--smtp_pass", type=str, help="Password for email server", default=None)
    parser.add_argument("-mail_enc", "--smtp_encryption", action='store_true',
                        help="use ssl encryption connection to smtp-Server")
    parser.add_argument("-mk", "--mailgun-key", type=str, help="key used to send emails via Mailgun Webservice",
                        default=None)
    parser.add_argument("-uci", "--uci-option", type=str, help="pass an UCI option to the engine (name;value)",
                        default=None)
    parser.add_argument("-beep", "--beep-level", type=int, help="sets a beep level from 0(=no beeps) to 15(=all beeps)",
                        default=0x0f)
    parser.add_argument("-uvoice", "--user-voice", type=str, help="voice for user", default=None)
    parser.add_argument("-cvoice", "--computer-voice", type=str, help="voice for computer", default=None)
    parser.add_argument("-inet", "--enable-internet", action='store_true', help="enable internet lookups")
    parser.add_argument("-nookmove", "--disable-ok-move", action='store_true', help="disable ok move messages")
    parser.add_argument("-v", "--version", action='version', version='%(prog)s version {}'.format(version),
                        help="show current version", default=None)
    parser.add_argument("-pi", "--dgtpi", action='store_true', help="use the dgtpi hardware")
    parser.add_argument("-lang", "--language", choices=['en', 'de', 'nl', 'fr', 'es'], default='en', help="picochess language")
    parser.add_argument("-c", "--console", action='store_true', help="use console interface")

    args = parser.parse_args()
    if args.engine is None:
        el = read_engine_ini()
        args.engine = el[0][0]  # read the first engine path and use it as standard
    else:
        args.engine = which(args.engine)

    # Enable logging
    if args.log_file:
        handler = RotatingFileHandler('logs' + os.sep + args.log_file, maxBytes=1024*1024, backupCount=9)
        logging.basicConfig(level=getattr(logging, args.log_level.upper()),
                            format='%(asctime)s.%(msecs)03d %(levelname)5s %(module)10s - %(funcName)s: %(message)s',
                            datefmt="%Y-%m-%d %H:%M:%S", handlers=[handler])
    logging.getLogger("chess.uci").setLevel(logging.INFO)  # don't want to get so many python-chess uci messages

    logging.debug('#'*20 + ' PicoChess v' + version + ' ' + '#'*20)
    # log the startup parameters but hide the password fields
    p = copy.copy(vars(args))
    p['mailgun_key'] = p['server_key'] = p['password'] = p['smtp_pass'] = '******'
    logging.debug('startup parameters: {}'.format(p))

    # Update
    if args.enable_internet:
        update_picochess(args.auto_reboot)

    gaviota = None
    if args.enable_gaviota:
        try:
            gaviota = chess.gaviota.open_tablebases('tablebases/gaviota')
            logging.debug('Tablebases gaviota loaded')
        except OSError:
            logging.error('Tablebases gaviota doesnt exist')
            gaviota = None

    # This class talks to DgtHw/DgtPi or DgtVr
    dgttranslate = DgtTranslate(args.beep_level, args.language)
    DgtDisplay(args.disable_ok_move, dgttranslate).start()

    # Launch web server
    if args.web_server_port:
        WebServer(args.web_server_port).start()

    if args.console:
        # Enable keyboard input and terminal display
        logging.debug("starting picochess with virtual DGT board")
        KeyboardInput().start()
        TerminalDisplay().start()
        dgthardware = DgtVr(args.enable_revelation_leds)
    else:
        # Connect to DGT board
        logging.debug("starting picochess with DGT board on [%s]", args.dgt_port)
        dgtserial = DgtSerial(args.dgt_port)
        if args.dgtpi:
            dgthardware = DgtPi(dgtserial, dgttranslate, args.enable_revelation_leds)
        else:
            dgthardware = DgtHw(dgtserial, dgttranslate, args.enable_revelation_leds)

    # Start the show
    dgthardware.start()
    dgthardware.startup()

    # Save to PGN
    PgnDisplay(
        args.pgn_file, net=args.enable_internet, email=args.email, mailgun_key=args.mailgun_key,
        smtp_server=args.smtp_server, smtp_user=args.smtp_user,
        smtp_pass=args.smtp_pass, smtp_encryption=args.smtp_encryption).start()
    if args.pgn_user:
        user_name = args.pgn_user
    else:
        if args.email:
            user_name = args.email.split('@')[0]
        else:
            user_name = "Player"

    # Create ChessTalker for speech output
    talker = None
    if args.user_voice or args.computer_voice:
        logging.debug("initializing ChessTalker [%s, %s]", str(args.user_voice), str(args.computer_voice))
        talker = chesstalker.chesstalker.ChessTalker(args.user_voice, args.computer_voice)
        talker.start()
    else:
        logging.debug("ChessTalker disabled")

    # Gentlemen, start your engines...
    engine = uci.Engine(args.engine, hostname=args.remote, username=args.user,
                        key_file=args.server_key, password=args.password)
    try:
        engine_name = engine.get().name
    except AttributeError:
        logging.error("no engines started")
        sys.exit(-1)

    # Startup - internal
    game = chess.Board()  # Create the current game
    legal_fens = compute_legal_fens(game)  # Compute the legal FENs
    all_books = get_opening_books()
    try:
        book_index = [book[1] for book in all_books].index('books/' + args.book)
    except ValueError:
        logging.warning("selected book not present, defaulting to %s", all_books[7][1])
        book_index = 7
    bookreader = chess.polyglot.open_reader(all_books[book_index][1])
    searchmoves = AlternativeMover()
    interaction_mode = Mode.NORMAL
    play_mode = PlayMode.USER_WHITE
    time_control = TimeControl(TimeMode.BLITZ, minutes_per_game=5)
    last_computer_fen = None
    game_declared = False  # User declared resignation or draw

    system_info_thread = threading.Timer(0, display_system_info)
    system_info_thread.start()
    engine_startup()  # send the args options to the engine

    # Startup - external
    text = Dgt.DISPLAY_TEXT(l=None, m='bl   5', s=None, beep=False, duration=0)
    DisplayMsg.show(Message.STARTUP_INFO(info={"interaction_mode": interaction_mode, "play_mode": play_mode,
                                               "book": all_books[book_index][1], "book_index": book_index,
                                               "time_text": text}))
    DisplayMsg.show(Message.UCI_OPTION_LIST(options=engine.options))
    DisplayMsg.show(Message.ENGINE_STARTUP(shell=engine.get_shell(), path=engine.get_path(),
                                           has_levels=engine.has_levels(), has_960=engine.has_chess960()))

    # Event loop
    while True:
        try:
            event = evt_queue.get()
        except queue.Empty:
            pass
        else:
            logging.debug('received event from evt_queue: %s', event)
            for case in switch(event):
                if case(EventApi.FEN):
                    legal_fens = process_fen(event.fen, legal_fens)
                    break

                if case(EventApi.KEYBOARD_MOVE):
                    move = event.move
                    logging.debug('keyboard move [%s]', move)
                    if move not in game.legal_moves:
                        logging.warning('illegal move [%s]', move)
                    else:
                        g = copy.deepcopy(game)
                        g.push(move)
                        legal_fens = process_fen(g.board_fen(), legal_fens)
                    break

                if case(EventApi.USER_MOVE):
                    move = event.move
                    logging.debug('user move [%s]', move)
                    if move not in game.legal_moves:
                        logging.warning('Illegal move [%s]', move)
                    else:
                        result = chess.uci.BestMove(bestmove=move, ponder=None)
                        game = handle_move(result, game)
                        # if check_game_state(game, interaction_mode):
                        legal_fens = compute_legal_fens(game)
                    break

                if case(EventApi.LEVEL):
                    logging.debug("setting engine to level %i", event.level)
                    if engine.level(event.level):
                        engine.send()
                        DisplayMsg.show(Message.LEVEL(level=event.level, level_text=event.level_text))
                    break

                if case(EventApi.NEW_ENGINE):
                    old_path = engine.path
                    engine_shutdown = True
                    # Stop the old engine cleanly
                    engine.stop()
                    # Closeout the engine process and threads
                    # The all return non-zero error codes, 0=success
                    if engine.quit():  # Ask nicely
                        if engine.terminate():  # If you won't go nicely.... 
                            if engine.kill():  # Right that does it!
                                logging.error('engine shutdown failure')
                                DisplayMsg.show(Message.ENGINE_FAIL())
                                engine_shutdown = False
                    if engine_shutdown:
                        # Load the new one and send args.
                        # Local engines only
                        engine_fallback = False
                        engine = uci.Engine(event.eng[0])
                        try:
                            engine_name = engine.get().name
                        except AttributeError:
                            # New engine failed to start, restart old engine
                            logging.error("new engine failed to start, reverting to %s", old_path)
                            engine_fallback = True
                            engine = uci.Engine(old_path)
                            try:
                                engine_name = engine.get().name
                            except AttributeError:
                                # Help - old engine failed to restart. There is no engine
                                logging.error("no engines started")
                                sys.exit(-1)
                        # Schedule cleanup of old objects
                        gc.collect()
                        # Restore options - this doesn't deal with any
                        # supplementary uci options sent 'in game', see event.UCI_OPTION_SET
                        engine_startup()
                        # Send user selected engine level to new engine
                        if event.level and engine.level(event.level):
                            engine.send()
                            DisplayMsg.show(Message.LEVEL(level=event.level, level_text=event.level_text))
                        # All done - rock'n'roll
                        if not engine_fallback:
                            DisplayMsg.show(Message.ENGINE_READY(eng=event.eng, engine_name=engine_name,
                                                                 eng_text=event.eng_text,
                                                                 has_levels=engine.has_levels(),
                                                                 has_960=engine.has_chess960()))
                        else:
                            DisplayMsg.show(Message.ENGINE_FAIL())
                        set_wait_state()
                        DisplayMsg.show(Message.WAIT_STATE())
                        # Go back to analysing or observing
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                    break

                if case(EventApi.SETUP_POSITION):
                    logging.debug("setting up custom fen: {0}".format(event.fen))
                    if engine.has_chess960():
                        engine.option('UCI_Chess960', event.uci960)
                        engine.send()
                    else:  # start normal new game if engine can't handle the user wish
                        event.uci960 = False
                        logging.warning('engine doesnt support 960 mode')
                    if game.move_stack:
                        if game.is_game_over() or game_declared:
                            custom_fen = getattr(game, 'custom_fen', None)
                            DisplayMsg.show(Message.GAME_ENDS(result=GameResult.ABORT, play_mode=play_mode,
                                                              game=copy.deepcopy(game), custom_fen=custom_fen))
                    game = chess.Board(event.fen, event.uci960)
                    game.custom_fen = event.fen
                    legal_fens = compute_legal_fens(game)
                    stop_search_and_clock()
                    time_control.reset()
                    interaction_mode = Mode.NORMAL
                    last_computer_fen = None
                    searchmoves.reset()
                    DisplayMsg.show(Message.START_NEW_GAME(time_control=time_control))
                    game_declared = False
                    set_wait_state()
                    DisplayMsg.show(Message.WAIT_STATE())
                    break

                if case(EventApi.STARTSTOP_THINK):
                    if engine.is_thinking() and (interaction_mode != Mode.REMOTE):
                        stop_clock()
                        engine.stop(show_best=True)
                    else:
                        play_mode = PlayMode.USER_WHITE if play_mode == PlayMode.USER_BLACK else PlayMode.USER_BLACK
                        DisplayMsg.show(Message.PLAY_MODE(play_mode=play_mode))
                        if check_game_state(game, play_mode) and (interaction_mode != Mode.REMOTE):
                            time_control.reset_start_time()
                            think(game, time_control)
                    break

                if case(EventApi.ALTERNATIVE_MOVE):
                    game.pop()
                    DisplayMsg.show(Message.ALTERNATIVE_MOVE())
                    think(game, time_control)
                    break

                if case(EventApi.STARTSTOP_CLOCK):
                    if time_control.is_ticking():
                        stop_clock()
                    else:
                        time_control.add_inc(game.turn)
                        DisplayMsg.show(Message.RUN_CLOCK(turn=game.turn, time_control=time_control, callback=timecontrol_callback))
                        # time_control.start(game.turn)
                    break

                if case(EventApi.NEW_GAME):
                    if game.move_stack:
                        logging.debug("starting a new game")
                        if not (game.is_game_over() or game_declared):
                            custom_fen = getattr(game, 'custom_fen', None)
                            DisplayMsg.show(Message.GAME_ENDS(result=GameResult.ABORT, play_mode=play_mode,
                                                              game=copy.deepcopy(game), custom_fen=custom_fen))
                        game = chess.Board()
                    legal_fens = compute_legal_fens(game)
                    # interaction_mode = Mode.NORMAL @todo
                    last_computer_fen = None
                    stop_search_and_clock()
                    time_control.reset()
                    searchmoves.reset()
                    DisplayMsg.show(Message.START_NEW_GAME(time_control=time_control))
                    game_declared = False
                    set_wait_state()
                    DisplayMsg.show(Message.WAIT_STATE())
                    break

                if case(EventApi.DRAWRESIGN):
                    if not game_declared:  # in case user leaves kings in place while moving other pieces
                        stop_search_and_clock()
                        custom_fen = getattr(game, 'custom_fen', None)
                        DisplayMsg.show(Message.GAME_ENDS(result=event.result, play_mode=play_mode,
                                                          game=copy.deepcopy(game), custom_fen=custom_fen))
                        game_declared = True
                    break

                if case(EventApi.REMOTE_MOVE):
                    if interaction_mode == Mode.REMOTE:
                        bm = chess.uci.BestMove(bestmove=chess.Move.from_uci(event.move), ponder=None)
                        game = handle_move(bm, game)
                        legal_fens = compute_legal_fens(game)
                    break

                if case(EventApi.BEST_MOVE):
                    if event.inbook:
                        DisplayMsg.show(Message.BOOK_MOVE(result=event.result))
                    game = handle_move(event.result, game)
                    legal_fens = compute_legal_fens(game)
                    break

                if case(EventApi.NEW_PV):
                    if interaction_mode == Mode.NORMAL:
                        pass
                    else:
                        DisplayMsg.show(Message.NEW_PV(pv=event.pv, mode=interaction_mode, fen=game.fen()))
                    break

                if case(EventApi.NEW_SCORE):
                    if event.score == 'book':
                        score = 'book'
                    elif event.score == 'tb':
                        score = 'tb {0}'.format(event.mate)
                    else:
                        try:
                            score = int(event.score)
                            if game.turn == chess.BLACK:
                                score *= -1
                        except ValueError:
                            score = event.score
                            logging.debug('could not convert score ' + score)
                        except TypeError:
                            score = 'm {0}'.format(event.mate)
                    DisplayMsg.show(Message.NEW_SCORE(score=score, mate=event.mate, mode=interaction_mode))
                    break

                if case(EventApi.SET_INTERACTION_MODE):
                    if interaction_mode == Mode.NORMAL or interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                        stop_clock()  # only stop, if the clock is really running
                    interaction_mode = event.mode
                    if engine.is_thinking():
                        stop_search()  # dont need to stop, if pondering
                    if engine.is_pondering() and interaction_mode == Mode.NORMAL:
                        stop_search()  # if change from ponder modes to game, also stops the pondering
                    set_wait_state()
                    DisplayMsg.show(Message.INTERACTION_MODE(mode=event.mode, mode_text=event.mode_text))
                    break

                if case(EventApi.SET_OPENING_BOOK):
                    logging.debug("changing opening book [%s]", event.book[1])
                    bookreader = chess.polyglot.open_reader(event.book[1])
                    DisplayMsg.show(Message.OPENING_BOOK(book_name=event.book[0], book_text=event.book_text))
                    break

                if case(EventApi.SET_TIME_CONTROL):
                    time_control = event.time_control
                    DisplayMsg.show(Message.TIME_CONTROL(time_text=event.time_text))
                    break

                if case(EventApi.OUT_OF_TIME):
                    stop_search_and_clock()
                    custom_fen = getattr(game, 'custom_fen', None)
                    DisplayMsg.show(Message.GAME_ENDS(result=GameResult.OUT_OF_TIME, play_mode=play_mode,
                                                      game=copy.deepcopy(game), custom_fen=custom_fen))
                    break

                if case(EventApi.UCI_OPTION_SET):
                    # Nowhere calls this yet, but they will need to be saved for engine restart
                    engine.option(event.name, event.value)
                    break

                if case(EventApi.SHUTDOWN):
                    if talker:
                        talker.say_event(event)
                    custom_fen = getattr(game, 'custom_fen', None)
                    DisplayMsg.show(Message.GAME_ENDS(result=GameResult.ABORT, play_mode=play_mode,
                                                      game=copy.deepcopy(game), custom_fen=custom_fen))
                    shutdown(args.dgtpi)
                    break

                if case(EventApi.REBOOT):
                    if talker:
                        talker.say_event(event)
                    custom_fen = getattr(game, 'custom_fen', None)
                    DisplayMsg.show(Message.GAME_ENDS(result=GameResult.ABORT, play_mode=play_mode,
                                                      game=copy.deepcopy(game), custom_fen=custom_fen))
                    reboot()
                    break

                if case(EventApi.DGT_BUTTON):
                    DisplayMsg.show(Message.DGT_BUTTON(button=event.button))
                    break

                if case(EventApi.DGT_FEN):
                    DisplayMsg.show(Message.DGT_FEN(fen=event.fen))
                    break

                if case(EventApi.DGT_CLOCK_STARTED):
                    if event.callback:
                        logging.debug('callback started {}'.format(event.callback))
                        event.callback()
                        logging.debug('callback ended {}'.format(event.callback))
                    else:
                        logging.debug('callback is not set')
                    break

                if case():  # Default
                    logging.warning("event not handled : [%s]", event)

            evt_queue.task_done()
Exemple #6
0
 def _process_clock_start(self, message):
     self.time_control = TimeControl(**message.tc_init)
     side = ClockSide.LEFT if (
         message.turn == chess.WHITE
     ) != self.dgtmenu.get_flip_board() else ClockSide.RIGHT
     self._set_clock(side=side, devs=message.devs)
Exemple #7
0
 def _process_time_control(self, message):
     wait = not self.dgtmenu.get_confirm() or not message.show_ok
     if wait:
         DgtObserver.fire(message.time_text)
     self.time_control = TimeControl(**message.tc_init)
     self._set_clock()
Exemple #8
0
    def __init__(self,whiteEng, blackEng, **kwargs):
        """
        The basic GameSession class.
        whiteEng and blackEng are the two engines, with respective colors

        Note: All times are in milliseconds as per UCI protocol, unless
        time_modifier (see the TimeControl class) is used.

        start_time is the time that the engines are given on the clock,
        not including this parameter will result in an untimed game. A tuple
        can be fed to allow different starting times like so:
        start_time = (<white start time>,<black start time>)

        inc is the incremented time value after each turn. Like start_time,
        it can be fed a tuple for different incrementations to each engine.

        time is a tuple that combines start_time and inc 
        in the following format: time=(start_time,inc).

        If time is a single number (int, float, etc.), the value it is passed
        will be considered as start_time, and inc as 0.

        The position keyword argument is used to pass a FEN value as the starting position,
        when not used, the default starting position (startpos) is used.
        """

        self.white = whiteEng
        self.black = blackEng
        self.gamehistory = ""
        self.timed = False

        self.tied = False
        self.tiereason = ''
        self.winner = None

        #Process TimeControl Arguments
        tc_dict = {}
            #Pass any argument in tc_dict to time control.
        if 'tc_dict' in kwargs:
            tc_dict.update(kwargs['tc_dict'])
        if 'time_unit' in kwargs:
            tc_dict['time_unit'] = kwargs['time_unit']
            

        if 'time' in kwargs:
            self.timeControl = TimeControl(*kwargs['time'], **tc_dict)
            self.timed = True
        elif 'start_time' in kwargs:
            inc = 0
            if 'inc' in kwargs:
                inc = kwargs['inc']
            self.timeControl = TimeControl(kwargs['start_time'],inc, **tc_dict)
            self.timed = True

        starting_position = 'startpos'
        if 'position' in kwargs:
            starting_position = kwargs['position']
        
        
        #Tie testing (repeated moves)
        self.played_positions = {}
        self.FENOp = FENOperator(starting_position)
        self.played_positions[self.FENOp.getPositionOnly()] = 1
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))
Exemple #9
0
class GameSession:
    def __init__(self,whiteEng, blackEng, **kwargs):
        """
        The basic GameSession class.
        whiteEng and blackEng are the two engines, with respective colors

        Note: All times are in milliseconds as per UCI protocol, unless
        time_modifier (see the TimeControl class) is used.

        start_time is the time that the engines are given on the clock,
        not including this parameter will result in an untimed game. A tuple
        can be fed to allow different starting times like so:
        start_time = (<white start time>,<black start time>)

        inc is the incremented time value after each turn. Like start_time,
        it can be fed a tuple for different incrementations to each engine.

        time is a tuple that combines start_time and inc 
        in the following format: time=(start_time,inc).

        If time is a single number (int, float, etc.), the value it is passed
        will be considered as start_time, and inc as 0.

        The position keyword argument is used to pass a FEN value as the starting position,
        when not used, the default starting position (startpos) is used.
        """

        self.white = whiteEng
        self.black = blackEng
        self.gamehistory = ""
        self.timed = False

        self.tied = False
        self.tiereason = ''
        self.winner = None

        #Process TimeControl Arguments
        tc_dict = {}
            #Pass any argument in tc_dict to time control.
        if 'tc_dict' in kwargs:
            tc_dict.update(kwargs['tc_dict'])
        if 'time_unit' in kwargs:
            tc_dict['time_unit'] = kwargs['time_unit']
            

        if 'time' in kwargs:
            self.timeControl = TimeControl(*kwargs['time'], **tc_dict)
            self.timed = True
        elif 'start_time' in kwargs:
            inc = 0
            if 'inc' in kwargs:
                inc = kwargs['inc']
            self.timeControl = TimeControl(kwargs['start_time'],inc, **tc_dict)
            self.timed = True

        starting_position = 'startpos'
        if 'position' in kwargs:
            starting_position = kwargs['position']
        
        
        #Tie testing (repeated moves)
        self.played_positions = {}
        self.FENOp = FENOperator(starting_position)
        self.played_positions[self.FENOp.getPositionOnly()] = 1
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))

    def get_move(self, engine):
        command = 'go'
        
        if self.timed:
            wtime, btime = self.timeControl.clocks
            winc, binc = self.timeControl.increment
            command += ' wtime {0} btime {1} winc {2} binc {3}'
            command = command.format(wtime, btime, winc, binc)
        
        engine.write('{0}\n'.format(command))
        while True:
            for line in engine.readAll():
                if "bestmove" in line:
                    return line[9:14]
        
    def update_board(self,position):
        if "none" in position:
            return False
        self.tied = self.check_tie(position) #Also updates FENOperator
        self.gamehistory += " {0}".format(position)
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))
        return True

    def write_all_engines(self,message):
        self.white.write(message)
        self.black.write(message)

    def play(self):
        engines = [self.white,self.black]
        turn = 0
        while True:
            if self.timed:
                self.timeControl.start(turn)

            newmove = self.get_move(engines[turn])
            if not self.update_board(newmove):
                print self.gamehistory
                if turn == 1:
                    print "White checkmates"
                else:
                    print "Black checkmates"
                break

            if self.tied:
                print self.gamehistory
                print "Draw by {0}".format(self.tiereason)
                break

            # Check for time
            if self.timed:
                if not self.timeControl.stop():
                    print self.gamehistory

                    #Print "loses on time" message.
                    if turn == 0:
                        print "White loses on time"
                    else:
                        print "Black loses on time"
                    break
                        
            turn = -(turn -1) # 0 -> 1; 1 -> 0

    def check_tie(self, move):
        """
        Checks for tie and updates FENOperator
        """
        if self.white.score == 0 and self.black.score == 0:
            return True

        self.FENOp.do_move(move)
        newstr = self.FENOp.getPositionOnly()
        
        if newstr in self.played_positions:
            self.played_positions[newstr] += 1
            if self.played_positions[newstr] >= 3:
                self.tiereason = '3 move repeat'
                return True
        else:
            self.played_positions[newstr] = 1

        if int(self.FENOp.cp_clock) >= 100:
            self.tiereason = '50 moves without capture or pawn moving'
            return True
        
        return False
Exemple #10
0
    def __init__(self, whiteEng, blackEng, **kwargs):
        """
        The basic GameSession class.
        whiteEng and blackEng are the two engines, with respective colors

        Note: All times are in milliseconds as per UCI protocol, unless
        time_modifier (see the TimeControl class) is used.

        start_time is the time that the engines are given on the clock,
        not including this parameter will result in an untimed game. A tuple
        can be fed to allow different starting times like so:
        start_time = (<white start time>,<black start time>)

        inc is the incremented time value after each turn. Like start_time,
        it can be fed a tuple for different incrementations to each engine.

        time is a tuple that combines start_time and inc 
        in the following format: time=(start_time,inc).

        If time is a single number (int, float, etc.), the value it is passed
        will be considered as start_time, and inc as 0.

        The position keyword argument is used to pass a FEN value as the starting position,
        when not used, the default starting position (startpos) is used.
        """

        self.white = whiteEng
        self.black = blackEng
        self.gamehistory = ""
        self.timed = False

        self.tied = False
        self.tiereason = ''
        self.winner = None

        #Process TimeControl Arguments
        tc_dict = {}
        #Pass any argument in tc_dict to time control.
        if 'tc_dict' in kwargs:
            tc_dict.update(kwargs['tc_dict'])
        if 'time_unit' in kwargs:
            tc_dict['time_unit'] = kwargs['time_unit']

        if 'time' in kwargs:
            self.timeControl = TimeControl(*kwargs['time'], **tc_dict)
            self.timed = True
        elif 'start_time' in kwargs:
            inc = 0
            if 'inc' in kwargs:
                inc = kwargs['inc']
            self.timeControl = TimeControl(kwargs['start_time'], inc,
                                           **tc_dict)
            self.timed = True

        starting_position = 'startpos'
        if 'position' in kwargs:
            starting_position = kwargs['position']

        #Tie testing (repeated moves)
        self.played_positions = {}
        self.FENOp = FENOperator(starting_position)
        self.played_positions[self.FENOp.getPositionOnly()] = 1
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))
Exemple #11
0
class GameSession:
    def __init__(self, whiteEng, blackEng, **kwargs):
        """
        The basic GameSession class.
        whiteEng and blackEng are the two engines, with respective colors

        Note: All times are in milliseconds as per UCI protocol, unless
        time_modifier (see the TimeControl class) is used.

        start_time is the time that the engines are given on the clock,
        not including this parameter will result in an untimed game. A tuple
        can be fed to allow different starting times like so:
        start_time = (<white start time>,<black start time>)

        inc is the incremented time value after each turn. Like start_time,
        it can be fed a tuple for different incrementations to each engine.

        time is a tuple that combines start_time and inc 
        in the following format: time=(start_time,inc).

        If time is a single number (int, float, etc.), the value it is passed
        will be considered as start_time, and inc as 0.

        The position keyword argument is used to pass a FEN value as the starting position,
        when not used, the default starting position (startpos) is used.
        """

        self.white = whiteEng
        self.black = blackEng
        self.gamehistory = ""
        self.timed = False

        self.tied = False
        self.tiereason = ''
        self.winner = None

        #Process TimeControl Arguments
        tc_dict = {}
        #Pass any argument in tc_dict to time control.
        if 'tc_dict' in kwargs:
            tc_dict.update(kwargs['tc_dict'])
        if 'time_unit' in kwargs:
            tc_dict['time_unit'] = kwargs['time_unit']

        if 'time' in kwargs:
            self.timeControl = TimeControl(*kwargs['time'], **tc_dict)
            self.timed = True
        elif 'start_time' in kwargs:
            inc = 0
            if 'inc' in kwargs:
                inc = kwargs['inc']
            self.timeControl = TimeControl(kwargs['start_time'], inc,
                                           **tc_dict)
            self.timed = True

        starting_position = 'startpos'
        if 'position' in kwargs:
            starting_position = kwargs['position']

        #Tie testing (repeated moves)
        self.played_positions = {}
        self.FENOp = FENOperator(starting_position)
        self.played_positions[self.FENOp.getPositionOnly()] = 1
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))

    def get_move(self, engine):
        command = 'go'

        if self.timed:
            wtime, btime = self.timeControl.clocks
            winc, binc = self.timeControl.increment
            command += ' wtime {0} btime {1} winc {2} binc {3}'
            command = command.format(wtime, btime, winc, binc)

        engine.write('{0}\n'.format(command))
        while True:
            for line in engine.readAll():
                if "bestmove" in line:
                    return line[9:14]

    def update_board(self, position):
        if "none" in position:
            return False
        self.tied = self.check_tie(position)  #Also updates FENOperator
        self.gamehistory += " {0}".format(position)
        self.write_all_engines('position fen {0}\n'.format(self.FENOp))
        return True

    def write_all_engines(self, message):
        self.white.write(message)
        self.black.write(message)

    def play(self):
        engines = [self.white, self.black]
        turn = 0
        while True:
            if self.timed:
                self.timeControl.start(turn)

            newmove = self.get_move(engines[turn])
            if not self.update_board(newmove):
                print self.gamehistory
                if turn == 1:
                    print "White checkmates"
                else:
                    print "Black checkmates"
                break

            if self.tied:
                print self.gamehistory
                print "Draw by {0}".format(self.tiereason)
                break

            # Check for time
            if self.timed:
                if not self.timeControl.stop():
                    print self.gamehistory

                    #Print "loses on time" message.
                    if turn == 0:
                        print "White loses on time"
                    else:
                        print "Black loses on time"
                    break

            turn = -(turn - 1)  # 0 -> 1; 1 -> 0

    def check_tie(self, move):
        """
        Checks for tie and updates FENOperator
        """
        if self.white.score == 0 and self.black.score == 0:
            return True

        self.FENOp.do_move(move)
        newstr = self.FENOp.getPositionOnly()

        if newstr in self.played_positions:
            self.played_positions[newstr] += 1
            if self.played_positions[newstr] >= 3:
                self.tiereason = '3 move repeat'
                return True
        else:
            self.played_positions[newstr] = 1

        if int(self.FENOp.cp_clock) >= 100:
            self.tiereason = '50 moves without capture or pawn moving'
            return True

        return False
Exemple #12
0
def main():
    def engine_startup():
        if "Hash" in engine.get().options:
            engine.option("Hash", args.hash_size)
        if "Threads" in engine.get().options:  # Stockfish
            engine.option("Threads", args.threads)
        if "Core Threads" in engine.get().options:  # Hiarcs
            engine.option("Core Threads", args.threads)
        if args.uci_option:
            for uci_option in args.uci_option.strip('"').split(";"):
                uci_parameter = uci_option.strip().split("=")
                engine.option(uci_parameter[0], uci_parameter[1])
        # send the options to the engine
        engine.send()
        # Notify other display processes
        Display.show(Message.UCI_OPTION_LIST, options=engine.get().options)

    def display_system_info():
        if args.enable_internet:
            place = get_location()
            addr = get_ip()
        else:
            place = "?"
            addr = "?"
        Display.show(
            Message.SYSTEM_INFO,
            info={
                "version": version,
                "location": place,
                "books": get_opening_books(),
                "ip": addr,
                "engine_name": engine_name,
                "user_name": user_name,
            },
        )

    def compute_legal_fens(g):
        """
        Computes a list of legal FENs for the given game.
        Also stores the initial position in the 'root' attribute.
        :param g: The game
        :return: A list of legal FENs, and the root FEN
        """

        class FenList(list):
            def __init__(self, *args):
                list.__init__(self, *args)
                self.root = ""

        fens = FenList()
        for move in g.legal_moves:
            g.push(move)
            fens.append(g.board_fen())
            g.pop()
        fens.root = g.board_fen()
        return fens

    def probe_tablebase(game):
        if not gaviota:
            return None
        score = gaviota.probe_dtm(game)
        if score:
            Observable.fire(Event.SCORE, score="tb", mate=score)
        return score

    def think(game, time):
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time)
        time.run(game.turn)

        book_move = searchmoves.book(bookreader, game)
        if book_move:
            Observable.fire(Event.BEST_MOVE, result=book_move, inbook=True)
            Observable.fire(Event.SCORE, score="book", mate=None)
        else:
            probe_tablebase(game)
            engine.position(copy.deepcopy(game))
            uci_dict = time.uci()
            uci_dict["searchmoves"] = searchmoves.all(game)
            engine.go(uci_dict)

    def analyse(game):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        probe_tablebase(game)
        engine.position(copy.deepcopy(game))
        engine.ponder()

    def observe(game, time):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time)
        time.run(game.turn)
        analyse(game)

    def stop_search():
        """
        Stop current search.
        :return:
        """
        engine.stop()

    def stop_clock():
        nonlocal time_control
        time_control.stop()
        Display.show(Message.STOP_CLOCK)

    def stop_search_and_clock():
        stop_clock()
        stop_search()

    def check_game_state(game, play_mode):
        """
        Check if the game has ended or not ; it also sends Message to Displays if the game has ended.
        :param game:
        :return: True is the game continues, False if it has ended
        """
        result = None
        if game.is_stalemate():
            result = GameResult.STALEMATE
        if game.is_insufficient_material():
            result = GameResult.INSUFFICIENT_MATERIAL
        if game.is_seventyfive_moves():
            result = GameResult.SEVENTYFIVE_MOVES
        if game.is_fivefold_repetition():
            result = GameResult.FIVEFOLD_REPETITION
        if game.is_checkmate():
            result = GameResult.MATE

        if result is None:
            return True
        else:
            Display.show(Message.GAME_ENDS, result=result, play_mode=play_mode, game=copy.deepcopy(game))
            return False

    def process_fen(fen, legal_fens):
        if fen in legal_fens:
            # Check if we have to undo a previous move (sliding)
            if interaction_mode == Mode.GAME:
                if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.BLACK) or (
                    play_mode == PlayMode.PLAY_BLACK and game.turn == chess.WHITE
                ):
                    stop_search()
                    if game.move_stack:
                        game.pop()
            legal_moves = list(game.legal_moves)
            Observable.fire(Event.USER_MOVE, move=legal_moves[legal_fens.index(fen)])
        elif fen == last_computer_fen:  # Player had done the computer move on the board
            if check_game_state(game, play_mode) and (
                (interaction_mode == Mode.GAME) or (interaction_mode == Mode.REMOTE)
            ):
                # finally reset all alternative moves see: handle_move()
                nonlocal searchmoves
                searchmoves.reset()
                Display.show(Message.COMPUTER_MOVE_DONE_ON_BOARD)
                if time_control.mode != ClockMode.FIXED_TIME:
                    Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time_control)
                    time_control.run(game.turn)
        else:  # Check if this a a previous legal position and allow user to restart from this position
            game_history = copy.deepcopy(game)
            while game_history.move_stack:
                game_history.pop()
                if (
                    (play_mode == PlayMode.PLAY_WHITE and game_history.turn == chess.WHITE)
                    or (play_mode == PlayMode.PLAY_BLACK and game_history.turn == chess.BLACK)
                    or (interaction_mode == Mode.OBSERVE)
                    or (interaction_mode == Mode.KIBITZ)
                    or (interaction_mode == Mode.REMOTE)
                    or (interaction_mode == Mode.ANALYSIS)
                ):
                    if game_history.board_fen() == fen:
                        logging.debug("Legal Fens root       : " + str(legal_fens.root))
                        logging.debug("Current game FEN      : " + str(game.fen()))
                        logging.debug("Undoing game until FEN: " + fen)
                        stop_search()
                        while len(game_history.move_stack) < len(game.move_stack):
                            game.pop()
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                        Display.show(Message.USER_TAKE_BACK)
                        legal_fens = compute_legal_fens(game)
                        break
        return legal_fens

    def set_wait_state():
        if interaction_mode == Mode.GAME:
            nonlocal play_mode
            play_mode = PlayMode.PLAY_WHITE if game.turn == chess.WHITE else PlayMode.PLAY_BLACK

    def handle_move(result, game):
        move = result.bestmove
        fen = game.fen()
        game.push(move)
        nonlocal last_computer_fen
        nonlocal searchmoves
        last_computer_fen = None
        if interaction_mode == Mode.GAME:
            stop_clock()
            # If UserMove: reset all alternative moves
            # If ComputerMove: disallow this move, and finally reset all if DONE_ON_BOARD event @see: process_fen()
            if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.WHITE) or (
                play_mode == PlayMode.PLAY_BLACK and game.turn == chess.BLACK
            ):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                Display.show(Message.COMPUTER_MOVE, result=result, fen=fen, game=game.copy(), time_control=time_control)
            else:
                searchmoves.reset()
                Display.show(Message.USER_MOVE, move=move, game=game.copy())
                if check_game_state(game, play_mode):
                    think(game, time_control)

        elif interaction_mode == Mode.REMOTE:
            stop_search_and_clock()
            # If UserMove: reset all alternative moves
            # If Remote Move: same process as for computer move above
            if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.WHITE) or (
                play_mode == PlayMode.PLAY_BLACK and game.turn == chess.BLACK
            ):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                Display.show(Message.COMPUTER_MOVE, result=result, fen=fen, game=game.copy(), time_control=time_control)
            else:
                searchmoves.reset()
                Display.show(Message.USER_MOVE, move=move, game=game.copy())
                if check_game_state(game, play_mode):
                    observe(game, time_control)

        elif interaction_mode == Mode.OBSERVE:
            stop_search_and_clock()
            Display.show(Message.REVIEW_MODE_MOVE, move=move, fen=fen, game=game.copy(), mode=interaction_mode)
            if check_game_state(game, play_mode):
                observe(game, time_control)

        elif interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
            stop_search()
            Display.show(Message.REVIEW_MODE_MOVE, move=move, fen=fen, game=game.copy(), mode=interaction_mode)
            if check_game_state(game, play_mode):
                analyse(game)

        return game

    # Enable garbage collection - needed for engine swapping as objects orphaned
    gc.enable()

    # Command line argument parsing
    parser = configargparse.ArgParser(default_config_files=[os.path.join(os.path.dirname(__file__), "picochess.ini")])
    parser.add_argument("-e", "--engine", type=str, help="UCI engine executable path", default="engines/stockfish")
    parser.add_argument(
        "-d", "--dgt-port", type=str, help="enable dgt board on the given serial port such as /dev/ttyUSB0"
    )
    parser.add_argument(
        "-b", "--book", type=str, help="Opening book - full name of book in 'books' folder", default="h-varied.bin"
    )
    parser.add_argument("-g", "--enable-gaviota", action="store_true", help="enable gavoita tablebase probing")
    parser.add_argument("-leds", "--enable-dgt-board-leds", action="store_true", help="enable dgt board leds")
    parser.add_argument("-hs", "--hash-size", type=int, help="hashtable size in MB (default:64)", default=64)
    parser.add_argument("-t", "--threads", type=int, help="number of engine threads (default:1)", default=1)
    parser.add_argument(
        "-l",
        "--log-level",
        choices=["notset", "debug", "info", "warning", "error", "critical"],
        default="warning",
        help="logging level",
    )
    parser.add_argument("-lf", "--log-file", type=str, help="log to the given file")
    parser.add_argument("-r", "--remote", type=str, help="remote server running the engine")
    parser.add_argument("-u", "--user", type=str, help="remote user on server running the engine")
    parser.add_argument("-p", "--password", type=str, help="password for the remote user")
    parser.add_argument("-sk", "--server-key", type=str, help="key file used to connect to the remote server")
    parser.add_argument("-pgn", "--pgn-file", type=str, help="pgn file used to store the games", default="games.pgn")
    parser.add_argument("-pgn_u", "--pgn-user", type=str, help="user name for the pgn file", default=None)
    parser.add_argument("-ar", "--auto-reboot", action="store_true", help="reboot system after update")
    parser.add_argument(
        "-web",
        "--web-server",
        dest="web_server_port",
        nargs="?",
        const=80,
        type=int,
        metavar="PORT",
        help="launch web server",
    )
    parser.add_argument("-mail", "--email", type=str, help="email used to send pgn files", default=None)
    parser.add_argument("-mail_s", "--smtp_server", type=str, help="Adress of email server", default=None)
    parser.add_argument("-mail_u", "--smtp_user", type=str, help="Username for email server", default=None)
    parser.add_argument("-mail_p", "--smtp_pass", type=str, help="Password for email server", default=None)
    parser.add_argument(
        "-mail_enc", "--smtp_encryption", action="store_true", help="use ssl encryption connection to smtp-Server"
    )
    parser.add_argument(
        "-mk", "--mailgun-key", type=str, help="key used to send emails via Mailgun Webservice", default=None
    )
    parser.add_argument(
        "-uci", "--uci-option", type=str, help="pass an UCI option to the engine (name;value)", default=None
    )
    parser.add_argument("-dgt3000", "--dgt-3000-clock", action="store_true", help="do NOT use it anymore (DEPRECATED!)")
    parser.add_argument(
        "-nobeep", "--disable-dgt-clock-beep", action="store_true", help="disable beeps on the dgt clock"
    )
    parser.add_argument("-uvoice", "--user-voice", type=str, help="voice for user", default=None)
    parser.add_argument("-cvoice", "--computer-voice", type=str, help="voice for computer", default=None)
    parser.add_argument("-inet", "--enable-internet", action="store_true", help="enable internet lookups")
    parser.add_argument("-nookmove", "--disable-ok-move", action="store_false", help="disable ok move messages")
    parser.add_argument(
        "-v",
        "--version",
        action="version",
        version="%(prog)s version {}".format(version),
        help="show current version",
        default=None,
    )

    args = parser.parse_args()

    # Enable logging
    logging.basicConfig(
        filename=args.log_file,
        level=getattr(logging, args.log_level.upper()),
        format="%(asctime)s.%(msecs)d %(levelname)s %(module)s - %(funcName)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    logging.getLogger("chess.uci").setLevel(logging.INFO)  # don't want to get so many python-chess uci messages

    # Update
    if args.enable_internet:
        update_picochess(args.auto_reboot)

    gaviota = None
    if args.enable_gaviota:
        try:
            gaviota = chess.gaviota.open_tablebases("tablebases/gaviota")
            logging.debug("Tablebases gaviota loaded")
        except OSError:
            logging.error("Tablebases gaviota doesnt exist")
            gaviota = None

    # This class talks to DGTHardware or DGTVirtual
    DGTDisplay(args.disable_ok_move).start()

    if args.dgt_port:
        # Connect to DGT board
        logging.debug("Starting picochess with DGT board on [%s]", args.dgt_port)
        DGTHardware(args.dgt_port, args.enable_dgt_board_leds, args.disable_dgt_clock_beep).start()
    else:
        # Enable keyboard input and terminal display
        logging.warning("No DGT board port provided")
        KeyboardInput().start()
        TerminalDisplay().start()
        DGTVirtual(args.enable_dgt_board_leds, args.disable_dgt_clock_beep).start()

    # Save to PGN
    PgnDisplay(
        args.pgn_file,
        net=args.enable_internet,
        email=args.email,
        fromINIMailGun_Key=args.mailgun_key,
        fromIniSmtp_Server=args.smtp_server,
        fromINISmtp_User=args.smtp_user,
        fromINISmtp_Pass=args.smtp_pass,
        fromINISmtp_Enc=args.smtp_encryption,
    ).start()
    if args.pgn_user:
        user_name = args.pgn_user
    else:
        if args.email:
            user_name = args.email.split("@")[0]
        else:
            user_name = "Player"

    # Create ChessTalker for speech output
    talker = None
    if args.user_voice or args.computer_voice:
        logging.debug("Initializing ChessTalker [%s, %s]", str(args.user_voice), str(args.computer_voice))
        talker = chesstalker.chesstalker.ChessTalker(args.user_voice, args.computer_voice)
        talker.start()
    else:
        logging.debug("ChessTalker disabled")

    # Launch web server
    if args.web_server_port:
        WebServer(args.web_server_port).start()

    # Gentlemen, start your engines...
    engine = uci.Engine(
        args.engine, hostname=args.remote, username=args.user, key_file=args.server_key, password=args.password
    )
    try:
        engine_name = engine.get().name
    except AttributeError:
        logging.debug("FATAL: no engines started")
        sys.exit(-1)
    logging.debug("Loaded engine [%s]", engine_name)
    logging.debug("Supported options [%s]", engine.get().options)

    # Startup - internal
    game = chess.Board()  # Create the current game
    legal_fens = compute_legal_fens(game)  # Compute the legal FENs
    all_books = get_opening_books()
    try:
        book_index = [book[1] for book in all_books].index("books/" + args.book)
    except ValueError:
        logging.debug("ERROR: Selected book not present, defaulting to %s", all_books[7][1])
        book_index = 7
    bookreader = chess.polyglot.open_reader(all_books[book_index][1])
    searchmoves = AlternativeMover()
    interaction_mode = Mode.GAME
    play_mode = PlayMode.PLAY_WHITE
    time_control = TimeControl(ClockMode.BLITZ, minutes_per_game=5)
    last_computer_fen = None
    game_declared = False  # User declared resignation or draw
    engine_level = None

    system_info_thread = threading.Timer(0, display_system_info)
    system_info_thread.start()
    # send the args options to the engine
    engine_startup()

    # Startup - external
    Display.show(
        Message.STARTUP_INFO,
        info={
            "interaction_mode": interaction_mode,
            "play_mode": play_mode,
            "book": all_books[book_index][1],
            "book_index": book_index,
            "time_control_string": "mov 5",
        },
    )
    Display.show(Message.ENGINE_START, path=engine.get_path(), has_levels=engine.has_levels())

    # Event loop
    while True:
        try:
            event = event_queue.get()
        except queue.Empty:
            pass
        else:
            logging.debug("Received event in event loop : %s", event)
            for case in switch(event):
                if case(Event.FEN):  # User sets a new position, convert it to a move if it is legal
                    legal_fens = process_fen(event.fen, legal_fens)
                    break

                if case(Event.KEYBOARD_MOVE):
                    move = event.move
                    logging.debug("Keyboard move [%s]", move)
                    if move not in game.legal_moves:
                        logging.warning("Illegal move [%s]", move)
                    else:
                        g = copy.deepcopy(game)
                        g.push(move)
                        legal_fens = process_fen(g.board_fen(), legal_fens)
                    break

                if case(Event.USER_MOVE):  # User sends a new move
                    move = event.move
                    logging.debug("User move [%s]", move)
                    if move not in game.legal_moves:
                        logging.warning("Illegal move [%s]", move)
                    else:
                        result = chess.uci.BestMove(bestmove=move, ponder=None)
                        game = handle_move(result, game)
                        # if check_game_state(game, interaction_mode):
                        legal_fens = compute_legal_fens(game)
                    break

                if case(Event.LEVEL):  # User sets a new level
                    engine_level = event.level
                    logging.debug("Setting engine to level %i", engine_level)
                    if engine.level(engine_level):
                        engine.send()
                        Display.show(Message.LEVEL, level=engine_level)
                    break

                if case(Event.NEW_ENGINE):
                    old_path = engine.path
                    engine_shutdown = True
                    # Stop the old engine cleanly
                    engine.stop()
                    # Closeout the engine process and threads
                    # The all return non-zero error codes, 0=success
                    if engine.quit():  # Ask nicely
                        if engine.terminate():  # If you won't go nicely....
                            if engine.kill():  # Right that does it!
                                logging.error("Serious: Engine shutdown failure")
                                Display.show(Message.ENGINE_FAIL)
                                engine_shutdown = False
                    if engine_shutdown:
                        # Load the new one and send args.
                        # Local engines only
                        engine_fallback = False
                        engine = uci.Engine(event.eng[0])
                        try:
                            engine_name = engine.get().name
                        except AttributeError:
                            # New engine failed to start, restart old engine
                            logging.error("New engine failed to start, reverting to %s", old_path)
                            engine_fallback = True
                            engine = uci.Engine(old_path)
                            try:
                                engine_name = engine.get().name
                            except AttributeError:
                                # Help - old engine failed to restart. There is no engine
                                logging.error("FATAL: no engines started")
                                sys.exit(-1)
                        # Schedule cleanup of old objects
                        gc.collect()
                        # Restore options - this doesn't deal with any
                        # supplementary uci options sent 'in game', see event.UCI_OPTION_SET
                        engine_startup()
                        # Send user selected engine level to new engine
                        if engine_level and engine.level(engine_level):
                            engine.send()
                            Display.show(Message.LEVEL, level=engine_level)
                        # Go back to analysing or observing
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                        # All done - rock'n'roll
                        if not engine_fallback:
                            Display.show(Message.ENGINE_NAME, ename=engine_name)
                            Display.show(Message.ENGINE_READY, eng=event.eng, has_levels=engine.has_levels())
                        else:
                            Display.show(Message.ENGINE_FAIL)
                    break

                if case(Event.SETUP_POSITION):  # User sets up a position
                    logging.debug("Setting up custom fen: {0}".format(event.fen))
                    if engine.has_chess960():
                        engine.option("UCI_Chess960", event.uci960)
                        engine.send()
                    else:  # start normal new game if engine can't handle the user wish
                        event.uci960 = False
                        Display.show(Message.ENGINE_FAIL)  # @todo not really true but inform the user about the result
                    if game.move_stack:
                        if game.is_game_over() or game_declared:
                            Display.show(
                                Message.GAME_ENDS,
                                result=GameResult.ABORT,
                                play_mode=play_mode,
                                game=copy.deepcopy(game),
                            )
                    game = chess.Board(event.fen, event.uci960)
                    game.custom_fen = event.fen
                    legal_fens = compute_legal_fens(game)
                    stop_search_and_clock()
                    time_control.reset()
                    interaction_mode = Mode.GAME
                    last_computer_fen = None
                    set_wait_state()
                    searchmoves.reset()
                    Display.show(Message.START_NEW_GAME)
                    game_declared = False
                    break

                if case(Event.STARTSTOP_THINK):
                    if engine.is_thinking() and (interaction_mode != Mode.REMOTE):
                        stop_search_and_clock()
                    else:
                        play_mode = PlayMode.PLAY_WHITE if play_mode == PlayMode.PLAY_BLACK else PlayMode.PLAY_BLACK
                        Display.show(Message.PLAY_MODE, play_mode=play_mode)
                        if check_game_state(game, play_mode) and (interaction_mode != Mode.REMOTE):
                            think(game, time_control)
                    break

                if case(Event.ALTERNATIVE_MOVE):
                    game.pop()
                    Display.show(Message.ALTERNATIVE_MOVE)
                    think(game, time_control)
                    break

                if case(Event.STARTSTOP_CLOCK):
                    if time_control.is_ticking():
                        stop_clock()
                    else:
                        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=time_control)
                        time_control.run(game.turn)
                    break

                if case(Event.NEW_GAME):  # User starts a new game
                    if game.move_stack:
                        logging.debug("Starting a new game")
                        if not (game.is_game_over() or game_declared):
                            Display.show(
                                Message.GAME_ENDS,
                                result=GameResult.ABORT,
                                play_mode=play_mode,
                                game=copy.deepcopy(game),
                            )
                        game = chess.Board()
                    legal_fens = compute_legal_fens(game)
                    last_computer_fen = None
                    stop_search_and_clock()
                    time_control.reset()
                    set_wait_state()
                    searchmoves.reset()
                    Display.show(Message.START_NEW_GAME)
                    game_declared = False
                    break

                if case(Event.DRAWRESIGN):
                    if not game_declared:  # in case user leaves kings in place while moving other pieces
                        Display.show(
                            Message.GAME_ENDS, result=event.result, play_mode=play_mode, game=copy.deepcopy(game)
                        )
                        game_declared = True
                    break

                if case(Event.OPENING_BOOK):
                    logging.debug("Changing opening book [%s]", event.book[1])
                    bookreader = chess.polyglot.open_reader(event.book[1])
                    Display.show(Message.OPENING_BOOK, book=event.book)
                    break

                if case(Event.REMOTE_MOVE):
                    if interaction_mode == Mode.REMOTE:
                        bm = chess.uci.BestMove(bestmove=chess.Move.from_uci(event.move), ponder=None)
                        game = handle_move(bm, game)
                        legal_fens = compute_legal_fens(game)
                    break

                if case(Event.BEST_MOVE):
                    if event.inbook:
                        Display.show(Message.BOOK_MOVE, result=event.result)
                    game = handle_move(event.result, game)
                    legal_fens = compute_legal_fens(game)
                    break

                if case(Event.NEW_PV):
                    if interaction_mode == Mode.GAME:
                        pass
                    else:
                        Display.show(Message.NEW_PV, pv=event.pv, mode=interaction_mode, fen=game.fen())
                    break

                if case(Event.SCORE):
                    if event.score == "book":
                        score = "book"
                    elif event.score == "tb":
                        score = "tb {0}".format(event.mate)
                    else:
                        try:
                            score = int(event.score)
                            if game.turn == chess.BLACK:
                                score *= -1
                        except ValueError:
                            score = event.score
                            logging.debug("Could not convert score " + score)
                        except TypeError:
                            score = "m {0}".format(event.mate)
                    Display.show(Message.SCORE, score=score, mate=event.mate, mode=interaction_mode)
                    break

                if case(Event.SET_MODE):
                    if (
                        interaction_mode == Mode.GAME
                        or interaction_mode == Mode.OBSERVE
                        or interaction_mode == Mode.REMOTE
                    ):
                        stop_clock()  # only stop, if the clock is really running
                    interaction_mode = event.mode
                    if engine.is_thinking():
                        stop_search()  # dont need to stop, if pondering
                    if engine.is_pondering() and interaction_mode == Mode.GAME:
                        stop_search()  # if change from ponder modes to game, also stops the pondering
                    set_wait_state()
                    Display.show(Message.INTERACTION_MODE, mode=event.mode)
                    break

                if case(Event.SET_TIME_CONTROL):
                    time_control = event.time_control
                    Display.show(Message.TIME_CONTROL, time_control_string=event.time_control_string)
                    break

                if case(Event.OUT_OF_TIME):
                    stop_search_and_clock()
                    Display.show(
                        Message.GAME_ENDS, result=GameResult.TIME_CONTROL, play_mode=play_mode, game=copy.deepcopy(game)
                    )
                    break

                if case(Event.UCI_OPTION_SET):
                    # Nowhere calls this yet, but they will need to be saved for engine restart
                    engine.option(event.name, event.value)
                    break

                if case(Event.SHUTDOWN):
                    if talker:
                        talker.say_event(event)
                    shutdown()
                    break

                if case(Event.DGT_BUTTON):
                    Display.show(Message.DGT_BUTTON, button=event.button)
                    break

                if case(Event.DGT_FEN):
                    Display.show(Message.DGT_FEN, fen=event.fen)
                    break

                if case():  # Default
                    logging.warning("Event not handled : [%s]", event)

            event_queue.task_done()
Exemple #13
0
def main():
    def engine_startup():
        if 'Hash' in engine.get().options:
            engine.option("Hash", args.hash_size)
        if 'Threads' in engine.get().options:  # Stockfish
            engine.option("Threads", args.threads)
        if 'Core Threads' in engine.get().options:  # Hiarcs
            engine.option("Core Threads", args.threads)
        if args.uci_option:
            for uci_option in args.uci_option.strip('"').split(";"):
                uci_parameter = uci_option.strip().split('=')
                engine.option(uci_parameter[0], uci_parameter[1])
        # send the options to the engine
        engine.send()
        # Notify other display processes
        Display.show(Message.UCI_OPTION_LIST, options=engine.get().options)

    def display_system_info():
        if args.enable_internet:
            place = get_location()
            addr = get_ip()
        else:
            place = "?"
            addr = "?"
        Display.show(Message.SYSTEM_INFO,
                     info={
                         "version": version,
                         "location": place,
                         "books": get_opening_books(),
                         "ip": addr,
                         "engine_name": engine_name,
                         "user_name": user_name
                     })

    def compute_legal_fens(g):
        """
        Computes a list of legal FENs for the given game.
        Also stores the initial position in the 'root' attribute.
        :param g: The game
        :return: A list of legal FENs, and the root FEN
        """
        class FenList(list):
            def __init__(self, *args):
                list.__init__(self, *args)
                self.root = ''

        fens = FenList()
        for move in g.legal_moves:
            g.push(move)
            fens.append(g.board_fen())
            g.pop()
        fens.root = g.board_fen()
        return fens

    def probe_tablebase(game):
        if not gaviota:
            return None
        score = gaviota.probe_dtm(game)
        if score:
            Observable.fire(Event.SCORE, score='tb', mate=score)
        return score

    def think(game, tc):
        """
        Starts a new search on the current game.
        If a move is found in the opening book, fire an event in a few seconds.
        :return:
        """
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=tc)
        tc.run(game.turn)

        book_move = searchmoves.book(bookreader, game)
        if book_move:
            Observable.fire(Event.SCORE, score='book', mate=None)
            time.sleep(0.5)
            Observable.fire(Event.BEST_MOVE, result=book_move, inbook=True)
        else:
            probe_tablebase(game)
            engine.position(copy.deepcopy(game))
            uci_dict = tc.uci()
            uci_dict['searchmoves'] = searchmoves.all(game)
            engine.go(uci_dict)

    def analyse(game):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        probe_tablebase(game)
        engine.position(copy.deepcopy(game))
        engine.ponder()

    def observe(game, tc):
        """
        Starts a new ponder search on the current game.
        :return:
        """
        Display.show(Message.RUN_CLOCK, turn=game.turn, time_control=tc)
        tc.run(game.turn)
        analyse(game)

    def stop_search():
        """
        Stop current search.
        :return:
        """
        engine.stop()

    def stop_clock():
        nonlocal time_control
        time_control.stop()
        Display.show(Message.STOP_CLOCK)

    def stop_search_and_clock():
        stop_clock()
        stop_search()

    def check_game_state(game, play_mode):
        """
        Check if the game has ended or not ; it also sends Message to Displays if the game has ended.
        :param game:
        :return: True is the game continues, False if it has ended
        """
        result = None
        if game.is_stalemate():
            result = GameResult.STALEMATE
        if game.is_insufficient_material():
            result = GameResult.INSUFFICIENT_MATERIAL
        if game.is_seventyfive_moves():
            result = GameResult.SEVENTYFIVE_MOVES
        if game.is_fivefold_repetition():
            result = GameResult.FIVEFOLD_REPETITION
        if game.is_checkmate():
            result = GameResult.MATE

        if result is None:
            return True
        else:
            Display.show(Message.GAME_ENDS,
                         result=result,
                         play_mode=play_mode,
                         game=copy.deepcopy(game))
            return False

    def process_fen(fen, legal_fens):
        if fen in legal_fens:
            # Check if we have to undo a previous move (sliding)
            if interaction_mode == Mode.GAME:
                if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.BLACK) or \
                        (play_mode == PlayMode.PLAY_BLACK and game.turn == chess.WHITE):
                    stop_search()
                    if game.move_stack:
                        game.pop()
            legal_moves = list(game.legal_moves)
            Observable.fire(Event.USER_MOVE,
                            move=legal_moves[legal_fens.index(fen)])
        elif fen == last_computer_fen:  # Player had done the computer move on the board
            if check_game_state(
                    game, play_mode) and ((interaction_mode == Mode.GAME) or
                                          (interaction_mode == Mode.REMOTE)):
                # finally reset all alternative moves see: handle_move()
                nonlocal searchmoves
                searchmoves.reset()
                Display.show(Message.COMPUTER_MOVE_DONE_ON_BOARD)
                if time_control.mode != ClockMode.FIXED_TIME:
                    Display.show(Message.RUN_CLOCK,
                                 turn=game.turn,
                                 time_control=time_control)
                    time_control.run(game.turn)
        else:  # Check if this a a previous legal position and allow user to restart from this position
            game_history = copy.deepcopy(game)
            while game_history.move_stack:
                game_history.pop()
                if (play_mode == PlayMode.PLAY_WHITE and game_history.turn == chess.WHITE) \
                        or (play_mode == PlayMode.PLAY_BLACK and game_history.turn == chess.BLACK) \
                        or (interaction_mode == Mode.OBSERVE) or (interaction_mode == Mode.KIBITZ) \
                        or (interaction_mode == Mode.REMOTE) or (interaction_mode == Mode.ANALYSIS):
                    if game_history.board_fen() == fen:
                        logging.debug("Legal Fens root       : " +
                                      str(legal_fens.root))
                        logging.debug("Current game FEN      : " +
                                      str(game.fen()))
                        logging.debug("Undoing game until FEN: " + fen)
                        stop_search()
                        while len(game_history.move_stack) < len(
                                game.move_stack):
                            game.pop()
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                        Display.show(Message.USER_TAKE_BACK)
                        legal_fens = compute_legal_fens(game)
                        break
        return legal_fens

    def set_wait_state():
        if interaction_mode == Mode.GAME:
            nonlocal play_mode
            play_mode = PlayMode.PLAY_WHITE if game.turn == chess.WHITE else PlayMode.PLAY_BLACK

    def handle_move(result, game):
        move = result.bestmove
        fen = game.fen()
        game.push(move)
        nonlocal last_computer_fen
        nonlocal searchmoves
        last_computer_fen = None
        if interaction_mode == Mode.GAME:
            stop_clock()
            # If UserMove: reset all alternative moves
            # If ComputerMove: disallow this move, and finally reset all if DONE_ON_BOARD event @see: process_fen()
            if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.WHITE)\
                    or (play_mode == PlayMode.PLAY_BLACK and game.turn == chess.BLACK):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                Display.show(Message.COMPUTER_MOVE,
                             result=result,
                             fen=fen,
                             game=game.copy(),
                             time_control=time_control)
            else:
                searchmoves.reset()
                Display.show(Message.USER_MOVE, move=move, game=game.copy())
                if check_game_state(game, play_mode):
                    think(game, time_control)

        elif interaction_mode == Mode.REMOTE:
            stop_search_and_clock()
            # If UserMove: reset all alternative moves
            # If Remote Move: same process as for computer move above
            if (play_mode == PlayMode.PLAY_WHITE and game.turn == chess.WHITE)\
                    or (play_mode == PlayMode.PLAY_BLACK and game.turn == chess.BLACK):
                last_computer_fen = game.board_fen()
                searchmoves.add(move)
                Display.show(Message.COMPUTER_MOVE,
                             result=result,
                             fen=fen,
                             game=game.copy(),
                             time_control=time_control)
            else:
                searchmoves.reset()
                Display.show(Message.USER_MOVE, move=move, game=game.copy())
                if check_game_state(game, play_mode):
                    observe(game, time_control)

        elif interaction_mode == Mode.OBSERVE:
            stop_search_and_clock()
            Display.show(Message.REVIEW_MODE_MOVE,
                         move=move,
                         fen=fen,
                         game=game.copy(),
                         mode=interaction_mode)
            if check_game_state(game, play_mode):
                observe(game, time_control)

        elif interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
            stop_search()
            Display.show(Message.REVIEW_MODE_MOVE,
                         move=move,
                         fen=fen,
                         game=game.copy(),
                         mode=interaction_mode)
            if check_game_state(game, play_mode):
                analyse(game)

        return game

    # Enable garbage collection - needed for engine swapping as objects orphaned
    gc.enable()

    # Command line argument parsing
    parser = configargparse.ArgParser(default_config_files=[
        os.path.join(os.path.dirname(__file__), "picochess.ini")
    ])
    parser.add_argument("-e",
                        "--engine",
                        type=str,
                        help="UCI engine executable path",
                        default='engines/stockfish')
    parser.add_argument(
        "-d",
        "--dgt-port",
        type=str,
        help="enable dgt board on the given serial port such as /dev/ttyUSB0")
    parser.add_argument(
        "-b",
        "--book",
        type=str,
        help="Opening book - full name of book in 'books' folder",
        default='h-varied.bin')
    parser.add_argument("-g",
                        "--enable-gaviota",
                        action='store_true',
                        help="enable gavoita tablebase probing")
    parser.add_argument("-leds",
                        "--enable-dgt-board-leds",
                        action='store_true',
                        help="enable dgt board leds")
    parser.add_argument("-hs",
                        "--hash-size",
                        type=int,
                        help="hashtable size in MB (default:64)",
                        default=64)
    parser.add_argument("-t",
                        "--threads",
                        type=int,
                        help="number of engine threads (default:1)",
                        default=1)
    parser.add_argument(
        "-l",
        "--log-level",
        choices=['notset', 'debug', 'info', 'warning', 'error', 'critical'],
        default='warning',
        help="logging level")
    parser.add_argument("-lf",
                        "--log-file",
                        type=str,
                        help="log to the given file")
    parser.add_argument("-r",
                        "--remote",
                        type=str,
                        help="remote server running the engine")
    parser.add_argument("-u",
                        "--user",
                        type=str,
                        help="remote user on server running the engine")
    parser.add_argument("-p",
                        "--password",
                        type=str,
                        help="password for the remote user")
    parser.add_argument("-sk",
                        "--server-key",
                        type=str,
                        help="key file used to connect to the remote server")
    parser.add_argument("-pgn",
                        "--pgn-file",
                        type=str,
                        help="pgn file used to store the games",
                        default='games.pgn')
    parser.add_argument("-pgn_u",
                        "--pgn-user",
                        type=str,
                        help="user name for the pgn file",
                        default=None)
    parser.add_argument("-ar",
                        "--auto-reboot",
                        action='store_true',
                        help="reboot system after update")
    parser.add_argument("-web",
                        "--web-server",
                        dest="web_server_port",
                        nargs="?",
                        const=80,
                        type=int,
                        metavar="PORT",
                        help="launch web server")
    parser.add_argument("-mail",
                        "--email",
                        type=str,
                        help="email used to send pgn files",
                        default=None)
    parser.add_argument("-mail_s",
                        "--smtp_server",
                        type=str,
                        help="Adress of email server",
                        default=None)
    parser.add_argument("-mail_u",
                        "--smtp_user",
                        type=str,
                        help="Username for email server",
                        default=None)
    parser.add_argument("-mail_p",
                        "--smtp_pass",
                        type=str,
                        help="Password for email server",
                        default=None)
    parser.add_argument("-mail_enc",
                        "--smtp_encryption",
                        action='store_true',
                        help="use ssl encryption connection to smtp-Server")
    parser.add_argument("-mk",
                        "--mailgun-key",
                        type=str,
                        help="key used to send emails via Mailgun Webservice",
                        default=None)
    parser.add_argument("-uci",
                        "--uci-option",
                        type=str,
                        help="pass an UCI option to the engine (name;value)",
                        default=None)
    parser.add_argument("-dgt3000",
                        "--dgt-3000-clock",
                        action='store_true',
                        help="do NOT use it anymore (DEPRECATED!)")
    parser.add_argument("-nobeep",
                        "--disable-dgt-clock-beep",
                        action='store_true',
                        help="disable beeps on the dgt clock")
    parser.add_argument("-uvoice",
                        "--user-voice",
                        type=str,
                        help="voice for user",
                        default=None)
    parser.add_argument("-cvoice",
                        "--computer-voice",
                        type=str,
                        help="voice for computer",
                        default=None)
    parser.add_argument("-inet",
                        "--enable-internet",
                        action='store_true',
                        help="enable internet lookups")
    parser.add_argument("-nookmove",
                        "--disable-ok-move",
                        action='store_false',
                        help="disable ok move messages")
    parser.add_argument("-v",
                        "--version",
                        action='version',
                        version='%(prog)s version {}'.format(version),
                        help="show current version",
                        default=None)

    args = parser.parse_args()

    # Enable logging
    logging.basicConfig(
        filename=args.log_file,
        level=getattr(logging, args.log_level.upper()),
        format=
        '%(asctime)s.%(msecs)d %(levelname)s %(module)s - %(funcName)s: %(message)s',
        datefmt="%Y-%m-%d %H:%M:%S")
    logging.getLogger("chess.uci").setLevel(
        logging.INFO)  # don't want to get so many python-chess uci messages

    # Update
    if args.enable_internet:
        update_picochess(args.auto_reboot)

    gaviota = None
    if args.enable_gaviota:
        try:
            gaviota = chess.gaviota.open_tablebases('tablebases/gaviota')
            logging.debug('Tablebases gaviota loaded')
        except OSError:
            logging.error('Tablebases gaviota doesnt exist')
            gaviota = None

    # This class talks to DGTHardware or DGTVirtual
    DGTDisplay(args.disable_ok_move).start()

    if args.dgt_port:
        # Connect to DGT board
        logging.debug("Starting picochess with DGT board on [%s]",
                      args.dgt_port)
        # DGTHardware(args.dgt_port, args.enable_dgt_board_leds, args.disable_dgt_clock_beep).start()
        DGTPi(args.dgt_port, args.enable_dgt_board_leds,
              args.disable_dgt_clock_beep).start()
    else:
        # Enable keyboard input and terminal display
        logging.warning("No DGT board port provided")
        KeyboardInput().start()
        TerminalDisplay().start()
        DGTVirtual(args.enable_dgt_board_leds,
                   args.disable_dgt_clock_beep).start()

    # Save to PGN
    PgnDisplay(args.pgn_file,
               net=args.enable_internet,
               email=args.email,
               fromINIMailGun_Key=args.mailgun_key,
               fromIniSmtp_Server=args.smtp_server,
               fromINISmtp_User=args.smtp_user,
               fromINISmtp_Pass=args.smtp_pass,
               fromINISmtp_Enc=args.smtp_encryption).start()
    if args.pgn_user:
        user_name = args.pgn_user
    else:
        if args.email:
            user_name = args.email.split('@')[0]
        else:
            user_name = "Player"

    # Create ChessTalker for speech output
    talker = None
    if args.user_voice or args.computer_voice:
        logging.debug("Initializing ChessTalker [%s, %s]",
                      str(args.user_voice), str(args.computer_voice))
        talker = chesstalker.chesstalker.ChessTalker(args.user_voice,
                                                     args.computer_voice)
        talker.start()
    else:
        logging.debug("ChessTalker disabled")

    # Launch web server
    if args.web_server_port:
        WebServer(args.web_server_port).start()

    # Gentlemen, start your engines...
    engine = uci.Engine(args.engine,
                        hostname=args.remote,
                        username=args.user,
                        key_file=args.server_key,
                        password=args.password)
    try:
        engine_name = engine.get().name
    except AttributeError:
        logging.debug("FATAL: no engines started")
        sys.exit(-1)
    logging.debug('Loaded engine [%s]', engine_name)
    logging.debug('Supported options [%s]', engine.get().options)

    # Startup - internal
    game = chess.Board()  # Create the current game
    legal_fens = compute_legal_fens(game)  # Compute the legal FENs
    all_books = get_opening_books()
    try:
        book_index = [book[1]
                      for book in all_books].index('books/' + args.book)
    except ValueError:
        logging.debug("ERROR: Selected book not present, defaulting to %s",
                      all_books[7][1])
        book_index = 7
    bookreader = chess.polyglot.open_reader(all_books[book_index][1])
    searchmoves = AlternativeMover()
    interaction_mode = Mode.GAME
    play_mode = PlayMode.PLAY_WHITE
    time_control = TimeControl(ClockMode.BLITZ, minutes_per_game=5)
    last_computer_fen = None
    game_declared = False  # User declared resignation or draw
    engine_level = None

    system_info_thread = threading.Timer(0, display_system_info)
    system_info_thread.start()
    # send the args options to the engine
    engine_startup()

    # Startup - external
    Display.show(Message.STARTUP_INFO,
                 info={
                     "interaction_mode": interaction_mode,
                     "play_mode": play_mode,
                     "book": all_books[book_index][1],
                     "book_index": book_index,
                     "time_control_string": "mov 5"
                 })
    Display.show(Message.ENGINE_START,
                 path=engine.get_path(),
                 has_levels=engine.has_levels())

    # Event loop
    while True:
        try:
            event = event_queue.get()
        except queue.Empty:
            pass
        else:
            logging.debug('Received event in event loop : %s', event)
            for case in switch(event):
                if case(
                        Event.FEN
                ):  # User sets a new position, convert it to a move if it is legal
                    legal_fens = process_fen(event.fen, legal_fens)
                    break

                if case(Event.KEYBOARD_MOVE):
                    move = event.move
                    logging.debug('Keyboard move [%s]', move)
                    if move not in game.legal_moves:
                        logging.warning('Illegal move [%s]', move)
                    else:
                        g = copy.deepcopy(game)
                        g.push(move)
                        legal_fens = process_fen(g.board_fen(), legal_fens)
                    break

                if case(Event.USER_MOVE):  # User sends a new move
                    move = event.move
                    logging.debug('User move [%s]', move)
                    if move not in game.legal_moves:
                        logging.warning('Illegal move [%s]', move)
                    else:
                        result = chess.uci.BestMove(bestmove=move, ponder=None)
                        game = handle_move(result, game)
                        # if check_game_state(game, interaction_mode):
                        legal_fens = compute_legal_fens(game)
                    break

                if case(Event.LEVEL):  # User sets a new level
                    engine_level = event.level
                    logging.debug("Setting engine to level %i", engine_level)
                    if engine.level(engine_level):
                        engine.send()
                        Display.show(Message.LEVEL, level=engine_level)
                    break

                if case(Event.NEW_ENGINE):
                    old_path = engine.path
                    engine_shutdown = True
                    # Stop the old engine cleanly
                    engine.stop()
                    # Closeout the engine process and threads
                    # The all return non-zero error codes, 0=success
                    if engine.quit():  # Ask nicely
                        if engine.terminate():  # If you won't go nicely....
                            if engine.kill():  # Right that does it!
                                logging.error(
                                    'Serious: Engine shutdown failure')
                                Display.show(Message.ENGINE_FAIL)
                                engine_shutdown = False
                    if engine_shutdown:
                        # Load the new one and send args.
                        # Local engines only
                        engine_fallback = False
                        engine = uci.Engine(event.eng[0])
                        try:
                            engine_name = engine.get().name
                        except AttributeError:
                            # New engine failed to start, restart old engine
                            logging.error(
                                "New engine failed to start, reverting to %s",
                                old_path)
                            engine_fallback = True
                            engine = uci.Engine(old_path)
                            try:
                                engine_name = engine.get().name
                            except AttributeError:
                                # Help - old engine failed to restart. There is no engine
                                logging.error("FATAL: no engines started")
                                sys.exit(-1)
                        # Schedule cleanup of old objects
                        gc.collect()
                        # Restore options - this doesn't deal with any
                        # supplementary uci options sent 'in game', see event.UCI_OPTION_SET
                        engine_startup()
                        # Send user selected engine level to new engine
                        if engine_level and engine.level(engine_level):
                            engine.send()
                            Display.show(Message.LEVEL, level=engine_level)
                        # Go back to analysing or observing
                        if interaction_mode == Mode.ANALYSIS or interaction_mode == Mode.KIBITZ:
                            analyse(game)
                        if interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                            observe(game, time_control)
                        # All done - rock'n'roll
                        if not engine_fallback:
                            Display.show(Message.ENGINE_NAME,
                                         ename=engine_name)
                            Display.show(Message.ENGINE_READY,
                                         eng=event.eng,
                                         has_levels=engine.has_levels())
                        else:
                            Display.show(Message.ENGINE_FAIL)
                    break

                if case(Event.SETUP_POSITION):  # User sets up a position
                    logging.debug("Setting up custom fen: {0}".format(
                        event.fen))
                    if engine.has_chess960():
                        engine.option('UCI_Chess960', event.uci960)
                        engine.send()
                    else:  # start normal new game if engine can't handle the user wish
                        event.uci960 = False
                        Display.show(
                            Message.ENGINE_FAIL
                        )  # @todo not really true but inform the user about the result
                    if game.move_stack:
                        if game.is_game_over() or game_declared:
                            Display.show(Message.GAME_ENDS,
                                         result=GameResult.ABORT,
                                         play_mode=play_mode,
                                         game=copy.deepcopy(game))
                    game = chess.Board(event.fen, event.uci960)
                    game.custom_fen = event.fen
                    legal_fens = compute_legal_fens(game)
                    stop_search_and_clock()
                    time_control.reset()
                    interaction_mode = Mode.GAME
                    last_computer_fen = None
                    set_wait_state()
                    searchmoves.reset()
                    Display.show(Message.START_NEW_GAME)
                    game_declared = False
                    break

                if case(Event.STARTSTOP_THINK):
                    if engine.is_thinking() and (interaction_mode !=
                                                 Mode.REMOTE):
                        stop_search_and_clock()
                    else:
                        play_mode = PlayMode.PLAY_WHITE if play_mode == PlayMode.PLAY_BLACK else PlayMode.PLAY_BLACK
                        Display.show(Message.PLAY_MODE, play_mode=play_mode)
                        if check_game_state(game, play_mode) and (
                                interaction_mode != Mode.REMOTE):
                            think(game, time_control)
                    break

                if case(Event.ALTERNATIVE_MOVE):
                    game.pop()
                    Display.show(Message.ALTERNATIVE_MOVE)
                    think(game, time_control)
                    break

                if case(Event.STARTSTOP_CLOCK):
                    if time_control.is_ticking():
                        stop_clock()
                    else:
                        Display.show(Message.RUN_CLOCK,
                                     turn=game.turn,
                                     time_control=time_control)
                        time_control.run(game.turn)
                    break

                if case(Event.NEW_GAME):  # User starts a new game
                    if game.move_stack:
                        logging.debug("Starting a new game")
                        if not (game.is_game_over() or game_declared):
                            Display.show(Message.GAME_ENDS,
                                         result=GameResult.ABORT,
                                         play_mode=play_mode,
                                         game=copy.deepcopy(game))
                        game = chess.Board()
                    legal_fens = compute_legal_fens(game)
                    last_computer_fen = None
                    stop_search_and_clock()
                    time_control.reset()
                    set_wait_state()
                    searchmoves.reset()
                    Display.show(Message.START_NEW_GAME)
                    game_declared = False
                    break

                if case(Event.DRAWRESIGN):
                    if not game_declared:  # in case user leaves kings in place while moving other pieces
                        Display.show(Message.GAME_ENDS,
                                     result=event.result,
                                     play_mode=play_mode,
                                     game=copy.deepcopy(game))
                        game_declared = True
                    break

                if case(Event.OPENING_BOOK):
                    logging.debug("Changing opening book [%s]", event.book[1])
                    bookreader = chess.polyglot.open_reader(event.book[1])
                    Display.show(Message.OPENING_BOOK, book=event.book)
                    break

                if case(Event.REMOTE_MOVE):
                    if interaction_mode == Mode.REMOTE:
                        bm = chess.uci.BestMove(bestmove=chess.Move.from_uci(
                            event.move),
                                                ponder=None)
                        game = handle_move(bm, game)
                        legal_fens = compute_legal_fens(game)
                    break

                if case(Event.BEST_MOVE):
                    if event.inbook:
                        Display.show(Message.BOOK_MOVE, result=event.result)
                    game = handle_move(event.result, game)
                    legal_fens = compute_legal_fens(game)
                    break

                if case(Event.NEW_PV):
                    if interaction_mode == Mode.GAME:
                        pass
                    else:
                        Display.show(Message.NEW_PV,
                                     pv=event.pv,
                                     mode=interaction_mode,
                                     fen=game.fen())
                    break

                if case(Event.SCORE):
                    if event.score == 'book':
                        score = 'book'
                    elif event.score == 'tb':
                        score = 'tb {0}'.format(event.mate)
                    else:
                        try:
                            score = int(event.score)
                            if game.turn == chess.BLACK:
                                score *= -1
                        except ValueError:
                            score = event.score
                            logging.debug('Could not convert score ' + score)
                        except TypeError:
                            score = 'm {0}'.format(event.mate)
                    Display.show(Message.SCORE,
                                 score=score,
                                 mate=event.mate,
                                 mode=interaction_mode)
                    break

                if case(Event.SET_MODE):
                    if interaction_mode == Mode.GAME or interaction_mode == Mode.OBSERVE or interaction_mode == Mode.REMOTE:
                        stop_clock(
                        )  # only stop, if the clock is really running
                    interaction_mode = event.mode
                    if engine.is_thinking():
                        stop_search()  # dont need to stop, if pondering
                    if engine.is_pondering() and interaction_mode == Mode.GAME:
                        stop_search(
                        )  # if change from ponder modes to game, also stops the pondering
                    set_wait_state()
                    Display.show(Message.INTERACTION_MODE, mode=event.mode)
                    break

                if case(Event.SET_TIME_CONTROL):
                    time_control = event.time_control
                    Display.show(Message.TIME_CONTROL,
                                 time_control_string=event.time_control_string)
                    break

                if case(Event.OUT_OF_TIME):
                    stop_search_and_clock()
                    Display.show(Message.GAME_ENDS,
                                 result=GameResult.TIME_CONTROL,
                                 play_mode=play_mode,
                                 game=copy.deepcopy(game))
                    break

                if case(Event.UCI_OPTION_SET):
                    # Nowhere calls this yet, but they will need to be saved for engine restart
                    engine.option(event.name, event.value)
                    break

                if case(Event.SHUTDOWN):
                    if talker:
                        talker.say_event(event)
                    shutdown()
                    break

                if case(Event.DGT_BUTTON):
                    Display.show(Message.DGT_BUTTON, button=event.button)
                    break

                if case(Event.DGT_FEN):
                    Display.show(Message.DGT_FEN, fen=event.fen)
                    break

                if case():  # Default
                    logging.warning("Event not handled : [%s]", event)

            event_queue.task_done()