def action_capture_key_position_images(): """ Captures the key position and returns positional data from the calibration grid. """ _ = gantry.calibrate() for key_position in key_positions: x, y = key_position.gantry_position gantry.set_position(x, y) frame = camera.capture_frame() markers = Marker.extract_markers(frame, marker_family=Marker.FAMILY_tag36h11) sid_center_positions = {} for sid in key_position.sid_fid_mapping: fid = key_position.sid_fid_mapping[sid] found_markers = list(filter(lambda m: m.id == fid, markers)) if len(found_markers) != 1: log.error( f"Found {len(found_markers)} markers with fid '{fid}'.") continue marker = found_markers[0] sid_center_positions[sid] = list([int(v) for v in marker.center]) draw_markers(frame, [marker]) save_frame_to_runtime_dir(frame, camera, calibration=True, name=f"key-position-{x}x{y}") log.info( f"Visible squares for key position at {key_position.gantry_position}:\n{json.dumps(sid_center_positions)}" ) gantry.set_position(0, 0)
def setup_board(): log.info('Setting up board.') start_state = Board.get_starting_board_state() board_state = get_board_state() # For each sid, move a piece that is in an incorrect location to the correct location for s_sid, s_piece in start_state.items(): # Get sids that do not have pieces in them free_sids = [ sid for sid in Board.get_all_sids() if sid not in board_state ] # s_piece = correct piece for s_sid, b_piece = current piece in sid b_piece = board_state[s_sid] if s_sid in board_state else None # If the board piece is the same as the start piece, continue if b_piece == s_piece: continue # If there is an incorrect piece in the sid, move it to a free space if b_piece is not None: f_sid = free_sids.pop(0) make_move(f"{s_sid}{f_sid}", board_state) del board_state[s_sid] board_state[f_sid] = b_piece # Get sids of pieces incorrectly located i_sids = [ sid for sid, piece in board_state.items() if piece == s_piece and ( sid not in start_state or start_state[sid] != piece) ] # If there are none on the board, continue if len(i_sids) == 0: continue # Make the move i_sid = i_sids[0] make_move(f"{i_sid}{s_sid}", board_state) del board_state[i_sid] board_state[s_sid] = s_piece
def calculate_fid_correction_coefficients(frame_center): top_img = CALIBRATION_DIR.joinpath('fcc-top.jpg') base_img = CALIBRATION_DIR.joinpath('fcc-base.jpg') if not top_img.exists() or not base_img.exists(): log.error('Missing calibration images.') return top_frame = cv2.imread(str(top_img.absolute())) base_frame = cv2.imread(str(base_img.absolute())) top_markers = Marker.extract_markers(top_frame, marker_family=Marker.FAMILY_tag16h5, scan_for_inverted_markers=True) valid_markers = ['0', '1', '2', '3', '4', '6', '14', '15', '16', '17', '19', '24'] top_markers = list(filter(lambda m: np.linalg.norm(frame_center - m.center) < 600 and m.id in valid_markers, top_markers)) base_markers = Marker.extract_markers(base_frame, marker_family=Marker.FAMILY_tag16h5, scan_for_inverted_markers=True) base_markers = list(filter(lambda m: np.linalg.norm(frame_center - m.center) < 600 and m.id in valid_markers, base_markers)) draw_markers(top_frame, top_markers) draw_markers(base_frame, base_markers) cv2.imshow('top', top_frame) cv2.imshow('base', base_frame) cv2.waitKey() present_top_marker_ids = [m.id for m in top_markers] present_base_marker_ids = [m.id for m in base_markers] if len(present_top_marker_ids) == 0 \ or len(set(present_top_marker_ids)) != len(present_top_marker_ids)\ or set(present_base_marker_ids) != set(present_top_marker_ids): log.error('Marker images are not valid or do not appear consistent.') return fcc = {} for tm in top_markers: bm = [m for m in base_markers if m.id == tm.id][0] bv = bm.center - frame_center tv = tm.center - frame_center x = bv[0] / tv[0] y = bv[1] / tv[1] fcc[tm.id] = (x + y) / 2 log.info(f"FCC:\n{json.dumps(fcc)}")
def get_board_state(save_images=False): """ Analyzes the board to find where all the pieces are. For each key position, move the gantry to that position, take a snapshot and locate the position of each of the visible pieces. :return: [square_id: piece.id] A map of square ids to piece ids. """ log.info('Analyzing board.') board_state = {} for key_position in key_positions: x, y = key_position.gantry_position gantry.set_position(x, y) markers, frame = take_snapshot() markers = filter_markers_by_range(markers, x_range=key_position.x_range, y_range=key_position.y_range) markers = filter_markers_by_id(markers, valid_ids=board.piece_fids) if save_images: draw_markers(frame, markers, board=board) save_frame_to_runtime_dir(frame, camera) for marker in markers: piece_id = board.translate_fid_to_piece(marker.id) sid = key_position.get_closest_sid(marker.center) if sid in board_state and board_state[sid] != piece_id: raise BoardPieceViolation( f"Two pieces ({board_state[sid]}, {piece_id}) found in the same square: {sid}" ) board_state[sid] = piece_id log.info(f"Board state: {board_state}") return board_state
def capture_frame(self, correct_distortion=True, exposure=None): """ Captures a raw RGB color frame from the camera. Corrects distortion if necessary. :param correct_distortion: Tell the function if it should correct for distortion. :param exposure: Exposure to capture. :return: np array of pixel data """ if self.mock_frame_path is not None: log.debug( 'Camera returning a mock frame in place of the captured image.' ) frame = cv2.imread(self.mock_frame_path) self.latest_frame = frame return frame log.info('Warming camera up.') camera = self.generate_camera(exposure) log.info( f"Capturing frame from camera with" f"{'' if correct_distortion else ' no'} distortion correction.") ret, frame = camera.read() if not ret: raise CameraError('Failed to read from from camera.') camera.release() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if correct_distortion: frame = self.correct_distortion(frame) self.latest_frame = frame return frame
def set_z_position(self, p, delay=None): """ Sets the Z position based on the input {p}. If p == 1 the z servo will be fully extended, if p == 0 the z servo will be fully retracted. """ p = max(0, min(p, 1)) log.info(f"Setting z to {int(p * 100)}% extension.") self.z_servo.set_angle( 180 * (1 - p), delay=max(self.z_delay, self.z_delay * abs(self.z_position - p)) if delay is None else delay) self.z_position = p
def take_snapshot(): """ Captures a frame and returns a map of all markers present in the frame as well as the coordinate in the center of the frame. :return: [Marker] A list of markers """ log.info('Taking snapshot from camera.') frame = camera.capture_frame() markers = Marker.extract_markers(frame, marker_family=Marker.FAMILY_tag16h5, scan_for_inverted_markers=True) adjust_markers(markers) return markers, frame
def action_play_self(): _ = gantry.calibrate() moves = [] chess_engine = Board.generate_chess_engine_instance() while True: chess_engine.set_position(moves) move = chess_engine.get_best_move_time(1) if move is None: break log.info(f"Making move: {move}") make_move(move, board_state=Board.fen_to_board_state( chess_engine.get_fen_position())) moves.append(move)
def action_main(): gantry.set_position(100, 100, rel=True, slow=True) play_audio_ids(AUDIO_IDS.START_MESSAGE, AUDIO_IDS.PAUSE_HALF_SECOND, AUDIO_IDS.WAKEUP) # Perform mechanical calibration log.info('Performing gantry calibration.') _ = gantry.calibrate() play_audio_ids(AUDIO_IDS.CALIBRATION_COMPLETE, AUDIO_IDS.PAUSE_HALF_SECOND, AUDIO_IDS.SASS_0, AUDIO_IDS.PAUSE_HALF_SECOND, AUDIO_IDS.HAHA, AUDIO_IDS.PAUSE_HALF_SECOND) # Start playing sequence while True: gantry.set_position(200, 200) check_for_game_options() play_game()
def extract_markers(frame, marker_family, scan_for_inverted_markers=False): """ Takes in a RGB color frame and extracts all of the apriltag markers present. Returns a list of markers. :param frame: The frame to search. :param marker_family: The marker family to search for. :param scan_for_inverted_markers: Determines if the image should be checked for markers that are inverted. :return: {[Marker]} List of markers """ log.info('Extracting apriltag markers from camera frame.' + (' Checking for inverted markers as well.' if scan_for_inverted_markers else '')) markers = [] options = apriltag.DetectorOptions(families=marker_family) detector = apriltag.Detector(options) gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) gray = Camera.blur_frame(gray, 2) results = detector.detect(gray) if scan_for_inverted_markers: results += detector.detect(Camera.invert_colors(gray)) for r in results: c0, c1, c2, c3 = r.corners c0 = np.array([ int(c0[0]), int(c0[1]) ]) c1 = np.array([ int(c1[0]), int(c1[1]) ]) c2 = np.array([ int(c2[0]), int(c2[1]) ]) c3 = np.array([ int(c3[0]), int(c3[1]) ]) fid = str(r.tag_id) markers.append( Marker( fid, [c0, c1, c2, c3] ) ) log.info(f"Found {len(markers)} markers: {Marker.get_fids_from_list(markers)}") return markers
def correct_distortion(self, frame): """ Corrects distortion due to curved lenses using the distortion variables 'k' and 'd'. :param frame: The frame to correct. :return: The corrected frame. """ log.info('Correcting camera distortion on frame.') new_k = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify( self.k, self.d, self.frame_size, np.eye(3), balance=1) m1, m2 = cv2.fisheye.initUndistortRectifyMap(self.k, self.d, np.eye(3), new_k, self.frame_size, cv2.CV_32FC1) undistorted_img = cv2.remap(frame, m1, m2, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) return undistorted_img
def check_for_game_options(): return x, y = key_positions[0].gantry_position gantry.set_position(x, y) play_audio_ids(AUDIO_IDS.OPTIONS_CHECK) wait_for_player_button_press() frame = camera.capture_frame() markers = Marker.extract_markers(frame, Marker.FAMILY_tag36h11) for marker in markers: if marker.id in game_options_map: option = game_options_map[marker.id] log.info(f"Game option found '{option}'") if option == 'level-easy': pass elif option == 'level-medium': pass elif option == 'level-hard': pass elif option == 'level-advanced': pass
def save_frame_to_runtime_dir(frame, camera=None, calibration=False, name=None, name_only=False): """ Saves a frame to the runtime dir. :param calibration: If true the file will be saved to the calibration dir :param name: Name of the file :param frame: The frame to save :param camera: The camera object used to capture the frame. """ data = Image.fromarray(frame) image_name = f"{Log.current_time_in_milliseconds()}" if camera is not None: image_name += f"-e{str(camera.exposure)[:6]}" if name is not None: image_name += f"-{name}" if name_only: image_name = name path = f"{CALIBRATION_DIR if calibration else IMAGES_DIR}/{image_name}.jpg" log.info(f"Saving frame to {path}") data.save(path)
def set_position(self, x, y, rel=False, slow=False): """ Sets the absolute position of the gantry to the given position. :param rel: Set position relatively instead of absolutely. :param x: {int} The x coordinate. :param y: {int} The y coordinate. """ log.info(f"Setting position to ({x}, {y})") if rel: self.x_stepper.set_position_rel(int(x)) self.y0_stepper.set_position_rel(int(y)) self.y1_stepper.set_position_rel(int(y)) else: self.x_stepper.set_position_abs(int(x)) self.y0_stepper.set_position_abs(int(y)) self.y1_stepper.set_position_abs(int(y)) if slow: Stepper.move(self.x_stepper, self.y0_stepper, self.y1_stepper, max_delay=0.0012, min_delay=0.0008) else: Stepper.move(self.x_stepper, self.y0_stepper, self.y1_stepper)
def calibrate(self, test_size=False): """ Calibrates the gantry and sets the current position to [0, 0]. Returns """ base_distance = 150 log.info('Starting calibration sequence.') self.x_stepper.set_position_rel(base_distance) Stepper.move(self.x_stepper, acceleration_function=Stepper.ACCELERATION_SIN) while not self.x_stop.is_pressed(): self.x_stepper.set_position_rel(-3) Stepper.move(self.x_stepper, acceleration_function=Stepper.ACCELERATION_CONST, min_delay=0.004, max_delay=0.004) log.info('X stop found.') x_pos = -self.x_stepper.get_current_position() self.x_stepper.reset() self.y0_stepper.set_position_rel(base_distance) self.y1_stepper.set_position_rel(base_distance) Stepper.move(self.y0_stepper, self.y1_stepper, acceleration_function=Stepper.ACCELERATION_SIN) while True: if self.y0_stop.is_pressed() and self.y1_stop.is_pressed(): break if not self.y0_stop.is_pressed(): self.y0_stepper.set_position_rel(-3) if not self.y1_stop.is_pressed(): self.y1_stepper.set_position_rel(-3) Stepper.move(self.y0_stepper, self.y1_stepper, acceleration_function=Stepper.ACCELERATION_CONST, min_delay=0.004, max_delay=0.004) log.info('Y stops found.') y0_pos = -self.y0_stepper.get_current_position() y1_pos = -self.y1_stepper.get_current_position() y_pos = int(round((y0_pos + y1_pos) / 2)) self.y0_stepper.reset() self.y1_stepper.reset() if test_size: self.y0_stepper.set_position_abs(self.y_size) self.y1_stepper.set_position_abs(self.y_size) self.x_stepper.set_position_abs(self.x_size) Stepper.move(self.y0_stepper, self.y1_stepper, self.x_stepper) self.y0_stepper.set_position_abs(0) self.y1_stepper.set_position_abs(0) self.x_stepper.set_position_abs(0) Stepper.move(self.y0_stepper, self.y1_stepper, self.x_stepper) return x_pos, y_pos
def get_shortest_clear_path(move, board_state): """ Returns the shortest path with no pieces in the way or None if there is not a clear path. """ log.info('Searching for a clear path.') s_sid, e_sid = move[:2], move[2:4] explored = [s_sid] paths = [[s_sid]] # For a max of 14 steps search_max = 14 while search_max > 0: search_max -= 1 # For each path for i in range(len(paths) - 1, -1, -1): c_sid = paths[i][-1] # If the path has reached the target, skip over the path if c_sid == e_sid: continue # Get the surrounding empty and unexplored sids surrounding = [ sid for sid in board.get_surrounding_sids(c_sid) if (sid not in board_state or sid == e_sid) and sid not in explored ] # Generate new paths for each and remove the older path if len(surrounding) > 0: for sid in surrounding: sid_distance = np.linalg.norm( np.array(board.get_square_location(sid)) - np.array(board.get_square_location(e_sid))) c_sid_distance = np.linalg.norm( np.array(board.get_square_location(c_sid)) - np.array(board.get_square_location(e_sid))) # Mark sid as explored if it is further away from the target than the current sid # This is done to reduce jagged paths by allowing alternate equal length routes if sid_distance > c_sid_distance: explored.append(sid) paths.append(paths[i] + [sid]) paths.pop(i) refined_paths = [refine_path(p) for p in paths if p[-1] == e_sid] if len(refined_paths) == 0: log.info('Could not find a clear path.') return None clear_path = sorted(refined_paths, key=lambda x: len(x)).pop(0) # If path has too many turns, return None if len(clear_path) > 3: return None log.info(f"Found clear path {clear_path}") return clear_path
def cleanup_runtime_dir(): log.info('Cleaning up runtime directory') image_retain_milliseconds = 1000 * 30 for filename in os.listdir(IMAGES_DIR): timestamp = filename.split('.')[0].split('-')[0] if timestamp.isnumeric(): timestamp = int(timestamp) if Log.current_time_in_milliseconds( ) - timestamp < image_retain_milliseconds: continue image_path = str(IMAGES_DIR.joinpath(filename).absolute()) os.remove(image_path) log.info(f"Removed image {filename} from runtime/images") log_retain_milliseconds = 60 * 60 * 12 * 1000 for filename in os.listdir(LOG_DIR): timestamp = filename.split('.')[0] if timestamp.isnumeric(): timestamp = int(timestamp) if Log.current_time_in_milliseconds( ) - timestamp < log_retain_milliseconds: continue log_path = str(LOG_DIR.joinpath(filename).absolute()) os.remove(log_path) log.info(f"Removed image {filename} from runtime/logs")
def release_grip(self): log.info('Releasing grip.') self.gripper.demagnetize()
def engage_grip(self): log.info('Engaging grip.') self.gripper.magnetize()
def play_game(): """ Game play logic. """ # Initialize state history, move list, and chess engine. Then verify the board is in starting position. state_history = [Board.get_starting_board_state()] moves = [] chess_engine = Board.generate_chess_engine_instance() verify_initial_state() # Begin the game play_audio_ids(AUDIO_IDS.BEFORE_GAME, AUDIO_IDS.GOOD_LUCK) best_player_move = None while True: log.info('Waiting for player move') wait_for_player_button_press() # Analyze the board to get the board state. If it fails, retry once before asking for the user # to intervene. Repeat until board state can be captured. attempts = 0 should_request_user_intervention = False while True: attempts += 1 if should_request_user_intervention: play_audio_ids(AUDIO_IDS.USER_CHECK_BOARD) log.info('Requesting user intervention.') wait_for_player_button_press() try: board_state = get_board_state(save_images=True) break except BoardPieceViolation as error: log.error(error) should_request_user_intervention = attempts % 2 == 0 # Get previous state in order to extract the move made by the player and add it to the move list previous_state = state_history[-1] log.debug( f"Previous state: {Board.board_state_to_fen(previous_state)}") log.debug(f"Board state: {Board.board_state_to_fen(board_state)}") try: detected_move = Board.get_move_from_board_states( previous_state, board_state, moves, chess_engine) except InvalidMove as err: # If an invalid move was detected, notify the payer and try again log.error(f"Invalid move detected: {err}") play_audio_ids(AUDIO_IDS.INVALID_MOVE) continue except NoMoveFound: # If no move was found, notify the player and try again log.error('No move found.') play_audio_ids(AUDIO_IDS.NO_MOVE_FOUND) continue log.info(f"Detected move {detected_move} from player") state_history.append(board_state) moves.append(detected_move) log.debug(f"Previous moves: {','.join(moves)}") # Update the chess engine with the latest moves chess_engine.set_position(moves) log.debug( f"Making move from current board:\n{chess_engine.get_board_visual()}{chess_engine.get_fen_position()}" ) # Generate the best move, append it to the moves list generated_move = chess_engine.get_best_move_time(2) # If the move is None, the player won if generated_move is None: play_audio_ids(AUDIO_IDS.LOST) log.info('Player won.') break # If the move is a promotion, make sure that it has the reserve piece if len(generated_move) == 5: piece_to_promote = generated_move[-1] black_pieces_on_board = ''.join([ board_state[sid] for sid in board_state if board_state[sid] in Board.get_black_pieces() ]) promotion_candidates = Board.get_full_black_pieces() promotion_candidates.replace('p', '') promotion_candidates.replace('k', '') for piece in black_pieces_on_board: promotion_candidates.replace(piece, '', 1) log.info(f"Promotion candidates {promotion_candidates}") if piece_to_promote not in promotion_candidates: if len(promotion_candidates) == 0: log.error('No promotion candidates') break generated_move = generated_move[:4] + random.choice( promotion_candidates) moves.append(generated_move) log.info(f"Moves: {','.join(moves)}") chess_engine.set_position(moves) state_history.append( Board.fen_to_board_state(chess_engine.get_fen_position())) # Generate the best move for the player to take next best_player_move = chess_engine.get_best_move_time(1) # Make the move TODO: handle move failure log.info(f"Making move {generated_move}") try: make_move(generated_move, board_state) except InvalidMove or InconsistentBoardState as err: log.error(f"Move failed due to: {err}") break # If the player has no valid moves, beth wins if best_player_move is None: play_audio_ids(AUDIO_IDS.WON) break # Reset to the key position x, y = key_positions[0].gantry_position gantry.set_position(x, y)
def action_determine_current_position(): """ Logs the current position of the gantry. """ x, y = gantry.calibrate() log.info(f"Gantry was at position {x}, {y}")
def action_show_board_state(): gantry.calibrate() state = get_board_state(save_images=True) chess_engine = Board.generate_chess_engine_instance() chess_engine.set_fen_position(Board.board_state_to_fen(state)) log.info('Board state:\n' + chess_engine.get_board_visual())
def make_move(move, board_state): """ Given a FEN move, execute the move on the board. :throws: InvalidMove, InconsistentBoardState """ # TODO: castling, pawn promotion # Raise exception if the move was invalid if len(move) < 4 or len(move) > 5: raise InvalidMove(f"{move} is invalid") # Split the move into 2 sids s_sid, e_sid, promotion_piece = move[:2], move[2:4], None if len( move) == 4 else move[-1] # Check is castling move is_castling = s_sid == 'e8' and (e_sid == 'g8' or e_sid == 'b8') and board_state[s_sid] == 'k' # Retrieve the sid positions for the gantry sx, sy = board.get_square_location(s_sid) ex, ey = board.get_square_location(e_sid) # Search for a clear path to take shortest_clear_path = get_shortest_clear_path(move, board_state) # If move captures a piece, remove the captured piece from the board if e_sid in board_state: extension_amount = get_extension_amount(board_state[e_sid]) gantry.set_position(ex, ey) gantry.set_z_position(extension_amount) gantry.engage_grip() gantry.set_z_position(min_extension) # TODO: have the machine place pieces in an area they can be retrieved gantry.set_position(100, 100) gantry.set_z_position(max_extension) gantry.release_grip() gantry.set_z_position(min_extension) # Raise an exception if the board state is inconsistent with the move if s_sid not in board_state: raise InconsistentBoardState(f"Could not find piece in sid {s_sid}") # Move the target piece to its destination gantry.set_position(sx, sy) extension_amount = get_extension_amount(board_state[s_sid]) gantry.set_z_position(extension_amount) gantry.engage_grip() gantry.set_z_position( min_extension if shortest_clear_path is None else extension_amount - 0.3, delay=None if shortest_clear_path is None else 0.05) # If a clear path exists, use it if shortest_clear_path is not None: log.info(f"Using shortest clear path {shortest_clear_path}") for sid in shortest_clear_path: tx, ty = board.get_square_location(sid) gantry.set_position(tx, ty) gantry.set_position(ex, ey) gantry.set_z_position(extension_amount, delay=None if shortest_clear_path is None else 0.5) gantry.release_grip() gantry.set_z_position(min_extension) # Ask for piece promotion if promotion_piece is not None: play_audio_ids(AUDIO_IDS.PIECE_PROMOTION, promotion_piece) # Perform castling if required if is_castling: rx, ry = 0, 0 nx, ny = 0, 0 if e_sid == 'g8': rx, ry = board.get_square_location('h8') nx, ny = board.get_square_location('f8') elif e_sid == 'b8': rx, ry = board.get_square_location('a8') nx, ny = board.get_square_location('c8') gantry.set_position(rx, ry) gantry.set_z_position(get_extension_amount('r')) gantry.engage_grip() gantry.set_z_position(min_extension) gantry.set_position(nx, ny) gantry.set_z_position(get_extension_amount('r')) gantry.release_grip() gantry.set_z_position(min_extension)
import sys from src.misc.Log import log mock_gpio_enabled = "--mock-gpio" in sys.argv if mock_gpio_enabled: log.info("--mock-gpio enabled. All GPIO setup and output will be mocked.") from src.misc.MockGPIO import MockGPIO as gpio else: import RPi.GPIO as gpio gpio.setwarnings(False) import time import math gpio.setmode(gpio.BCM) def cleanup(): """ Cleans up gpio """ gpio.cleanup() def p_out(pin, val): """ Validates the pin is not None and then writes the value. :param pin: Pin to output to. :param val: Value to output. """
def get_move_from_board_states(board_state_before, board_state_after, previous_moves, chess_engine: Stockfish): """ Determine the move given the state before the move and the state after the move. """ # Determine the sids in the prev state that differ in the current state changed_before = [ sid for sid in board_state_before if sid not in board_state_after or board_state_after[sid] != board_state_before[sid] ] # Determine the sids in the current state that differ from the prev state changed_after = [ sid for sid in board_state_after if sid not in board_state_before or board_state_after[sid] != board_state_before[sid] ] # Check for promoted pieces promoted_piece = None pieces_before = ''.join( sorted([ p for p in board_state_before.values() if p in Board.get_white_pieces() ])) pieces_after = ''.join( sorted([ p for p in board_state_after.values() if p in Board.get_white_pieces() ])) if pieces_before != pieces_after: differing_pieces = pieces_before + pieces_after for p in pieces_before: if p in pieces_after: differing_pieces.replace(p, '', 2) log.info( f"Found differing pieces between states: {differing_pieces}") potential_piece = differing_pieces.replace('p', '') if len(differing_pieces ) != 2 or 'p' not in differing_pieces or len( potential_piece) != 1: raise InvalidMove('Piece promotion is invalid.') promoted_piece = potential_piece # If no change raise exception if len(changed_before) == 0 or len(changed_after) == 0: raise NoMoveFound() # If more than one sid was changed in the after board state this could be a castling move if len(changed_after) != 1: # Construct a set of all the pieces involved in the move pieces_moved = set([board_state_before[k] for k in changed_before] + [board_state_after[k] for k in changed_after]) # Check if the move is a castling move is_castling_move = len(pieces_moved) == 2 and ( ('r' in pieces_moved and 'k' in pieces_moved) or ('R' in pieces_moved and 'K' in pieces_moved)) # If it is a castling move, filter out board state changes that are not related to the king if is_castling_move: log.info('Castling move detected.') board_state_before = { k: board_state_before[k] for k in board_state_before if (board_state_before[k] == 'k' or board_state_before[k] == 'K') } board_state_after = { k: board_state_after[k] for k in board_state_after if (board_state_after[k] == 'k' or board_state_after[k] == 'K') } changed_before = [ sid for sid in board_state_before if sid not in board_state_after or board_state_after[sid] != board_state_before[sid] ] changed_after = [ sid for sid in board_state_after if sid not in board_state_before or board_state_after[sid] != board_state_before[sid] ] else: raise InvalidMove('Move affected too many sids.') # Get the move end sid e_sid = changed_after[0] piece_moved = board_state_after[ e_sid] if promoted_piece is None else promoted_piece # Find the move start sid s_sid = None for sid in changed_before: if board_state_before[sid] == piece_moved: s_sid = sid if s_sid is None: raise InvalidMove('Could not find move start.') move = f"{s_sid}{e_sid}{promoted_piece if promoted_piece is not None else ''}" # Verify that the move is valid chess_engine.set_position(previous_moves) if not chess_engine.is_move_correct(move): raise InvalidMove(f"Move {move} is invalid.") return move
LOG_DIR = str(src.parent.absolute().joinpath('runtime').absolute().joinpath( 'logs').absolute()) from src.tracking.Board import Board, KeyPosition from src.tracking.Marker import Marker from src.mechanical.Camera import Camera from src.mechanical.Gantry import Gantry from src.misc.Exceptions import InvalidMove, InconsistentBoardState, NoMoveFound from src.misc.Helpers import * from src.calibration.Calibration import calculate_fid_correction_coefficients from src.misc.Log import log from src.audio.Audio import play_audio_ids, AUDIO_IDS """ Initialize global objects/variables using the config file. """ log.info('Initializing components.') # Read the config file f = open(str(src.parent.joinpath('config.json').absolute())) config = json.load(f) f.close() # Extract global variables from the config file fcc_map = config['fid-correction-coefficients'] key_positions = [ KeyPosition(position=kp['gantry-position'], sid_centers=kp['sid-centers'], sid_fid_mapping=kp['square-calibration-fid-mapping'], x_range=kp['x-range'], y_range=kp['y-range']) for kp in config['key-positions'] ] extension_values = config['z-axis-extension'] min_extension = extension_values['min']