pig.py (plain text)


"""
The Game of Pig
Name:
Login:
TA:
Section:
"""

from dice import make_fair_die, make_test_die
from ucb import main, trace, log_current_line, interact

goal = 100  # The goal of pig is always to score 100 points.

# Taking turns

def roll(turn_total, outcome):
    """Performs the roll action, which adds outcome to turn_total, or loses the
    turn on outcome == 1.

    Arguments:
    turn_total -- number of points accumulated by the player so far during the turn
    outcome -- the outcome of the roll (the number generated by the die)

    Returns three values in order:
    - the player's turn score after the roll
      Note: If the turn is not over after this roll, this return value is 0.
            No points are scored until the end of the turn.
    - the player turn total after the roll
    - a boolean; whether or not the player's turn is over
    
    >>> roll(7, 3)
    (0, 10, False)
    >>> roll(99, 1)
    (1, 0, True)
    """
    "*** YOUR CODE HERE ***"

def hold(turn_total, outcome):
    """Performs the hold action, which adds turn_total to the player's score.

    Arguments:
    turn_total -- number of points accumulated by the player so far during the turn
    outcome -- the outcome of the roll, ie. the number generated by the die

    Returns three values in order:
    - the player's turn score after holding
    - the player turn total after the roll (always 0)
    - a boolean; whether or not the player's turn is over
    
    >>> hold(99, 1)
    (99, 0, True)
    """
    "*** YOUR CODE HERE ***"

def take_turn(tactic, dice=make_fair_die(), who='Someone', comments=False):
    """Simulate a single turn and return the points scored for the whole turn.

    Important: The dice function should be called once, **and only once**, for
               every action taken!  Testing depends upon this fact.
    
    Arguments:
    tactic -- a function that takes the turn total and returns an action function
    dice -- a function that takes no args and returns an integer outcome.
            Note: dice is non-pure!  Call it exactly once per action.
    who -- name of the current player
    comments -- a boolean; whether commentary is enabled
    """
    turn_score = 0  # Points scored in the whole turn
    "*** YOUR CODE HERE ***"
    return turn_score

def take_turn_test():
    """Test the take_turn function using test dice."""
    tactic = make_roll_until_tactic(10)  # tactic is a higher-order
                                         # function (see problem 2)
    print('----- Testing take_turn() with deterministic test dice -----')
    "*** YOUR CODE HERE ***"
    print('\nTests passed')   # Will only print if all assert statements pass
    print('\n----- Value of take_turn() with non-deterministic test dice -----')
    print(take_turn(tactic))  # Not deterministic

# Commentating

def commentate(action, outcome, turn_score, turn_total, over, who):
    """Print descriptive comments about a game event.
    
    action -- the action function chosen by the current player
    outcome -- the outcome of the die roll
    turn_score -- the points scored in this turn by the current player
    turn_total -- the current turn total
    over -- a boolean that indicates whether the turn is over
    who -- the name of the current player 
    """
    if action is roll:
        print(draw_number(outcome))
    else:
        print()
    print(who, describe_action(action))
    if over:
        print(who, 'scored', turn_score, 'point(s) on this turn.')
    else:
        print(who, 'now has a turn total of', turn_total, 'point(s).')

def describe_action(action):
    """Generate a string that describes an action.

    action -- a function, which should be either hold or roll    

    If action is neither the hold nor roll function, the description should
    announce that cheating has occurred.

    >>> describe_action(roll)
    'chose to roll.'
    >>> describe_action(hold)
    'decided to hold.'
    >>> describe_action(commentate)
    'took an illegal action!'
    """
    "*** YOUR CODE HERE ***"
    return 'did something...'
 
def draw_number(n, dot='*'):
    """Return an ascii art representation of rolling the number n.
    If a number has multiple possible representations (such as 2 and 
    3), any valid representation is acceptable.

    >>> print(draw_number(5))
     -------
    | *   * |
    |   *   |
    | *   * |
     -------
    """
    "*** YOUR CODE HERE ***"
    return ''

def draw_die(c, f, b, s, dot):
    """Return an ascii art representation of a die.

    c, f, b, & s are boolean arguments. This function returns a multi-line
    string of the following form, where the letters in the diagram are either
    filled if the corresponding argument is true, or empty if it is false.
    
     -------
    | b   f |
    | s c s |
    | f   b |
     -------    

    Note: The sides with 2 and 3 dots have 2 possible depictions due to
          rotation. Either representation is acceptable. 

    Note: This function uses Python syntax not yet covered in the course.
    
    c, f, b, s -- booleans; whether to place dots in corresponding positions
    dot        -- A length-one string to use for a dot
    """
    border = ' -------'
    def draw(b): 
        return dot if b else ' '
    c, f, b, s = map(draw, [c, f, b, s])
    top = ' '.join(['|', b, ' ', f, '|'])
    middle = ' '.join(['|', s, c, s, '|'])
    bottom = ' '.join(['|', f, ' ', b, '|'])
    return '\n'.join([border, top, middle, bottom, border])


# Game simulator

def play(strategy, opponent_strategy):
    """Simulate a game and return 0 if the first player wins and 1 otherwise.
    
    strategy -- The strategy function for the first player (who plays first)
    opponent_strategy -- The strategy function for the second player
    """
    who = 0 # Which player is about to take a turn, 0 (first) or 1 (second)
    "*** YOUR CODE HERE ***"
    return who

def other(who):
    """Return the other player, for players numbered 0 and 1.
    
    >>> other(0)
    1
    >>> other(1)
    0
    """
    return (who + 1) % 2


# Basic Strategies

def make_roll_until_tactic(turn_goal=20):
    """Return a tactic to roll until turn total is at least turn_goal."""
    def tactic(turn_total):
        if turn_total >= turn_goal:
            return hold
        else:
            return roll
    return tactic

def make_roll_until_strategy(turn_goal):
    """Return a strategy that returns a tactic that will roll until
    it reaches turn_goal.
    
    A strategy is a function that takes two game scores as arguments
    (the player's score, and the opponent's score), and returns a 
    tactic (which is a function from turn totals to actions).
    
    Note: The player's turn_total need not exceed the turn_goal in 
          order to hold. If the player's turn_total is equal to
          the turn_goal, then your tactic should hold.

    >>> strategy = make_roll_until_strategy(15)
    >>> tactic = strategy(0, 0)
    >>> tactic(14) == roll
    True
    >>> tactic(15) == hold
    True
    >>> tactic(16) == hold
    True
    """
    "*** YOUR CODE HERE ***"

# Experiments (Phase 2)

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

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

    >>> die = make_test_die(3, 1, 5, 7)
    >>> avg_die = make_average(die)
    >>> avg_die()
    4.0
    >>> avg_turn = make_average(take_turn)
    >>> avg_turn(make_roll_until_tactic(4), die, 'The player', False)
    3.0

    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 1.
    - In the other, the player rolls a 5 (then holds on the 7), scoring 5.
    Thus, the average value is 3.0

    Note: If this last test is called with comments=True in take_turn, the
    doctests will fail because of the extra output.
    """
    "*** YOUR CODE HERE ***"

def compare_strategies(strategy, baseline=make_roll_until_strategy(20)):
    """Return the average win rate (out of 1) of strategy against baseline."""
    as_first = 1 - make_average(play)(strategy, baseline)
    as_second = make_average(play)(baseline, strategy)
    return (as_first + as_second) / 2  # Average the two results

def eval_strategy_range(make_strategy, lower_bound, upper_bound):
    """Return the best integer argument value for make_strategy to use against
    the roll-until-20 baseline, between lower_bound and upper_bound (inclusive).

    make_strategy -- A one-argument function that returns a strategy.
    lower_bound -- lower bound of the evaluation range
    upper_bound -- upper bound of the evaluation range
    """
    best_value, best_win_rate = 0, 0
    value = lower_bound
    while value <= upper_bound:
        strategy = make_strategy(value)
        win_rate = compare_strategies(strategy)
        print(value, 'win rate against the baseline:', win_rate) 
        if win_rate > best_win_rate:
            best_win_rate, best_value = win_rate, value
        value += 1
    return best_value

def run_strategy_experiments():
    """Run a series of strategy experiments and report results."""
    "*** YOUR CODE HERE ***"

def make_die_specific_strategy(four_side_goal, six_side_goal=20):
    """Return a strategy that returns a die-specific roll-until tactic.
    
    four_side_goal -- the roll-until goal whenever the turn uses a 4-sided die
    six_side_goal -- the roll-until goal whenever the turn uses a 6-sided die

    """
    "*** YOUR CODE HERE ***"

def make_pride_strategy(margin, turn_goal=20):
    """Return a strategy that wants to finish a turn winning by at least margin.

    margin -- the size of the lead that the player requires
    turn_goal -- the minimum roll-until turn goal, even when winning
    """
    "*** YOUR CODE HERE ***"
    

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

    *** YOUR DESCRIPTION HERE ***
    """
    "*** YOUR CODE HERE ***"
    

def final_strategy_test(win_rate):
    """Tests how many times final_strategy returns higher than win_rate.
    
    This function runs compare_strategies 100 times and counts how many times 
    final_strategy wins less than the desired win_rate, and how many times 
    it wins over the desired win_rate. It also computes the average win rate.
    
    win_rate -- the desired win rate
    """
    above = below = total = 0
    for i in range(100):
        score = compare_strategies(final_strategy)
        total += score
        if score < win_rate:
            below += 1
        else:
            above += 1
    print('----- Testing final_strategy -----')
    print('Number of times above', win_rate, ':', above)
    print('Number of times below', win_rate, ':', below)
    print('Average win rate:', total / 100)
    if above > below:
        print('Good job!')

#################################################################
##   You don't need to look at this unless you really want to  ##
#################################################################

def interactive_strategy(score, opponent_score):
    """Prints total game scores and returns an interactive tactic.
    
    Note: this function uses Python syntax not yet covered in the course.
    """
    print('You have', score, 'and they have', opponent_score, 'total score')
    def tactic(turn):
        if turn > 0:
            print('You now have a turn total of', turn, 'points')
        while True:
            response = input('(R)oll or (H)old?')
            if response.lower()[0] == 'r':
                return roll
            elif response.lower()[0] == 'h':
                return hold
            print('Huh?')
    return tactic

def setup_args():
    """Reads in the command-line argument, and chooses an appropriate
    action.

    Note: this function uses Python syntax/techniques not yet covered
          in this course. You do not need to understand how this works.
    """
    import argparse
    fns = (('take_turn_test', take_turn_test),
           ('play', lambda : play(interactive_strategy, 
                                  make_roll_until_strategy(20))),
           ('run_strat_exps', run_strategy_experiments),
           ('final_strat_test', lambda : final_strategy_test(0.6)))

    description = "Run your project code in a specific manner. For \
instance, to run the take_turn_test code, do: 'python3 pig.py \
--take_turn_test'. Defaults to {0} if no argument is given.".format(fns[0][0])

    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('--take_turn_test', '-t', action='store_true',
                        dest='take_turn_test')
    parser.add_argument('--play', '-p', action='store_true',
                        dest='play')
    parser.add_argument('--run_strat_exps', '-r', action='store_true',
                        dest='run_strat_exps')
    parser.add_argument('--final_strat_test', '-f', action='store_true',
                        dest='final_strat_test')
    parser.add_argument('--all_tests', '-a', action='store_true',
                        dest='all_tests')
    args = parser.parse_args()
    if args.all_tests:
        return (take_turn_test, run_strategy_experiments,
                lambda : final_strategy_test(0.6))
    return_fns = []
    for name, fn in fns:
        if getattr(args, name):
            return_fns.append(fn)
    return return_fns

@main
def run(*args):
    fns = setup_args()
    for fn in fns:
        fn()