### pig.py (plain text)

```"""The Game of Pig"""

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 -- 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 number of points the player scores 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 point 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 -- 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 number of points the player scores 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(plan, dice=make_fair_die(), who='Someone', comments=False):
"""Simulate a single turn and return the points scored for the whole turn.

Important: The d function should be called once, **and only once**, for
every action taken!  Testing depends upon this fact.

Arguments:
plan -- 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
"""
score_for_turn = 0  # Points scored in the whole turn
"*** YOUR CODE HERE ***"
return score_for_turn

def take_turn_test():
"""Test the take_turn function using deterministic test dice."""
plan = make_roll_until_plan(10)  # plan is a function (see problem 2)
"*** YOUR CODE HERE ***"
print(take_turn(plan))  # Not deterministic

# Commentating

def commentate(action, outcome, score_for_turn, 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
score_for_turn -- 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
"""
print(draw_number(outcome))
print(who, describe_action(action))
if over:
print(who, 'scored', score_for_turn, '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.

>>> 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_plan(turn_goal=20):
"""Return a plan to roll until turn total is at least turn_goal."""
def plan(turn):
if turn >= turn_goal:
return hold
else:
return roll
return plan

def make_roll_until_strategy(turn_goal):
"""Return a strategy to always adopt a plan to roll until turn_goal.

A strategy is a function that takes two game scores as arguments and
returns a plan (which is a function from turn totals to actions).
"""
"*** YOUR CODE HERE ***"

def make_roll_until_strategy_test():
"""Test that make_roll_until_strategy gives a strategy that returns correct
roll-until plans."""
strategy = make_roll_until_strategy(15)
plan = strategy(0, 0)
assert plan(14) == roll, 'Should have returned roll'
assert plan(15) == hold, 'Should have returned hold'
assert plan(16) == hold, 'Should have returned hold'

# Experiments (Phase 2)

def average_value(fn, num_samples):
"""Compute the average value returned by fn over num_samples trials.

>>> d = make_test_die(1, 3, 5, 7)
>>> average_value(d, 100)
4.0
"""
"*** YOUR CODE HERE ***"

def averaged(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 = averaged(die)
>>> avg_die()
4.0
>>> avg_turn = averaged(take_turn)
>>> avg_turn(make_roll_until_plan(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 - averaged(play)(strategy, baseline)
as_second = averaged(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 plan.

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 interactive_strategy(score, opponent_score):
"""Prints total game scores and returns an interactive plan.

Note: this function uses Python syntax not yet covered in the course.
"""
print('You have', score, 'and they have', opponent_score, 'total score')
def plan(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 plan

@main
def run():
take_turn_test()

# Uncomment the next line to play an interactive game
# play(interactive_strategy, make_roll_until_strategy(20))

# Uncomment the next line to test make_roll_until_strategy
# make_roll_until_strategy_test()

run_strategy_experiments()

```