# Path: lantern/score.py
# def score(text, *score_functions):
#     """Score ``text`` using ``score_functions``.
# 
#     Examples:
#         >>> score("abc", function_a)
#         >>> score("abc", function_a, function_b)
# 
#     Args:
#         text (str): The text to score
#         *score_functions (variable length argument list): functions to score with
# 
#     Returns:
#         Arithmetic mean of scores
# 
#     Raises:
#         ValueError: If score_functions is empty
#     """
#     if not score_functions:
#         raise ValueError("score_functions must not be empty")
# 
#     return statistics.mean(func(text) for func in score_functions)
# 
# Path: lantern/analysis/search.py
# def hill_climb(nsteps, start_node, get_next_node):
#     """Modular hill climbing algorithm.
# 
#     Example:
#         >>> def get_next_node(node):
#         ...     a, b = random.sample(range(len(node)), 2)
#         ...     node[a], node[b] = node[b], node[a]
#         ...     plaintext = decrypt(node, ciphertext)
#         ...     score = lantern.score(plaintext, *fitness_functions)
#         ...     return node, score, Decryption(plaintext, ''.join(node), score)
#         >>> final_node, best_score, outputs = hill_climb(10, "ABC", get_next_node)
# 
#     Args:
#         nsteps (int): The number of neighbours to visit
#         start_node: The starting node
#         get_next_node (function): Function to return the next node
#             the score of the current node and any optional output from the current node
# 
#     Returns:
#         The highest node found, the score of this node and the outputs from the best nodes along the way
#     """
#     outputs = []
#     best_score = -float('inf')
# 
#     for step in range(nsteps):
#         next_node, score, output = get_next_node(copy.deepcopy(start_node))
# 
#         # Keep track of best score and the start node becomes finish node
#         if score > best_score:
#             start_node = copy.deepcopy(next_node)
#             best_score = score
#             outputs.append(output)
# 
#     return start_node, best_score, outputs
# 
# Path: lantern/structures/decryption.py
# class Decryption:
#     """A decryption object, composed of plaintext, a score and the key.
# 
#     Example:
#         >>> decryption = Decryption("example", "key", -10)
#         >>> decryption.plaintext
#         example
#         >>> decryption.key
#         key
#         >>> decryption.score
#         -10
#     """
# 
#     def __init__(self, plaintext, key, score):
#         """
#         Args:
#             plaintext: The decrypted ciphertext
#             key: The key which resulted in this decryption
#             score: The score of this decryption
#         """
#         self.plaintext = plaintext
#         self.key = key
#         self.score = score
# 
#     def __lt__(self, other):
#         """Compare decryptions with other decryptions by score.
# 
#         Args:
#             other: Object to compare with
# 
#         Returns:
#             True if self is less than other, else False
#         """
#         return self.score < other.score


import random
import string
from lantern import score
from lantern.analysis.search import hill_climb
from lantern.structures import Decryption

    Keyword Args:
        ntrials (int): The number of times to run the hill climbing algorithm
        nswaps (int): The number of rounds to find a local maximum

    Returns:
        Sorted list of decryptions

    Raises:
        ValueError: If nswaps or ntrails are not positive integers
        ValueError: If no fitness_functions are given
    """
    if ntrials <= 0 or nswaps <= 0:
        raise ValueError("ntrials and nswaps must be positive integers")

    # Find a local maximum by swapping two letters and scoring the decryption
    def next_node_inner_climb(node):
        # Swap 2 characters in the key
        a, b = random.sample(range(len(node)), 2)
        node[a], node[b] = node[b], node[a]
        plaintext = decrypt(node, ciphertext)
        node_score = score(plaintext, *fitness_functions)
        return node, node_score, Decryption(plaintext, ''.join(node), node_score)

    # Outer climb rereuns hill climb ntrials number of times each time at a different start location
    def next_node_outer_climb(node):
        random.shuffle(node)
        key, best_score, outputs = hill_climb(nswaps, node[:], next_node_inner_climb)
        return key, best_score, outputs[-1]  # The last item in this list is the item with the highest score


# Path: lantern/score.py
# def score(text, *score_functions):
#     """Score ``text`` using ``score_functions``.
# 
#     Examples:
#         >>> score("abc", function_a)
#         >>> score("abc", function_a, function_b)
# 
#     Args:
#         text (str): The text to score
#         *score_functions (variable length argument list): functions to score with
# 
#     Returns:
#         Arithmetic mean of scores
# 
#     Raises:
#         ValueError: If score_functions is empty
#     """
#     if not score_functions:
#         raise ValueError("score_functions must not be empty")
# 
#     return statistics.mean(func(text) for func in score_functions)
# 
# Path: lantern/analysis/search.py
# def hill_climb(nsteps, start_node, get_next_node):
#     """Modular hill climbing algorithm.
# 
#     Example:
#         >>> def get_next_node(node):
#         ...     a, b = random.sample(range(len(node)), 2)
#         ...     node[a], node[b] = node[b], node[a]
#         ...     plaintext = decrypt(node, ciphertext)
#         ...     score = lantern.score(plaintext, *fitness_functions)
#         ...     return node, score, Decryption(plaintext, ''.join(node), score)
#         >>> final_node, best_score, outputs = hill_climb(10, "ABC", get_next_node)
# 
#     Args:
#         nsteps (int): The number of neighbours to visit
#         start_node: The starting node
#         get_next_node (function): Function to return the next node
#             the score of the current node and any optional output from the current node
# 
#     Returns:
#         The highest node found, the score of this node and the outputs from the best nodes along the way
#     """
#     outputs = []
#     best_score = -float('inf')
# 
#     for step in range(nsteps):
#         next_node, score, output = get_next_node(copy.deepcopy(start_node))
# 
#         # Keep track of best score and the start node becomes finish node
#         if score > best_score:
#             start_node = copy.deepcopy(next_node)
#             best_score = score
#             outputs.append(output)
# 
#     return start_node, best_score, outputs
# 
# Path: lantern/structures/decryption.py
# class Decryption:
#     """A decryption object, composed of plaintext, a score and the key.
# 
#     Example:
#         >>> decryption = Decryption("example", "key", -10)
#         >>> decryption.plaintext
#         example
#         >>> decryption.key
#         key
#         >>> decryption.score
#         -10
#     """
# 
#     def __init__(self, plaintext, key, score):
#         """
#         Args:
#             plaintext: The decrypted ciphertext
#             key: The key which resulted in this decryption
#             score: The score of this decryption
#         """
#         self.plaintext = plaintext
#         self.key = key
#         self.score = score
# 
#     def __lt__(self, other):
#         """Compare decryptions with other decryptions by score.
# 
#         Args:
#             other: Object to compare with
# 
#         Returns:
#             True if self is less than other, else False
#         """
#         return self.score < other.score


# Path: lantern/modules/simplesubstitution.py
import random
import string
from lantern import score
from lantern.analysis.search import hill_climb
from lantern.structures import Decryption


    Keyword Args:
        ntrials (int): The number of times to run the hill climbing algorithm
        nswaps (int): The number of rounds to find a local maximum

    Returns:
        Sorted list of decryptions

    Raises:
        ValueError: If nswaps or ntrails are not positive integers
        ValueError: If no fitness_functions are given
    """
    if ntrials <= 0 or nswaps <= 0:
        raise ValueError("ntrials and nswaps must be positive integers")

    # Find a local maximum by swapping two letters and scoring the decryption
    def next_node_inner_climb(node):
        # Swap 2 characters in the key
        a, b = random.sample(range(len(node)), 2)
        node[a], node[b] = node[b], node[a]
        plaintext = decrypt(node, ciphertext)
        node_score = score(plaintext, *fitness_functions)
        return node, node_score, Decryption(plaintext, ''.join(node), node_score)

    # Outer climb rereuns hill climb ntrials number of times each time at a different start location
    def next_node_outer_climb(node):
        random.shuffle(node)
        key, best_score, outputs = hill_climb(nswaps, node[:], next_node_inner_climb)
        return key, best_score, outputs[-1]  # The last item in this list is the item with the highest score


    _, _, decryptions = hill_climb(ntrials, list(string.ascii_uppercase), next_node_outer_climb)