1
\$\begingroup\$

Today I learnt the basics of OOP. I have tried to apply them to this Rock-Paper-Scissors project. However I'm still a beginner, and so I feel my code can be improved. Are there any tips, trick or other other advice I can follow to improve the look, runtime, readability or take full advantage of OOP?

The program is a flexible rock paper scissors game, with a rating system, as long the number of possible choices are odd like rock,paper, scissors which is 3.

  1. Program will ask for your name and update in a text file named rating.txt located in the current working directory with your name and a score of 0.
  2. Program will ask for the choices from rock,paper,scissors to rock,gun,lightning,devil,dragon,water,air,paper,sponge,wolf,tree,human,snake,scissors,fire.
  3. After the following, you can do these options: !rating to get your current rating, and !exit to exit the game.
  4. you can also do any of the options you've chosen and receive an appropriate prompt on how the results went.
  5. !rating changes based on your results of matches, you are playing with a computer.
  6. after each round, the results are saved in a text file, and if you start the game again, the rating you had with a name previously also gets transfered.
  7. Oh Forgot to mention, but wins increase your rating by 100 points, a draw increases it by 50, and nothing changes when you lose.

For more information, on for example, how i decided what of the choices is superior, and inferior to a given choice, check this link

Basically this program goes as follows:

Enter your name: Tom
Hello, Tom
Enter an Odd Number of Options: rock,gun,lightning,devil,dragon,water,air,paper,sponge,wolf,tree,human,snake,scissors,fire
Okay, let's start
!rating
Your rating: 0
rock
Well done. Computer chose snake and failed
!rating
Your rating: 100
rock
Well done. Computer chose human and failed
rock
Well done. Computer chose fire and failed
rock
Sorry, but computer chose air
!rating
Your rating: 300
paper
Sorry, but computer chose sponge
wolf
Well done. Computer chose sponge and failed
!rating
Your rating: 400
!exit
Bye!
from random import choice

class Rock_Paper_Scissors:
    def __init__(self):
        self.name = self.getname()
        self.choices = self.getchoices()
        self.options = self.getoptions()
        self.current_score = 0
        self.running = True
        self.main()


    def getname(self):
        name = input('Enter your name: ')
        print(f'Hello, {name}')
        return name


    def getchoices(self):
        choices = input('Enter an Odd Number of Options: ')
        print("Okay, let's start")
        return choices


    def getoptions(self):
        choices = self.choices
        default_options = ('rock', 'paper', 'scissors')
        return choices.split(',') if choices != "" else default_options


    def SearchForPlayer(self):
        scores = open('rating.txt', 'r')
        for line in scores:
            score = line.split()
            if score[0] == self.name:
                self.current_score = int(score[1])
                self.UserFound = True
                scores.close()
                return
        self.UserFound = False


    def CreateNewUser(self):
        scores = open('rating.txt', 'a')
        print(f'\n{self.name} 0', file=scores, flush=True)
        scores.close()


    def check_choice(self, human):
        if human == '!exit':
            print('Bye!')
            self.running = False
            return True
        elif human == '!rating':
            print(f'Your rating: {self.current_score}')
            return False
        elif human in self.options:
            return True
        print('Invalid input')
        return False


    def check_result(self, human, computer):
        human_winning = []
        board = self.options*2
        each_side = int((len(board)/2)//2)
        start = int(board.index(human) + 1)
        for i in range(start, start+each_side):
            human_winning.append(board[i])

        if human == computer: # Draw
            print(f'There is a draw ({computer})')
            _round = 'Draw'
        elif computer not in human_winning: # Win
            print(f'Well done. Computer chose {computer} and failed')
            _round = 'Win'
        else: # Lose
            print(f'Sorry, but computer chose {computer}')
            _round = 'Lose'

        return _round


    def update_score(self, match_result):
        match_results = ['Win', 'Lose', 'Draw']
        points = [100, 0, 50]
        for i in range(len(match_results)):
            if match_result == match_results[i]:
                self.current_score += points[i]
                break

        if points[i] != 0:
            scores = open('rating.txt', 'r')
            list_of_scores = scores.readlines()
            for index, line in enumerate(list_of_scores):
                if line.split()[0] == self.name:
                    list_of_scores[index] = f'{self.name} {self.current_score}'
                    break

            scores = open('rating.txt', 'w')
            scores.writelines(list_of_scores)
            scores.close()


    def main(self):
        self.SearchForPlayer()
        if self.UserFound is False:
            self.CreateNewUser()

        while self.running:
            response = False
            while response is False:
                computer = choice(self.options)
                human = input()
                response = self.check_choice(human)

            if self.running and response:
                _round = self.check_result(human, computer)
                self.update_score(_round)

Rock_Paper_Scissors()
\$\endgroup\$
2
\$\begingroup\$

Well, the first thing I can see is it's good you have been exposed to the concept of OOP, but you're needing a bit more understanding, which will come with experience. Primarily, OOP is really around objects and their data or the actions which they do - for instance Dog("Spot").bark() as an action or Dog("Spot").name as a property/value.

I mention that because for Rock, Paper, Scissors - you're folding everything into one class, which is not what you should do. That would be like all the dogs in the world being inside one big 'dog' blob which barks - doesn't make sense, right? Don't worry - you'll improve with learning and experience.

Now, your code has a lot of functionality in it, and this is going to be difficult to change and verify, so we're going to go down the Test Driven Development (TDD) road. This is going to be new for you, so hold on.

We'll save your code as rockpaper.py, and create our testing file test_rockpaper.py. The first thing is to disable your class from executing straight away. So .getname() .getchoices() .getoptions() .main() and the Rock_Paper_Scissors() call will be commented out from rockpaper.py - and we'll write our first test:

import pytest
from .rockpaper import RockPaperScissors

def test_class_create():
    game = RockPaperScissors()
    assert game

Let's run the test:

C:\Users\user\Documents\dev\tests>pytest tests_rockpaper.py
========== test session starts ==========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 1 item

tests_rockpaper.py .     [100%]

========== 1 passed in 0.05s ==========

Great. Now, you might be thinking "why test creating the class? it's pointless" - and it's a good question. You're not actually testing the class itself (well, obviously you are), but what you are testing is that your testing harness/setup works properly.

Let's test the Player's name - from the .getname() function we disabled before. Now, as we're doing input, we need to fake the input. Here is the test function for that -

import io

def test_get_name(monkeypatch):
    game = RockPaperScissors()
    monkeypatch.setattr('sys.stdin', io.StringIO('myname'))
    game.getname()
    assert game.name == "myname"

and we run the test again:

C:\Users\user\Documents\dev\tests>pytest tests_rockpaper.py
========== test session starts ==========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 2 items

tests_rockpaper.py .F       [100%]

========== FAILURES ==========
_____ test_get_name _____

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x06a27c30>

    def test_get_name(monkeypatch):
        game = RockPaperScissors()
        monkeypatch.setattr('sys.stdin', io.StringIO('myname'))
        game.getname()
>       assert game.name == "myname"
E       AttributeError: 'RockPaperScissors' object has no attribute 'name'

tests_rockpaper.py:13: AttributeError
----- Captured stdout call -----
Enter your name: Hello, myname
========== short test summary info ==========
FAILED tests_rockpaper.py::test_get_name - AttributeError: 'RockPaperScissors' object has no attribute 'name'
========== 1 failed, 1 passed in 0.27s ==========

Here we can see that it recognises myname as the player's name - but we can see that the class doesn't have an attribute "name". Remember Dog("Spot").name? Each class should have properties/attributes that you can query externally, if you need to. For instance, name is a public attribute/property, but perhaps "magical_name" is only known to the player themselves and not exposed - this is called a private property/attribute. Maybe that makes sense? If not, read up on public verses private attributes.

Inside RockPaperScissors, we need to add the getter and setter for the public attribute name, as well as fix up getname(self) to make it cleaner:

class RockPaperScissors:
    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        print(f'Hello, {value}')
        self._name = value

    def getname(self):
        self.name = input('Enter your name: ')

What we have done is introduce a private variable, _name, initialise it inside the class, and set it initially to an empty string (this is important to define variables in your classes before using them). Now RockPaperScissors has a public .name property (get, and set) and the getname function/method refers to that public property.

The reason why you use the public property in your class methods/functions is that you can add validation (or other modifiers) inside the setter, before the value is placed in the private variable. This is something you can read about further. The point is not to cheat by changing the private variable self._name anywhere else in your program - except in the setter - this is because many bugs in coding come from changing values of the variables at different points in the code. If you only ever change variable data in one place, then bugs are very easy to track down.

Now, if we re-run the pytest, we see:

C:\Users\user\Documents\dev\tests>pytest tests_rockpaper.py
========= test session starts =========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 2 items

tests_rockpaper.py ..            [100%]

========== 2 passed in 0.07s ==========

Great! 2 passing tests. Now, before we can look at getchoices(), we can see there is a dependency between it and getoptions(). The default_options should be moved to the __init__ - and really for getoptions() - what we're looking at is a validation function - not a "get" at all. Let's integrate both functions into one. Firstly, let's create our test and see it fail.

def test_get_choices(monkeypatch):
    game = RockPaperScissors()
    monkeypatch.setattr('sys.stdin', io.StringIO('rock,paper,scissors'))
    game.getchoices()
    assert game.choices == ["rock","paper","scissors"]

and the failure (chopped to save space):

        monkeypatch.setattr('sys.stdin', io.StringIO('rock,paper,scissors'))
        game.getchoices()
>       assert game.choices == ["rock","paper","scissors"]
E       AttributeError: 'RockPaperScissors' object has no attribute 'choices'

tests_rockpaper.py:19: AttributeError
---------- Captured stdout call ---------
Enter an Odd Number of Options: Okay, let's start
======= short test summary info =======
FAILED tests_rockpaper.py::test_get_choices - AttributeError: 'RockPaperScissors' object has no attribute 'choices'
===== 1 failed, 2 passed in 0.29s =====

This is the same as what we had for the .name property - again with OOP, we need properties (expose public properties, and hide the private properties). The details we can see below in the final code, but you can see that we have incorporated the validation into the getter for the game choices.

So - that takes care of fixing the class __init__ issues and making those properties proper for Python. Let's move onto the operation you have for main(). Talking further about OOP, objects aren't a program. Another analogy - you could have a class Key - and create a key = Key(). Does that key automatically insert and turn? No. A person puts the key into a lock and turns. The program is the one to create the key, and the program should insert the key (or fails inserting the key) and if successful, turns the key.

It is the program which will create your RockPaperScissors object, which contains data, and contains functions which act on that internal data.

What this means for your class, is that the code in main() must be removed from the class and placed into the program.

The program, for Python, starts with a statement at the bottom (I call it the entry point). It is necessary we have this, first because it tells your readers where your code starts "here", and second - if your code employs automated documentors, like Sphinx, it instantiates all the objects in your code before performing reflection to document your code. If you are missing the entry point, your code would be executed immediately, which would break things like Sphinx. So, we have your entry point like this so far:

if __name__ == "__main__":
    game = RockPaperScissors()
    game.getname()
    game.getchoices()

and if I re-run the tests again - the tests work perfectly - because they don't interact (like Sphinx) with the entry point - because the tests only want the class RockPaperScissors() - and not the program. I hope it's starting to make a little more sense now? If not, keep at it, it should become clear in time.

So, the first few lines of your main() are:

def main(self):
    self.search_for_player()
    if self.user_found is False:
        self.create_new_user()

So we will create a test for search_for_player(). As you can see, I have "pythonised" the function and property names - we use snake_case for everything except classes. This is part of the PEP8 naming standard which Python uses. I recommend spending a little time reading about PEP8 - it will help make your Python code better. Okay, so first, as is the method in TDD, we create a failing test.

def test_search_for_player():
    game = RockPaperScissors()
    game.search_for_player()
    assert game.user_found is False

That looks very similar to your code above, right? I'm rushing a bit, but if the code is valid, it should pass easily - but it fails -

    def test_search_for_player():
        game = RockPaperScissors()
>       game.search_for_player()

tests_rockpaper.py:23:
 _ _ _

self = <tests.rockpaper.RockPaperScissors object at 0x0649dfd0>

    def search_for_player(self):
>       scores = open('rating.txt', 'r')
E       FileNotFoundError: [Errno 2] No such file or directory: 'rating.txt'

rockpaper.py:42: FileNotFoundError
======= short test summary info =======
FAILED tests_rockpaper.py::test_search_for_player - FileNotFoundError: [Errno 2] No such file or directory: 'rating.txt'
===== 1 failed, 3 passed in 0.31s =====

It looks like we've encountered a hidden bug in your code. I'm guessing you created the rating.txt file when you were first creating your program? However, as I'm running it on my system, and as the file doesn't exist, the program should naturally crash (or perhaps continue on silently?). This TDD processes allows us to test parts of the code independently without "prior states" affecting the outcome. Let's look at the function -

def search_for_player(self):
    scores = open('rating.txt', 'r')
    for line in scores:
        score = line.split()
        if score[0] == self.name:
            self.current_score = int(score[1])
            self.user_found = True
            scores.close()
            return
    self.user_found = False

There are some issues with this function that jump out at me. Firstly, you don't check for the file existing (as we just found out) on the first line of code, and the last line is creating self.user_found - we should put that variable inside the class __init__ (and you were creating a class variable inside the class instead of initialising it at the start). I can also see there will be other issues, like not validating each line of data found in the file, and the forced function exit with return, but we can leave that for an exercise for you to handle at your leisure.

So a minor change, the user_found is placed in the __init__, and we come out with:

def search_for_player(self):
    try:
        scores = open('rating.txt', 'r')
        for line in scores:
            score = line.split()
            if score[0] == self.name:
                self.current_score = int(score[1])
                self.user_found = True
                scores.close()
    except FileNotFoundError:
        pass

The test now passes, but as the code from main() says:

game.search_for_player()
if game.user_found is False:
    game.create_new_user()

Let's extend the test to:

def test_search_for_player():
    game = RockPaperScissors()
    game.search_for_player()
    assert game.user_found is False
    game.name = "myname"
    game.create_new_user()
    assert game.user_found is True

And re-run the test:

        assert game.user_found is False
        game.create_new_user()
>       assert game.user_found is True
E       assert False is True
E        +  where False = <tests.rockpaper.RockPaperScissors object at 0x06d51e10>.user_found

tests_rockpaper.py:27: AssertionError
======= short test summary info =======
FAILED tests_rockpaper.py::test_search_for_player - assert False is True

We can see the error appears that the user_found flag isn't being set correctly. Of course! Let's fix that:

def create_new_user(self):
    scores = open('rating.txt', 'a')
    print(f'\n{self.name} 0', file=scores, flush=True)
    scores.close()
    self.user_found = True

and let's delete the file rating.txt just to make sure this particular test runs properly -

C:\Users\user\Documents\dev\tests>pytest tests_rockpaper.py
========= test session starts =========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 4 items

tests_rockpaper.py ....      [100%]

========== 4 passed in 0.07s ==========

Perfect. However, I think there might be a problem in the code, let's run the test again:

_____ test_search_for_player ______

    def test_search_for_player():
        game = RockPaperScissors()
>       game.search_for_player()

tests_rockpaper.py:24:
 _ _ _ _

self = <tests.rockpaper.RockPaperScissors object at 0x07089030>

    def search_for_player(self):
        try:
            scores = open('rating.txt', 'r')
            for line in scores:
                score = line.split()
>               if score[0] == self.name:
E               IndexError: list index out of range

rockpaper.py:46: IndexError
======= short test summary info =======
FAILED tests_rockpaper.py::test_search_for_player - IndexError: list index out of range
===== 1 failed, 3 passed in 0.31s =====

Let's look at the contents of rating.txt? It is:

C:\Users\user\Documents\dev\tests>type rating.txt

myname 0

that looks fine? If we delete the rating.txt and run the tests again - they pass - but running the tests another time (a "post-file creation" scenario) - the tests fail.

I think I can see the bug - the line:

print(f'\n{self.name} 0', file=scores, flush=True)

with the "\n" line-feed is not being taken into consideration. Removing that, and deleting the rating.txt - the tests all run fine first time, a second time, and a third time. Let's look at the rating.txt file:

C:\Users\user\Documents\dev\tests>type rating.txt
myname 0
myname 0
myname 0

Ah, that's no good. It's continually appending the data file.

So, let's change:

scores = open('rating.txt', 'a')

into:

scores = open('rating.txt', 'w')

and run the tests again a few times - yes, that fixes it - we're only seeing a single line of data in the file, and all the tests still pass successfully.

Let's complete the rest of the program into the entry point, remove self.main() and fix game.options into game.choices, as we merged those two earlier:

if __name__ == "__main__":
    game = RockPaperScissors()
    game.getname()
    game.getchoices()
    game.search_for_player()
    if game.user_found is False:
        game.create_new_user()
    while game.running:
        response = False
        while response is False:
            computer = choice(game.choices)
            human = input()
            response = game.check_choice(human)

        if game.running and response:
            _round = game.check_result(human, computer)
            game.update_score(_round)

My IDE, PyCharm, highlights _round = game.check_result(human, computer) - it says human and computer are being referenced before being used. This is true because of "variable scope" - human and computer are defined and used inside the while loop - but once we leave the while loop - they are "lost". Python is a bit more forgiving than other languages. This code would crash in most other languages.

Let's address that later, and test the while loop with (and later, to monkeypatch the input):

def test_input_loop():
    game = RockPaperScissors()
    game.name = "myname"
    game.search_for_player()
    response = False
    while response is False:
        computer = choice(game.choices)
        human = input()
        response = game.check_choice(human)

Do the tests run successfully? No, we get a failure -

_________ test_input_loop _________

    def test_input_loop():
        game = RockPaperScissors()
        game.name = "myname"
>       game.search_for_player()

tests_rockpaper.py:35:
_ _ _ _ _ _ _ _ _ _ _ _

self = <tests.rockpaper.RockPaperScissors object at 0x06dd03b0>

    def search_for_player(self):
        try:
            scores = open('rating.txt', 'r')
>           for line in scores:
E           ValueError: I/O operation on closed file

rockpaper.py:44: ValueError
----- Captured stdout call --------
Hello, myname
======= short test summary info =======
FAILED tests_rockpaper.py::test_input_loop - ValueError: I/O operation on closed file
===== 1 failed, 4 passed in 0.35s =====

This is an interesting bug we have discovered. Usually people use a concept called a Context Manager to manage the scope of a resource. It automatically manages closing of the file, we don't need to specifically close that resource. Let's update the code to that standard pattern:

def search_for_player(self):
    try:
        scores = []
        with open('rating.txt', 'r') as score_file:
            scores = score_file.readlines()
        for line in scores:
            score = line.split()
            if score[0] == self.name:
                self.current_score = int(score[1])
                self.user_found = True
                # scores.close()
    except FileNotFoundError:
        pass

def create_new_user(self):
    with open('rating.txt', 'w') as score_file:
        score_file.write(f'{self.name} 0')
        self.user_found = True

And run the tests:

tests_rockpaper.py ....F        [100%]

============== FAILURES ===============
_________________ test_input_loop __________________

    def test_input_loop():
        game = RockPaperScissors()
        game.name = "myname"
        game.search_for_player()
        response = False
        while response is False:
>           computer = choice(game.choices)

tests_rockpaper.py:38:
_ _ _  _ _ _ _ _ _ _ _ _ _ _

self = <random.Random object at 0x03a14510>, seq = []

    def choice(self, seq):
        """Choose a random element from a non-empty sequence."""
        try:
            i = self._randbelow(len(seq))
        except ValueError:
>           raise IndexError('Cannot choose from an empty sequence') from None
E           IndexError: Cannot choose from an empty sequence

c:\pypy3.6\lib-python\3\random.py:267: IndexError
------------ Captured stdout call --------------------
Hello, myname
======= short test summary info =======
FAILED tests_rockpaper.py::test_input_loop - IndexError: Cannot choose from an empty sequence
===== 1 failed, 4 passed in 0.39s =====

An empty set of choices? We're not calling game.getchoices(), so the default set isn't being set correctly. As we're defining the default set of choices, let's force that during RockPaperScissors() instantiation.

    self.default_options = ["rock", "paper", "scissors"]
    self.choices = ""

Having an empty string will ensure the default choices are set because of the validation in the game.choices setter.

Running the tests again, we see that I forgot to add the default input - so let's do that - we'll make it the choice of "rock" -

def test_input_loop(monkeypatch):
    game = RockPaperScissors()
    game.name = "myname"
    game.search_for_player()
    response = False
    while response is False:
        computer = choice(game.choices)
        monkeypatch.setattr('sys.stdin', io.StringIO('rock'))
        human = input()
        response = game.check_choice(human)


========= test session starts =========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 5 items

tests_rockpaper.py ....F                                                                                                  [100%]

============== FAILURES ===============
______________ test_input_loop ______________

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x06d19d70>

    def test_input_loop(monkeypatch):
        game = RockPaperScissors()
        game.name = "myname"
        game.search_for_player()
        response = False
        while response is False:
            computer = choice(game.choices)
            monkeypatch.setattr('sys.stdin', io.StringIO('rock'))
            human = input()
>           response = game.check_choice(human)

tests_rockpaper.py:41:
_ _ _ _ _ _ _ _ _ _ _

self = <tests.rockpaper.RockPaperScissors object at 0x06d19d90>, human = 'rock'

    def check_choice(self, human):
        if human == '!exit':
            print('Bye!')
            self.running = False
            return True
        elif human == '!rating':
            print(f'Your rating: {self.current_score}')
            return False
>       elif human in self.options:
E       AttributeError: 'RockPaperScissors' object has no attribute 'options'

rockpaper.py:68: AttributeError
------------------------------- Captured stdout call -----------------------
Okay, let's start
Hello, myname
======= short test summary info =======
FAILED tests_rockpaper.py::test_input_loop - AttributeError: 'RockPaperScissors' object has no attribute 'options'
===== 1 failed, 4 passed in 0.42s =====

Of course, we changed that code to only look at game.choices - let's do a find all refactoring for game.options and make it game.choices, and re-run the tests. Result? 5 passed in 0.07s Great.

Being cheeky, let's fix the same test with proper-scoped values (human and computer), and see if it works:

def test_input_loop(monkeypatch):
    game = RockPaperScissors()
    game.name = "myname"
    game.search_for_player()
    response = False
    human = ""
    computer = ""
    while response is False:
        computer = choice(game.choices)
        monkeypatch.setattr('sys.stdin', io.StringIO('rock'))
        human = input()
        response = game.check_choice(human)

    if game.running and response:
        _round = game.check_result(human, computer)
        game.update_score(_round)

And run the tests:

C:\Users\user\Documents\dev\tests>pytest tests_rockpaper.py
========= test session starts =========
platform win32 -- Python 3.6.9[pypy-7.3.0-final], pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: C:\Users\user\Documents\dev\tests
plugins: dash-1.9.1
collected 5 items

tests_rockpaper.py .....     [100%]

========== 5 passed in 0.09s ==========

C:\Users\user\Documents\dev\tests>type rating.txt
myname 50

Let's try running the program!

C:\Users\user\Documents\dev\tests>pypy3 rockpaper.py
Okay, let's start
Enter your name: user
Hello, user
Enter an Odd Number of Options: rock,gun,lightning,devil,dragon,water,air,paper
Okay, let's start
rock
Sorry, but computer chose gun
devil
There is a draw (devil)
dragon
Well done. Computer chose lightning and failed
!rating
Your rating: 150
!exit
Bye!

C:\Users\user\Documents\dev\tests>type rating.txt
user 150

Great! Looks good. Here is the final testing file, and your program file.

There are still some minor fixes to make, such as - lines 105-107 could be rewritten to use a Context Manager - line 97 uses i which hasn't been initialised outside of the loop, the variable _round could be returned 3 times rather than set and returned at the end of the function (lines 79, 82, 85). Please try those fixes.

Otherwise, good effort. Keep it up!

Listing rockpaper.py:

from random import choice


class RockPaperScissors:
    def __init__(self):
        self.user_found = False
        self._name = ""
        self._choices = []
        self.default_options = ["rock", "paper", "scissors"]
        self.choices = ""
        self.current_score = 0
        self.running = True

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        print(f'Hello, {value}')
        self._name = value

    def getname(self):
        self.name = input('Enter your name: ')

    @property
    def choices(self):
        return self._choices

    @choices.setter
    def choices(self, value):
        print("Okay, let's start")
        self._choices = value.split(',') if value != "" else self.default_options

    def getchoices(self):
        self.choices = input('Enter an Odd Number of Options: ')

    def search_for_player(self):
        try:
            scores = []
            with open('rating.txt', 'r') as score_file:
                scores = score_file.readlines()
            for line in scores:
                score = line.split()
                if score[0] == self.name:
                    self.current_score = int(score[1])
                    self.user_found = True
        except FileNotFoundError:
            pass

    def create_new_user(self):
        with open('rating.txt', 'w') as score_file:
            score_file.write(f'{self.name} 0')
            self.user_found = True

    def check_choice(self, human):
        if human == '!exit':
            print('Bye!')
            self.running = False
            return True
        elif human == '!rating':
            print(f'Your rating: {self.current_score}')
            return False
        elif human in self.choices:
            return True
        print('Invalid input')
        return False

    def check_result(self, human, computer):
        human_winning = []
        board = self.choices * 2
        each_side = int((len(board) / 2) // 2)
        start = int(board.index(human) + 1)
        for i in range(start, start + each_side):
            human_winning.append(board[i])

        if human == computer:  # Draw
            print(f'There is a draw ({computer})')
            _round = 'Draw'
        elif computer not in human_winning:  # Win
            print(f'Well done. Computer chose {computer} and failed')
            _round = 'Win'
        else:  # Lose
            print(f'Sorry, but computer chose {computer}')
            _round = 'Lose'

        return _round

    def update_score(self, match_result):
        match_results = ['Win', 'Lose', 'Draw']
        points = [100, 0, 50]
        for i in range(len(match_results)):
            if match_result == match_results[i]:
                self.current_score += points[i]
                break

        if points[i] != 0:
            scores = open('rating.txt', 'r')
            list_of_scores = scores.readlines()
            for index, line in enumerate(list_of_scores):
                if line.split()[0] == self.name:
                    list_of_scores[index] = f'{self.name} {self.current_score}'
                    break

            scores = open('rating.txt', 'w')
            scores.writelines(list_of_scores)
            scores.close()


if __name__ == "__main__":
    game = RockPaperScissors()
    game.getname()
    game.getchoices()
    game.search_for_player()
    if game.user_found is False:
        game.create_new_user()
    while game.running:
        response = False
        human = ""
        computer = ""
        while response is False:
            computer = choice(game.choices)
            human = input()
            assert human
            response = game.check_choice(human)
        if game.running and response:
            _round = game.check_result(human, computer)
            game.update_score(_round)

Testing file tests_rockpaper.py:

import pytest
import io
from .rockpaper import RockPaperScissors
from random import choice

def test_class_create():
    game = RockPaperScissors()
    assert game

def test_get_name(monkeypatch):
    game = RockPaperScissors()
    monkeypatch.setattr('sys.stdin', io.StringIO('myname'))
    game.getname()
    assert game.name == "myname"

def test_get_choices(monkeypatch):
    game = RockPaperScissors()
    monkeypatch.setattr('sys.stdin', io.StringIO('rock,paper,scissors'))
    game.getchoices()
    assert game.choices == ["rock","paper","scissors"]


def test_search_for_player():
    game = RockPaperScissors()
    game.search_for_player()
    assert game.user_found is False
    game.name = "myname"
    game.create_new_user()
    assert game.user_found is True

# @pytest.mark.skip
def test_input_loop(monkeypatch):
    game = RockPaperScissors()
    game.name = "myname"
    game.search_for_player()
    response = False
    human = ""
    computer = ""
    while response is False:
        computer = choice(game.choices)
        monkeypatch.setattr('sys.stdin', io.StringIO('rock'))
        human = input()
        response = game.check_choice(human)

    if game.running and response:
        _round = game.check_result(human, computer)
        game.update_score(_round)
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ You're wrong about the scope of local variables. Their scope is the whole function, not the indentation level. PyCharm 2020.1 says Local variable 'inner' might be referenced before assignment. \$\endgroup\$ – Roland Illig May 9 at 11:33
  • \$\begingroup\$ Wow this is much more info that i expected, thanks so much for your time, I'll keep it in mind definitely. Again thank you so much for the length response. \$\endgroup\$ – DeltaHaxor May 9 at 14:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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