def select_move(self, game_state: GameState): winning_moves = [] draw_moves = [] losing_moves = [] # loop through all legal moves: for move in game_state.legal_moves(): # state of the game after this move is applied next_state = game_state.apply_move(move) # determine opponent's best outcome given that state opponent_best_outcome = best_result(next_state) our_best_outcome = reverse_game_result(opponent_best_outcome) if our_best_outcome == GameResult.win: winning_moves.append(move) elif our_best_outcome == GameResult.draw: draw_moves.append(move) else: losing_moves.append(move) # try to win, with drawing the next best choice if winning_moves or draw_moves: return random.choice(winning_moves or draw_moves) # lost the game return random.choice(draw_moves)
def best_result(game_state: GameState): if game_state.is_over(): if game_state.winner() == game_state.next_player: return GameResult.win elif game_state.winner() is None: return GameResult.draw else: return GameResult.loss best_result_so_far = GameResult.loss for candidate_move in game_state.legal_moves(): next_state = game_state.apply_move(candidate_move) opponent_best_result = best_result(next_state) our_result = reverse_game_result(opponent_best_result) best_result_so_far = max(our_result, best_result_so_far) return best_result_so_far
class Position: ruleset: str = 'japanese' komi: float = 6.5 initial_black: Optional[List[str]] = None initial_white: Optional[List[str]] = None initial_player: Optional[str] = 'b' moves: Optional[List[str]] = None _game: Optional[GameState] = None @property def game(self) -> GameState: if not self._game: black = batch_translate_labels_to_coordinates(self.initial_black) white = batch_translate_labels_to_coordinates(self.initial_white) move_points = batch_translate_labels_to_coordinates(self.moves) player = Player.black if self.initial_player == 'b' else Player.white board = Board(19, 19) for b in black: board.place_stone(Player.black, b) for w in white: board.place_stone(Player.white, w) self._game = GameState(board, player, None, None) for move_point in move_points: move = Move.pass_turn() if not move_point else Move.play( move_point) self._game = self._game.apply_move(move) return self._game def command(self, move: Move = None) -> Tuple[Command, int]: # Get the game that reflects the passed move having been played (if supplied). game = self.game if not move else self.game.apply_move(move) # Process the game board to get the necessary information to generate canonical encodings. point_plus_code: List[Tuple[Point, int]] = [] for i in intersections: color = game.board.get(i) if not color: code = 0 if game.is_valid_move(Move.play(i)) else 3 else: code = 1 if color == Player.black else 2 if code: point_plus_code.append((i, code)) # Select the transformation that leads to the lowest canonical position representation. selected_form = float('inf') selected_ordinal = -1 selected_transformation = None for ordinal, transformation in enumerate(transformations): encoding = self._encode_point_colorings(point_plus_code, transformation) if encoding < selected_form: selected_form = encoding selected_ordinal = ordinal selected_transformation = transformation # Encode the resulting board position as a string. position_representation = self._convert_code_to_dense_string( selected_form) # Transform the starting stone points using the selected transformation. initial_positions_plus_colors: List[Tuple[Point, int]] = [] initial_stones: List[Move] = [] if self.initial_black: transformed_black_points = [ selected_transformation(translate_label_to_point(x)) for x in self.initial_black ] initial_positions_plus_colors += [ (x, 1) for x in transformed_black_points ] initial_stones += [ MoveInfo(KataGoPlayer.b, coords_from_point(x)) for x in transformed_black_points ] if self.initial_white: transformed_white_points = [ selected_transformation(translate_label_to_point(x)) for x in self.initial_white ] initial_positions_plus_colors += [ (x, 2) for x in transformed_white_points ] initial_stones += [ MoveInfo(KataGoPlayer.w, coords_from_point(x)) for x in transformed_white_points ] initial_form = self._encode_point_colorings( initial_positions_plus_colors) initial_representation = self._convert_code_to_dense_string( initial_form) # Compose an ID to use when communicating with KataGo. Because it is possible to arrive at the same position # in multiple paths and/or transformations, this ID does NOT contain the information necessary to return to the # original representation. That exists for communicating between the UI and the server ONLY. next_player = "b" if game.next_player == Player.black else "w" id = f'{self.ruleset}_{self.komi}_{next_player}_{initial_representation}_{position_representation}' # Build the command! command = Command() command.id = id command.komi = self.komi command.initialPlayer = KataGoPlayer.b if self.initial_player == 'b' else KataGoPlayer.w command.rules = self.ruleset command.initialStones = initial_stones command.moves = [] player = command.initialPlayer for move in game.history: move_info = MoveInfo( player, 'pass' if not move or move.is_pass else coords_from_point( selected_transformation(move.point))) command.moves.append(move_info) player = player.opposite command.analyzeTurns = [len(command.moves)] return command, selected_ordinal def _encode_point_colorings( self, point_plus_code: List[Tuple[Point, int]], transformation: Optional[Callable[[Optional[Point]], Optional[Point]]] = None ) -> int: encoding = 0 for point, code in point_plus_code: transformed = transformation(point) if transformation else point power = (transformed.row - 1) * 19 + (transformed.col - 1) encoding += code * (4**power) return encoding def _convert_code_to_dense_string(self, value: int) -> str: if not value: value = 0 bit_count = value.bit_length() return base64.b64encode( value.to_bytes(bit_count // 8 + (0 if bit_count % 8 == 0 else 1), byteorder='big')).decode('utf-8')