2
\$\begingroup\$

Took me a couple days and I had to make 2 versions. Version 1.2 is better looking and more readable, with better functionality. No where near optimized though, as I know nothing about that. Save and Load Functionality won't work on your OS unless you change the file paths in the code, but those took me a while to get working on mine.

Always looking for ways to make my code better.

#v1.2 Final draft. functional shell Rock-Paper-Scissors Game

#- Rock-Paper-Scissors Game
#- Beginner Project
#- Functional file save
#- Functional file import
#-----------------------------
#         TO DO LIST
#-----------------------------
#- Web scrape for win and loss
#    quotes
#- Gui (Big MAYBE, don't know
#    how to yet)
#- File save and load to work on every os


#imports
import random
import os
from time import sleep

#dictionary holding game choices
rps_list = {
    'rock': 'rock',
    'paper': 'paper',
    'scissors': 'scissors'
}


#saves player data to file and names file using data input
def player_file_save(player):
    saving = True
    #loop in case of invalid input
    while saving:
        #checks if file doesn't exists
        if not os.path.exists(f'/home/daniel/Documents/Coding/{player.data[0]}_save'):
            #creates new files with player_dataw
            with open(f'{player.data[0]}_save', 'w+') as file:
                file.write(f'Username: {player.data[0]}\nDifficulty: {player.data[1]}\nWin Streak: {player.data[2]}')
                print('File Saved!')
                break
        else:
            #if player wants to overwrite file
            continue_save = input('\nThis player save file already exist. Would you like to overwrite[Yes or No]? ')
            #if answer isn't yes
            if not (continue_save.lower() == 'yes' or continue_save.lower() == 'y'):
                #if answer isn't no
                if not(continue_save.lower() == 'no' or continue_save.lower() == 'n'):
                    print('\nNot a valid option!')
                    continue
                #if answer is no breaks out of loop
                else:
                    print('\nFile Save Aborted!')
                    break
            #if answer is yes, overwrites previously existing file
            else:
                with open(f'{player.data[0]}_save', 'w+') as file:
                    file.write(f'Username: {player.data[0]}\nDifficulty: {player.data[1]}\nWin Streak: {player.data[2]}')
                    print('\nFile Saved')
                    break

#loads player file and stores it in player.data
def file_load(player, player_name = 'Default'):
    #if file doesn't exists
    if not (os.path.exists(f'/home/daniel/Documents/Coding/{player_name}_save')):
        print("\nThis save file doesn't exist")
        return None
    #checks if the file exists
    else:
        #writes every stat to the file
        with open(f'{player_name}_save', 'r+') as file:
            #writes username
            username = ''.join(file.readline()).strip('\n')[10::]
            player.modify_stats('Username', username, 'edit')
            
            #writes difficulty
            difficulty = int(''.join(file.readline()).strip('\n')[12::])
            player.modify_stats('Difficulty', difficulty, 'edit')
            
            #writes win_streak
            win_streak = int(''.join(file.readline()).strip('\n')[11::])
            player.modify_stats('Win_streak', win_streak, 'edit')
        
            print('\nSave Data Loaded!')

#player class. Each instance uses player_data index variables during initialization
class Player():

    #initialization of stats for player
    def __init__(self, player_data):
        self.username = player_data[0]
        self.difficulty = player_data[1]
        self.win_streak = player_data[2]
        self.data = [self.username, self.difficulty, self.win_streak]

    #replace player statistics
    def modify_stats(self, stat, value, mode):
        #if mode is invalid
        if not (mode == 'edit'):
            if not (mode == 'tweak'):
                pass
            else:
                #if stat isn't difficulty/win_streak
                if not(stat == 'Win_streak'):
                    pass
                #if stat is win_streak
                else:
                    self.win_streak += value
                
                #set self.data to updated values
                self.data = [self.username, self.difficulty, self.win_streak]
        else: 
            #if stat isn't username/difficulty/win_streak
            if not (stat == 'Username'):
                if not(stat == 'Difficulty'):
                    if not(stat == 'Win_streak'):
                        pass
                    #if stat is win_streak
                    else:
                            self.win_streak = value
                #if stat is difficulty
                else:
                    self.difficulty = value
            #if stat is username
            else:
                self.username = value
            
            #set self.data to updated values
            self.data = [self.username, self.difficulty, self.win_streak]

    #returns player choice
    def play(self):
        playing = True
        
        while playing:
            choice = input('[Rock:Paper:Scissors] Which will you play?')
            if not(choice.lower() == 'rock' or choice.lower() == 'r'):
                if not(choice.lower() == 'paper' or choice.lower() == 'p'):
                    if not(choice.lower() == 'scissors' or choice.lower() == 's'):
                        print("This isn't a valid option")
                        continue
                    else:
                        print('\nYOU HAVE CHOSEN SCISSORS!')
                        return rps_list['scissors']
                else:
                    print('\nYOU HAVE CHOSEN PAPER!')
                    return rps_list['paper']
            else:
                print('\nYOU HAVE CHOSEN ROCK!')
                return rps_list['rock']

    def __str__(self):
        return f'\nUsername: {self.username}\nDifficulty: {self.difficulty}\nWin Steak: {self.win_streak}\n'

#returns bot choice 
def bot_play(player, player_choice, choice = rps_list):

    num = random.randint(1,3)
    #if player difficulty is 1, player has highest chance of winning
    if player.data[1] == 1:
        if num == 1:
            return choice['rock']
        elif num == 2:
            return choice['paper']
        else:
            return choice['scissors']
    #if player difficulty is 2, player's chance of winning is 2/3
    elif player.data[1] == 2:
        if not(num == 1 or num == 3):
            if player_choice.lower() == 'rock' or player_choice.lower() == 'r':
                return choice['paper']
            elif player_choice.lower() == 'paper' or player_choice.lower() == 'p':
                return choice['scissors']
            elif player_choice.lower() == 'scissors' or player_choice.lower() == 's':
                return choice['rock']
        else:
            if num == 3:
                return choice['rock']
            elif num == 1:
                return choice['paper']
            elif num == 2:
                return choice['scissors']
    #if player difficulty is 3, player's chance of winning is 1/3
    elif player.data[1] == 3:
        if not(num == 1 or num == 3):
            if num == 1:
                return choice['rock']
            elif num == 3:
                return choice['paper']
            elif num == 2:
                return choice['scissors']
        else:
            if player_choice.lower() == 'rock' or player_choice.lower() == 'r':
                return choice['paper']
            elif player_choice.lower() == 'paper' or player_choice.lower() == 'p':
                return choice['scissors']
            elif player_choice.lower() == 'scissors' or player_choice.lower() == 's':
                return choice['rock']
    #NO CHANCE OF WINNING. HAHAHAHHAAH. Troll setting if you want to reset your win_streak built up by sheer luck
    else:
        if player_choice.lower() == 'rock' or player_choice.lower() == 'r':
            return choice['paper']
        elif player_choice.lower() == 'paper' or player_choice.lower() == 'p':
            return choice['scissors']
        elif player_choice.lower() == 'scissors' or player_choice.lower() == 's':
            return choice['rock']

init = True
while init:
    player = Player(['Default', 1, 1])
    initialization = input('\nNew Game[Yes or No]: ').lower()
    if not(initialization == 'yes' or initialization == 'y'):
        if not(initialization == 'no' or initialization == 'n'):
            print('\nNot an option')
        else:
            username = input('\nWhose save data are you loading? ')
            if os.path.exists(f'/home/daniel/Documents/Coding/{username}_save'):
                file_load(player, username)
                init = False        
    else:
        username = input('\nWhat is your username? ')
        player = Player([username, 1, 0])
        print('\nNew game started!')
        init = False

print(player)

running = True
while running:
    mode = input('\n|| Play || Save || Load || Stats || Close ||: ').lower()

    if not(mode == 'play'):
        if not(mode == 'save'):
            if not(mode == 'load'):
                if not(mode == 'stats'):
                    if not(mode == 'close'):
                        #if option is not valid
                        print('\nNot an option!')
                    else:
                        #if mode is close
                        last_check = input('\nDo you want to save your file before exiting[Yes or NO or Cancel]? ').lower()
                        closing = True
                        while closing:
                            if not(last_check == 'yes' or last_check == 'y'):
                                if not(last_check == 'no' or last_check == 'no'):
                                    if not(last_check == 'cancel' or last_check == 'c'):
                                        print('\nNot an option!')
                                    else:
                                        closing = False
                                else:
                                    exit()
                            else:
                                player_file_save(player)
                                exit()
                else:
                    #if mode is stats
                    print(player)

                    stat_change_check = True
                    while stat_change_check:
                        stat_change_confirmation = input('\nDo you want to change difficulty[Yes or No]? ').lower()
                        if not(stat_change_confirmation == 'yes' or stat_change_confirmation == 'y'):
                            if not(stat_change_confirmation == 'no' or stat_change_confirmation == 'n'):
                                print('\nNot a valid option')
                            else:
                                stat_change_check = False
                        else:
                            diff_check = True
                            while diff_check:
                                diff_change = int(input('\nWhat difficulty would you like to chnge to[1,2,3,4]: '))
                                if diff_change != 1 and diff_change != 2 and diff_change != 3 and diff_change != 4:
                                    print('\nNot an option')
                                else:
                                    player.modify_stats('Difficulty', diff_change, 'edit')
                                    diff_check = False
                                    stat_change_check = False
            else:
                #if mode is load
                loading = True
                while loading:
                    input_confirm = input('\nDo you want to load another game[Yes or No]? ').lower()
                    if not(input_confirm == 'yes' or input_confirm == 'y'):
                        if not(input_confirm == 'no' or input_confirm == 'n'):
                            print('\nNot an option!')
                        else:
                            loading = False
                    else:
                        username = input('\nWhat was your username? ')
                        file_load(player, username)
                        loading = False

        else:
            #if mode is save
            player_file_save(player)
    else:
        #if mode is play
        playing = True
        while playing:
            
            player_choice = player.play()
            bot_choice = bot_play(player, player_choice)

            if not(bot_choice == rps_list['paper']):
                if not(bot_choice == rps_list['rock']):
                    #if bot plays scissors
                    print('BOT HAS PLAYED SCISSORS')
                    if not(player_choice == rps_list['paper']):
                        if not(player_choice == rps_list['rock']):
                            #if you played scissors
                            print('\nYou have tied with the bot! Win streak remains')
                        else:
                            #if you played rock
                            print('\nYou have won against the bot! Win streak has gone up!')
                            player.modify_stats('Win_streak', 1, 'tweak')
                    else:
                        #if you played paper
                        print('\nYou have lost against the bot! Win streak reset')
                        player.modify_stats('Win_streak', 0, 'edit')
                else:
                    #if bot played rock
                    print('BOT HAS PLAYED ROCK')
                    if not(player_choice == rps_list['paper']):
                        if not(player_choice == rps_list['rock']):
                            #if you played scissors
                            print('\nYou have lost against the bot! Win streak reset')
                            player.modify_stats('Win_streak', 0, 'edit')
                        else:
                            #if you played rock
                            print('\nYou have tied with the bot! Win streak remains')
                    else:
                        #if you played paper
                        print('\nYou have won against the bot! Win streak has gone up!')
                        player.modify_stats('Win_streak', 1, 'tweak')
            else:
                #if bot played paper
                print('BOT HAS PLAYED PAPER')
                if not(player_choice == rps_list['paper']):
                    if not(player_choice == rps_list['rock']):
                        #if you played scissors
                        print('\nYou have won against the bot! Win streak has gone up!')
                        player.modify_stats('Win_streak', 1, 'tweak')
                    else:
                        #if you played rock
                        print('\nYou have lost against the bot! Win streak reset')
                        player.modify_stats('Win_streak', 0, 'edit')
                else:
                    #if you played paper
                    print('\nYou have tied with the bot! Win streak remains')
            
            #checks if you want to keep playing
            cont_check = True
            while cont_check:
                cont = input('\nWould you like to keep playing[Yes or No]? ')
                if not(cont.lower() == 'yes' or cont.lower() == 'y'):
                    if not(cont.lower() == 'no' or cont.lower() == 'n'):
                        print('\nNot a valid option')
                    else:
                        cont_check = False
                        playing = False
                else:
                    cont_check = False
\$\endgroup\$
4
  • \$\begingroup\$ Why Python version are you using? There's a much better method for your mode checking. \$\endgroup\$
    – Mast
    Feb 15, 2023 at 9:23
  • \$\begingroup\$ I don't know the rules of this SE page, but want to say that you should use docstrings for documenting your functions. See realpython.com/documenting-python-code/… . \$\endgroup\$
    – til_b
    Feb 15, 2023 at 9:49
  • \$\begingroup\$ I would suggest not creating an if statement for every possible combination. Your code becomes super long... If you convert rock,paper,scissors to 0,1,2 you can already simplify your code a lot. \$\endgroup\$
    – Kokodoko
    Feb 15, 2023 at 10:47
  • \$\begingroup\$ @Mast I'm using the latest Python version. \$\endgroup\$
    – Beginner
    Feb 15, 2023 at 13:30

3 Answers 3

2
\$\begingroup\$

Input Checking

I'm going to comment on this first, since the bulk of your code is basically checking user input, over and over again. The simplest way to remove any doubt, and any specific checking, is to only take the first letter of the input. Because no other letter matters after that.

choice = input("[Rock:Papyer:Scissors] Which will you play? ")[0].upper()

I use .upper() here, and you will see why later in this answer.

File Saving

When you save a file, you save it to the current working directory. Although when you try to load the file, you check a very specific path when doing so. This is not required.

When saving a file with just a name and no specified file path, Python automatically used the current working directory. Thus, eliminating the "need" to specify an exact file path for each system.

def player_file_save(player: object) -> None:
    if os.path.exists(f"{player.data[0]}_save"):
        continue_save = ""
        while continue_save not in ["Y", "N"]:
            continue_save = input("\nThis player save file already exists Overwrite? [Yes/No]")[0].upper()
        if continue_save == "N":
            print("\nFile Save Aborted!")
            return
    with open(f"{player.data[0]}_save", "w") as file:
        for attribute in player.data:
            file.write(f"{attribute}\n")
        print("\nFile Saved!")

Also with this function: You only need to write file saving code once, not twice. The first part of the functions only checks if the user wants to overwrite the file, if it exists. If not, it returns from the function. If it doesn't exist, or the user wants to overwrite, then we save. We don't need to check specifically if the file doesn't exist or the user wants to overwrite because we're doing the exact same thing regardless of which it is.

File Loading

You're making this process a lot harder on yourself. Instead of having descriptions of each attribute in the save file, simply just load the data in the same order as you saved it.

def file_load(player: object, player_name: str="Default") -> None:
    if os.path.exists(f"{player_name}_save"):
        with open(f"{player_name}_save", "r") as file:
            player.username = file.readline().strip('\n')
            player.difficulty = int(file.readline().strip('\n'))
            player.win_streak = int(file.readline().strip('\n'))
            print("\nSave Data Loaded!")
            return
    print("\nThis save file doesn't exist.")

You don't need ''.join() here, because .readline() returns a string; not a list.

Player Class

.modify_stats(), quite frankly, is a completely redundant function. You're basically forcing yourself to use a function that replaces pythons member access functionality altogether. Here are some alternatives to the function you wrote:

player.modify_stats('Win_streak', 1, 'tweak')
player.modify_stats('Win_streak', 0, 'edit')
player.modify_stats('Difficulty', diff_change, 'edit')

can, and probably should, be

player.win_streak += 1
player.win_streak = 0
player.difficulty = diff_change

There's no need to create a function where you specify how you want a value to be changed; you can do that already in a lot shorter amount of code.

rps_list

Just a quick note about this: When using only the first letter in the input, it allows you to index this list correctly.

rps_list = {
    'R': 'rock',
    'P': 'paper',
    'S': 'scissors'
}

Also, confusing name, as rps_list implies this variable is a list, which it isn't.

Player.play

Using the above change, we can simply sit in a loop until the user inputs a correct choice.

def play(self):
    choice = ""
    while choice not in ["R", "P", "S"]:
        choice = input("[Rock:Papyer:Scissors] Which will you play? ")[0].upper()
    choice = rps_list[choice]
    print(f"YOU HAVE CHOSEN {choice.upper()}!")
    return choice

bot_play

This one was the most satisfying to fix. Instead of checking for each number, determining the difficulty, and returning a string based on that, you can write a cheat function that does half the work for you. All it does is take in the players choice, and returns whatever string beats it.

Now, if the computer isn't cheating, you still don't have to check the random number to see which string to return. Simply use random.choice() to pick a random options from the values of the rps_list dictionary.

def bot_play(player: object, player_choice: str) -> str:

    def cheat(player_choice: str) -> str:
        if player_choice == 'R':
            return 'paper'
        if player_choice == 'S':
            return 'rock'
        if player_choice == 'P':
            return 'scissors'

    player_choice = player_choice[0].upper()
    outcomes = list(rps_list.values())

    number = random.randint(1, 3)
    # No Cheating
    if player.difficulty == 1:
        return random.choice(outcomes)
    # 1/3 Chance of Cheating
    if player.difficulty == 2:
        if number == 2:
            return cheat(player_choice)
        return random.choice(outcomes)
    # 2/3 Chance of Cheating
    if player.difficulty == 3:
        if number == 1 or number == 2:
            return cheat(player_choice)
        return random.choice(outcomes)
    # 3/3 Chance of Cheating
    if player.difficulty == 4:
        return cheat(player_choice)

Also, you can see here I have nested functions. Here is a Stack Overflow discussion that has some pros and cons to using them. Here, since I'm only using the cheat function inside this function, I've opted to keep it within this scope.

\$\endgroup\$
1
  • \$\begingroup\$ The Bot part blew my mind. It was so simple, yet I couldn't think of that no matter how long I thought. I am internalizing all of this right now, thanks. \$\endgroup\$
    – Beginner
    Feb 16, 2023 at 18:34
1
\$\begingroup\$

Taking Linny's bot_play we can simplify it a bit further.

We don't need a function to return a fixed response to an input (map input to output), that is reproducing the job of a dictionary.

To determine if we need to cheat, we can test a random number against the difficulty and let that determine whether we cheat.

def bot_play(player: object, player_choice: str) -> str:

    # Dictionary to map input to response
    # Should probably be constant outside function
    # If constructed properly
    # can then also be used to determine winner
    to_beat_player = {'R': 'paper',
                      'S': 'rock',
                      'P': 'scissors'}

    player_choice = player_choice[0].upper()
    outcomes = list(to_beat_player.values())

    number = random.randint(1, 3)
    # If our difficulty is greater than the random number
    # The bot will win, 
    # Otherwise a random response is given
    # That means that lower difficulty numbers have lower
    # chance of cheating 1 -> Never cheat, 4 -> Always cheat
    if player.difficulty > number:
        return to_beat_player[player_choice]
    return random.choice(outcomes)
\$\endgroup\$
2
  • 1
    \$\begingroup\$ That dict is a perfect replacement for my function; and the logic is a lot simpler. \$\endgroup\$
    – Linny
    Feb 17, 2023 at 1:10
  • \$\begingroup\$ Wow could be this short huh. The more you learn. \$\endgroup\$
    – Beginner
    Mar 11, 2023 at 2:04
1
\$\begingroup\$

Loooong Methods

The longest method isn't even a method, it's the code at the bottom that's in global space. While this is okay for scripts, it's bad practice for anything more complex than a simple script. It means you have a lot of global variables, which we generally don't like.

I'd argue that python methods should be 3-5 lines long. This may seem short, but here's why:

  • short methods (usually) do only one thing when they're short (if you want to know why that's important: SRP) and therefore increase comprehension and testability.
  • short methods paired with good naming increase code comprehension by abstracting concrete implementations to their intents. Therefore we do not need to look into a method to see how it works, we can just look at its name. We only need to look into the method if we also need to know how a method is working.

There's some other benefits but these are the main ones I think about.

Deep nestings

The deeper the nesting, the more context we have to keep in mind. This makes code comprehension harder, therefore we want to keep nestings as low as possible. I'd argue that 2 nestings are the maximum per method - it's enough for a loop and a conditional. Everything else can be extracted to its own method until we only have two max nestings.

However, when we have a lot of conditional nestings, we may need to look at whether we can refactor that logic into a pattern like the strategy pattern. A good example of how this works is Sandi Metz' Talk about refactoring the Gilded Rose Kata.

Misc

naming any version 'final' is pretty daring.

I want to learn more!

Here's a link to a summary about the problems (and solutions) I see most often with code.

\$\endgroup\$
1
  • \$\begingroup\$ My bad for the final part. A better word would be revised as I had simply redid the while code and made it as better as I could. Thank you for the review, I will look into the better logic. \$\endgroup\$
    – Beginner
    Feb 22, 2023 at 16:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.