This is actually the hardest problem to me among all 5 final projects of the AI course.
This probably is cheating, but I learn the idea for the solution from Milk's solution.
I don't who implement the abby
player, but it's really hard to beat.
Time spent here: 5h
Ignore this. This is failed attempt.
I was really tempted to look into the game driver code and learn about how other players is implemented, but I didn't
Since we're not supposed to look in the opponent implementations, let's think about how would FCC implement them.
n
times.R
, the opponent
might "think" that we want to play P
next, so he play S
. For this
case, we should use the same with opponent previous move. I don't think
FCC would implement such logic, as it's too simple, no randomn
repeated R
, then pick
randomly next move and repeat them n
times.For each strategy our opponent can employ, we should have a counter strategy. We will need to use random move for a while, until we have enougb data to see his strategy.
However, since this is just a game, I don't think we should try to be perfect and implement all ideas we have. We will just add enough code until we beat all the bots.
That means the code in the Solution section below doesn't grow linearly.
I changed my mind. Need to look at opponent code to learn their strategy and counter it
For this challenge, you will create a program to play Rock, Paper, Scissors. A program that picks at random will usually win 50% of the time. To pass this challenge your program must play matches against four different bots, winning at least 60% of the games in each match.
In the file RPS.py
you are provided with a function called player
. The
function takes an argument that is a string describing the last move of the
opponent ("R", "P", or "S"). The function should return a string
representing the next move for it to play ("R", "P", or "S").
A player function will receive an empty string as an argument for the first game in a match since there is no previous play.
The file RPS.py
shows an example function that you will need to update.
The example function is defined with two arguments
(player(prev_play, opponent_history = [])
). The function is never called
with a second argument so that one is completely optional. The reason why
the example function contains a second argument (opponent_history = []
) is
because that is the only way to save state between consecutive calls of the
player
function. You only need the opponent_history
argument if you want
to keep track of the opponent_history.
Hint: To defeat all four opponents, your program may need to have multiple strategies that change depending on the plays of the opponent.
Do not modify RPS_game.py
. Write all your code in RPS.py
. For
development, you can use main.py
to test your code.
main.py
imports the game function and bots from RPS_game.py
.
To test your code, play a game with the play
function. The play
function
takes four arguments:
True
to see
these messages.play(player1, player2, num_games[, verbose])
For example, here is how you would call the function if you want player
and quincy
to play 1000 games against each other and you want to see the
results of each game:
play(player, quincy, 1000, verbose=True)
This is provided by FCC.
import random
from collections import Counter
def play(player1, player2, num_games, verbose=False):
p1_prev_play = ""
p2_prev_play = ""
results = {"p1": 0, "p2": 0, "tie": 0}
for _ in range(num_games):
p1_play = player1(p2_prev_play)
p2_play = player2(p1_prev_play)
if p1_play == p2_play:
results["tie"] += 1
winner = "Tie."
elif (
(p1_play == "P" and p2_play == "R")
or (p1_play == "R" and p2_play == "S")
or (p1_play == "S" and p2_play == "P")
):
results["p1"] += 1
winner = "Player 1 wins."
elif (
p2_play == "P"
and p1_play == "R"
or p2_play == "R"
and p1_play == "S"
or p2_play == "S"
and p1_play == "P"
):
results["p2"] += 1
winner = "Player 2 wins."
if verbose:
print("Player 1:", p1_play, "| Player 2:", p2_play)
print(winner)
print()
p1_prev_play = p1_play
p2_prev_play = p2_play
games_won = results["p2"] + results["p1"]
if games_won == 0:
win_rate = 0
else:
win_rate = results["p1"] / games_won * 100
print("Final results:", results)
print(f"Player 1 win rate: {win_rate}%")
return win_rate
def quincy(prev_play, counter=[0]):
counter[0] += 1
choices = ["R", "R", "P", "P", "S"]
return choices[counter[0] % len(choices)]
def mrugesh(prev_opponent_play, opponent_history=[]):
opponent_history.append(prev_opponent_play)
last_ten = opponent_history[-10:]
most_frequent = max(set(last_ten), key=last_ten.count)
if most_frequent == "":
most_frequent = "S"
ideal_response = {"P": "S", "R": "P", "S": "R"}
return ideal_response[most_frequent]
def kris(prev_opponent_play):
if prev_opponent_play == "":
prev_opponent_play = "R"
ideal_response = {"P": "S", "R": "P", "S": "R"}
return ideal_response[prev_opponent_play]
def abbey(
prev_opponent_play,
opponent_history=[],
play_order=[
{
"RR": 0,
"RP": 0,
"RS": 0,
"PR": 0,
"PP": 0,
"PS": 0,
"SR": 0,
"SP": 0,
"SS": 0,
}
],
):
if not prev_opponent_play:
prev_opponent_play = "R"
opponent_history.append(prev_opponent_play)
last_two = "".join(opponent_history[-2:])
if len(last_two) == 2:
play_order[0][last_two] += 1
potential_plays = [
prev_opponent_play + "R",
prev_opponent_play + "P",
prev_opponent_play + "S",
]
sub_order = {
k: play_order[0][k] for k in potential_plays if k in play_order[0]
}
prediction = max(sub_order, key=sub_order.get)[-1:]
ideal_response = {"P": "S", "R": "P", "S": "R"}
return ideal_response[prediction]
def human(prev_opponent_play):
play = ""
while play not in ["R", "P", "S"]:
play = input("[R]ock, [P]aper, [S]cissors? ")
print(play)
return play
def random_player(prev_opponent_play):
return random.choice(["R", "P", "S"])
Map between a move and its counter.
counter_move = {"R": "P", "P": "S", "S": "R"}
Our player implementation
steps = {}
# the strategy is similar to abbey, but we look backs harder than her.
# she only look back 2 steps, find most frequently pattern of all 2 moves,
#
# Other strategies:
#
# - quincy repeat 5 moves
# - kris always counter our last moves, hence, once we establed a patterns, he
# is not a problem
# - mrugresh look for our top pick in last 10 moves, hence, similar to kris,
# once we establed a pattern, we're in control.
def player(prev_play, opponent_history=[]):
if prev_play != "":
opponent_history.append(prev_play)
# Interestingly, 3 to 6 works best, as in we win more than 60%.
# If n is larger than 6, we start to get terrible result.
# I guess it's becauase we don't have enough data to predict once n get that
# larger, we only play 1000 games.
n = 5
hist = opponent_history
guess = "R"
if len(hist) > n:
pattern = join(hist[-n:])
if join(hist[-(n + 1):]) in steps.keys():
steps[join(hist[-(n + 1):])] += 1
else:
steps[join(hist[-(n + 1):])] = 1
possible = [pattern + "R", pattern + "P", pattern + "S"]
for i in possible:
if not i in steps.keys():
steps[i] = 0
predict = max(possible, key=lambda key: steps[key])
if predict[-1] == "P":
guess = "S"
if predict[-1] == "R":
guess = "P"
if predict[-1] == "S":
guess = "R"
return guess
def join(moves):
return "".join(moves)
play(player, quincy, 1000)
play(player, mrugesh, 1000)
play(player, abbey, 1000)
play(player, kris, 1000)
Final results: {'p1': 992, 'p2': 3, 'tie': 5} Player 1 win rate: 99.69849246231156% Final results: {'p1': 828, 'p2': 165, 'tie': 7} Player 1 win rate: 83.38368580060424% Final results: {'p1': 447, 'p2': 305, 'tie': 248} Player 1 win rate: 59.441489361702125% Final results: {'p1': 754, 'p2': 120, 'tie': 126} Player 1 win rate: 86.2700228832952%
86.2700228832952
import unittest
class UnitTests(unittest.TestCase):
print()
def test_player_vs_quincy(self):
print("Testing game against quincy...")
actual = play(player, quincy, 1000) >= 60
self.assertTrue(
actual,
'Expected player to defeat quincy at least 60% of the time.')
def test_player_vs_abbey(self):
print("Testing game against abbey...")
actual = play(player, abbey, 1000) >= 60
self.assertTrue(
actual,
'Expected player to defeat abbey at least 60% of the time.')
def test_player_vs_kris(self):
print("Testing game against kris...")
actual = play(player, kris, 1000) >= 60
self.assertTrue(
actual, 'Expected player to defeat kris at least 60% of the time.')
def test_player_vs_mrugesh(self):
print("Testing game against mrugesh...")
actual = play(player, mrugesh, 1000) >= 60
self.assertTrue(
actual,
'Expected player to defeat mrugesh at least 60% of the time.')
if __name__ == "__main__":
unittest.main(argv=["first-arg-is-ignored"], exit=False)
....
Testing game against abbey... Final results: {'p1': 521, 'p2': 273, 'tie': 206} Player 1 win rate: 65.61712846347606% Testing game against kris... Final results: {'p1': 999, 'p2': 1, 'tie': 0} Player 1 win rate: 99.9% Testing game against mrugesh... Final results: {'p1': 841, 'p2': 158, 'tie': 1} Player 1 win rate: 84.18418418418419% Testing game against quincy... Final results: {'p1': 998, 'p2': 1, 'tie': 1} Player 1 win rate: 99.8998998998999%
---------------------------------------------------------------------- Ran 4 tests in 0.035s OK