"""The Game of Hog."""

from dice import four_sided, six_sided, make_test_dice
from ucb import main, trace, log_current_line, interact
from math import sqrt

GOAL_SCORE = 100  # The goal of Hog is to score 100 points.


######################
# Phase 1: Simulator #
######################


def roll_dice(num_rolls, dice=six_sided):
    """Simulate rolling the DICE exactly NUM_ROLLS times. Return the sum of
    the outcomes unless any of the outcomes is 1. In that case, return 0.
    """
    # These assert statements ensure that num_rolls is a positive integer.
    assert type(num_rolls) == int, 'num_rolls must be an integer.'
    assert num_rolls > 0, 'Must roll at least once.'
    # BEGIN Question 1
    score, turn, rolled_one = 0, 1, False
    while turn <= num_rolls:
        dice_val = dice()
        if dice_val == 1:
            rolled_one = True
        score, turn = score + dice_val, turn + 1
    if rolled_one:
        score = 0
    return score
    # END Question 1

def is_prime(val):
    if val <= 1:
        return False
    else:
        div = 2
        while div * div <= val:
            if val % div == 0:
                return False
            div = div + 1
    return True

def next_prime(val):
    if val == 2:
        val = val + 1
    else:
        val = val + 2
    while not is_prime(val):
        val = val + 2
    return val


def take_turn(num_rolls, opponent_score, dice=six_sided):
    """Simulate a turn rolling NUM_ROLLS dice, which may be 0 (Free bacon).

    num_rolls:       The number of dice rolls that will be made.
    opponent_score:  The total score of the opponent.
    dice:            A function of no args that returns an integer outcome.
    """
    assert type(num_rolls) == int, 'num_rolls must be an integer.'
    assert num_rolls >= 0, 'Cannot roll a negative number of dice.'
    assert num_rolls <= 10, 'Cannot roll more than 10 dice.'
    assert opponent_score < 100, 'The game should be over.'
    # BEGIN Question 2
    dice_score = 1
    if num_rolls == 0:
        while opponent_score != 0:
            digit, opponent_score = opponent_score % 10, opponent_score // 10
            if digit + 1 > dice_score:
                dice_score = digit + 1
    else:
        dice_score = roll_dice(num_rolls, dice)

    # Hogtimus Prime: if the score is a prime number, boost to the next prime
    if is_prime(dice_score):
        dice_score = next_prime(dice_score)
    return dice_score
    # END Question 2


def select_dice(score, opponent_score):
    """Select six-sided dice unless the sum of SCORE and OPPONENT_SCORE is a
    multiple of 7, in which case select four-sided dice (Hog wild).
    """
    # BEGIN Question 3
    if (score + opponent_score) % 7 == 0:
        return four_sided
    return six_sided
    # END Question 3


def is_swap(score0, score1):
    """Returns whether the last two digits of SCORE0 and SCORE1 are reversed
    versions of each other, such as 19 and 91 and 2 and 20
    """
    # BEGIN Question 4
    last0, last1 = score0 % 100, score1 % 100
    if last0 < last1:
        return is_swap(last1, last0)
    rev = 0
    while last0 != 0:
       rev, last0 = rev * 10 + last0 % 10, last0 // 10
    return rev == last1
    # END Question 4


def other(who):
    """Return the other player, for a player WHO numbered 0 or 1.

    >>> other(0)
    1
    >>> other(1)
    0
    """
    return 1 - who


def play(strategy0, strategy1, score0=0, score1=0, goal=GOAL_SCORE):
    """Simulate a game and return the final scores of both players, with
    Player 0's score first, and Player 1's score second.

    A strategy is a function that takes two total scores as arguments
    (the current player's score, and the opponent's score), and returns a
    number of dice that the current player will roll this turn.

    strategy0:  The strategy function for Player 0, who plays first
    strategy1:  The strategy function for Player 1, who plays second
    score0   :  The starting score for Player 0
    score1   :  The starting score for Player 1
    """
    who = 0  # Which player is about to take a turn, 0 (first) or 1 (second)
    # BEGIN Question 5
    while score0 < goal and score1 < goal:
        num_roll, curscore, opposcore = 0, 0, 0
        if who == 0:
            num_roll, curscore, opposcore = strategy0(score0, score1), score0, score1
        else:
            num_roll, curscore, opposcore = strategy1(score1, score0), score1, score0
        score_add = take_turn(num_roll, opposcore, select_dice(curscore, opposcore))
        # PiggyBack:
        if score_add == 0:
           opposcore += num_roll
        curscore = curscore + score_add
        if is_swap(curscore, opposcore):
            curscore, opposcore = opposcore, curscore 
        if who == 0:
            score0, score1 = curscore, opposcore
        else:
            score1, score0 = curscore, opposcore
        who = other(who)
    # END Question 5
    return score0, score1


#######################
# Phase 2: Strategies #
#######################


def always_roll(n):
    """Return a strategy that always rolls N dice.

    A strategy is a function that takes two total scores as arguments
    (the current player's score, and the opponent's score), and returns a
    number of dice that the current player will roll this turn.

    >>> strategy = always_roll(5)
    >>> strategy(0, 0)
    5
    >>> strategy(99, 99)
    5
    """
    def strategy(score, opponent_score):
        return n

    return strategy


# Experiments

def make_averaged(fn, num_samples=1000):
    """Return a function that returns the average_value of FN when called.

    To implement this function, you will have to use *args syntax, a new Python
    feature introduced in this project.  See the project description.

    >>> dice = make_test_dice(3, 1, 5, 6)
    >>> averaged_dice = make_averaged(dice, 1000)
    >>> averaged_dice()
    3.75
    >>> make_averaged(roll_dice, 1000)(2, dice)
    5.5

    In this last example, two different turn scenarios are averaged.
    - In the first, the player rolls a 3 then a 1, receiving a score of 0.
    - In the other, the player rolls a 5 and 6, scoring 11.
    Thus, the average value is 5.5.
    Note that the last example uses roll_dice so the hogtimus prime rule does
    not apply.
    """
    # BEGIN Question 6
    def averaged_fn(*args):
        score, rd = 0, 0
        while  rd < num_samples:
           score, rd = score + fn(*args), rd + 1
        return score / num_samples
    return averaged_fn

    # END Question 6


def max_scoring_num_rolls(dice=six_sided, num_samples=1000):
    """Return the number of dice (1 to 10) that gives the highest average turn
    score by calling roll_dice with the provided DICE over NUM_SAMPLES times.
    Assume that dice always return positive outcomes.

    >>> dice = make_test_dice(3)
    >>> max_scoring_num_rolls(dice)
    10
    """
    # BEGIN Question 7
    "*** REPLACE THIS LINE ***"
    num_roll, opt_roll, max_score, average_dice = 1, 0, -1, make_averaged(roll_dice, num_samples)
    while num_roll <= 10:
        score = average_dice(num_roll, dice) 
        if score > max_score:
            opt_roll, max_score = num_roll, score
        num_roll += 1
    return opt_roll 
    # END Question 7


def winner(strategy0, strategy1):
    """Return 0 if strategy0 wins against strategy1, and 1 otherwise."""
    score0, score1 = play(strategy0, strategy1)
    if score0 > score1:
        return 0
    else:
        return 1


def average_win_rate(strategy, baseline=always_roll(5)):
    """Return the average win rate of STRATEGY against BASELINE. Averages the
    winrate when starting the game as player 0 and as player 1.
    """
    win_rate_as_player_0 = 1 - make_averaged(winner)(strategy, baseline)
    win_rate_as_player_1 = make_averaged(winner)(baseline, strategy)

    return (win_rate_as_player_0 + win_rate_as_player_1) / 2


def run_experiments():
    """Run a series of strategy experiments and report results."""
    if False:  # Change to False when done finding max_scoring_num_rolls
        six_sided_max = max_scoring_num_rolls(six_sided)
        print('Max scoring num rolls for six-sided dice:', six_sided_max)
        four_sided_max = max_scoring_num_rolls(four_sided)
        print('Max scoring num rolls for four-sided dice:', four_sided_max)

    if True:  # Change to True to test always_roll(8)
        print('always_roll(8) win rate:', average_win_rate(always_roll(8)))

    if True:  # Change to True to test bacon_strategy
        print('bacon_strategy win rate:', average_win_rate(bacon_strategy))

    if True:  # Change to True to test swap_strategy
        print('swap_strategy win rate:', average_win_rate(swap_strategy))

    "*** You may add additional experiments as you wish ***"


# Strategies

def zero_roll_gain(score, opponent_score):
    score = 1 
    while opponent_score > 0:
        score_gain, opponent_score = opponent_score % 10 + 1, opponent_score // 10
        if score_gain > score:
            score = score_gain
    if is_prime(score):
        score = next_prime(score)
    return score


def bacon_strategy(score, opponent_score, margin=8, num_rolls=5):
    """This strategy rolls 0 dice if that gives at least MARGIN points,
    and rolls NUM_ROLLS otherwise.
    """
    # BEGIN Question 8
    score_gain = zero_roll_gain(score, opponent_score)
    if score_gain >= margin:
        return 0
    return num_rolls
    # END Question 8


def swap_strategy(score, opponent_score, num_rolls=5):
    """This strategy rolls 0 dice when it results in a beneficial swap and
    rolls NUM_ROLLS otherwise.
    """
    # BEGIN Question 9
    add_score = zero_roll_gain(score, opponent_score)
    score += add_score
    if is_swap(score, opponent_score) and score < opponent_score:
        return 0
    return num_rolls
    # END Question 9


def final_strategy(score, opponent_score):
    """Write a brief description of your final strategy.

    """
    # BEGIN Question 10
    noroll_gain = zero_roll_gain(score, opponent_score)

    # When scores are higher, try roll less dice, in case we roll a 1
    margin, num_roll = 8, 6 

    if score > 75:
        margin, num_roll = 7, 5
    elif score > 84:
        margin, num_roll = 6, 3
    elif score > 93:
        margin, num_roll = 4, 2

    if (score + opponent_score) % 7 == 0:
        margin, num_roll = 4, 3

    # If winning with 0 roll, then do it
    if score + noroll_gain >= 100:
        return 0

    # Force opponent to roll 4-sided dice if lagged away over 15 points
    if (score + noroll_gain + opponent_score) % 7 == 0 and (opponent_score - score >= 10):
        return 0

    # If swap is beneficial, do it
    if is_swap(score + noroll_gain, opponent_score) and score < opponent_score:
        return 0

    # If margin is met, don't roll
    if noroll_gain >= margin:
        return 0

    return num_roll
    # END Question 10


##########################
# Command Line Interface #
##########################


# Note: Functions in this section do not need to be changed. They use features
#       of Python not yet covered in the course.


@main
def run(*args):
    """Read in the command-line argument and calls corresponding functions.

    This function uses Python syntax/techniques not yet covered in this course.
    """
    import argparse
    parser = argparse.ArgumentParser(description="Play Hog")
    parser.add_argument('--run_experiments', '-r', action='store_true',
                        help='Runs strategy experiments')

    args = parser.parse_args()

    if args.run_experiments:
        run_experiments()
