1
\$\begingroup\$

Recently I have been learning Python after years of putting it off, and I am having a really fun time with it. However after I spent a few hours making something that I thought was pretty cool, I decided to share it to one of the Discord servers that I am in. And then from there on everyone just kind of mocked me, and my code even got posted on the r/badcode subreddit where they mocked me even more. All I want to know is if I actually did something really stupid and if so how can I fix it, or if they are all just being mean because I am new to the language.

What I made:

While learning python I kept stumbling across a ton of "Python projects for beginners" sites, and I took a lot of interest in them, as they were fun to make and it was cool to see the result. What I did with this was take two of the projects and combine them into one just for a little challenge for myself.

The two that I combined were; Making a register/login form for the console, and a rock paper scissors game against the computer. A silly idea that is impractical but was just a little side project for fun.

The code:

The register snippet:

import random

def register():
    username = input("Please create a username: ")
    password = input("Please create a password: ")
    file = open("accountfile.txt","a")
    file.write(username)
    file.write(" ")
    file.write(password)
    file.write("\n")
    file.close()
    if login():
        print("You are now logged in...")
    else:
        print("You aren't logged in!")

The login snippet:

def login():
    username = input("Please enter your username: ")
    password = input("Please enter your password: ")  
    for line in open("accountfile.txt","r").readlines():
        login_info = line.split()
        if username == login_info[0] and password == login_info[1]:
            print("Correct credentials!")
            return True
    print("Incorrect credentials.")
    return False



required_input1 = "register"
required_input2 = "login"
user_input = input("What would you like to do?: ")
while user_input == "commands":
    print("My commands are: register, login and commands, all commands are case sensitive!")
    user_input = input("What would you like to do?: ")
if user_input == required_input1:
      register()
elif user_input == required_input2:
      login()

The rock/paper/scissors snippet:

def game(): 
  place = "test"
user_wins = 0
computer_wins = 0
draw = 0

options = ["rock", "paper", "scissors"]
while True:
    user_input = input("Type Rock/Paper/Scissors or Q to quit: ").lower()
    if user_input == "q":
        print("Goodbye!")
        quit()
    elif user_input not in options:
            continue

    random_number = random.randint(0, 2)
    # rock : 0, paper: 1, scissors: 2
    computer_pick = options[random_number]
    print("Computer picked", computer_pick + ".")

    if user_input == computer_pick:
        print('Draw!')
        draw += 1
    elif user_input == "rock" and computer_pick == "scissors":
        print("You won!")
        user_wins += 1
    elif user_input == "paper" and computer_pick == "rock":
        print("You won!")
        user_wins += 1
    elif user_input == "scissors" and computer_pick == "scissors":
        print("You won!")
        user_wins += 1
    else:
        print("Computer wins")
        computer_wins += 1

    if computer_wins == 3:
            print("The computer won that game, better luck next time!")
            quit()
    elif user_wins == 3:
            print("You won the game, nice job!")
            quit()

Summary

As I stated previously, all the code functions how I want it to. However because of the mockery I am receiving I am assuming that there is a much easier way of executing this.
Let me know what you guys think, and thank you for your time!

\$\endgroup\$
3
  • 4
    \$\begingroup\$ Welcome to Code Review! Please verify that the code you posted is indented as intended, especially for the rock-paper-scissors snippet. \$\endgroup\$ May 6, 2022 at 18:54
  • 6
    \$\begingroup\$ Are you sure you want a Python-2.x review? This looks more like Python-3.x code. \$\endgroup\$
    – AJNeufeld
    May 6, 2022 at 19:26
  • 5
    \$\begingroup\$ I think it's mean-spirited for someone to have posted this on /r/badcode. Since you describe yourself as a beginner, it's not like you're expected to know better; and it's not great but it isn't a disaster either. \$\endgroup\$
    – Reinderien
    May 6, 2022 at 19:27

1 Answer 1

1
\$\begingroup\$

Recently I have been learning Python after years of putting it off, and I am having a really fun time with it.

Keep it up!

In register, your file needs to be protected with a with so that - if anything bad happens (an exception) after the file is open, closure is still guaranteed.

Consider checking whether the username already exists when the user attempts to register.

Your database format for accounts is a problem. If a person has spaces in their username or password, your current code will not work. There are many ways to fix this. One simple way is to base64-encode your fields; spaces will not affect them.

Never input() to get a password. Use getpass instead, which is more secure because it does not echo characters that are input.

I assume that you login() during your register so that the user can re-enter their credentials, to make sure no typos were made. But in your implementation, the damage is already done: if they did make a typo, you've already saved it to a file. You should make an attempt at temporarily verifying those credentials before they're saved to the file.

options should be a tuple rather than a list since you're not changing it.

Don't quit() where you do so; instead return from a function that includes this code. That way, in the future if you want to include the ability for a player to play again, it won't be as difficult.

I assume that you need to log in before you play the game. You have not shown this.

Some of your commands are case-sensitive and others aren't. Why? Might as well make them all case-insensitive.

Suggested

import random
from getpass import getpass
from base64 import b64encode, b64decode
from typing import Iterator


def load_accounts(filename: str = 'accountfile.txt') -> Iterator[tuple[str, str]]:
    with open(filename) as file:
        for line in file:
            username, password = (
                b64decode(word).decode()
                for word in line.split()
            )
            yield username, password


def login_with_accounts(accounts: dict[str, str]) -> bool:
    username = input("Please enter your username: ")
    password = getpass("Please enter your password: ")
    if accounts.get(username) == password:
        print("Correct credentials!")
        return True
    print("Incorrect credentials.")
    return False


def login() -> bool:
    return login_with_accounts(
        dict(load_accounts())
    )


def register() -> None:
    try:
        accounts = dict(load_accounts())
    except FileNotFoundError:
        accounts = {}

    while True:
        username = input("Please create a username: ")
        password = getpass("Please create a password: ")

        if username in accounts:
            print('That username is already taken.')
            continue

        print('Re-type your credentials for verification.')
        trial_accounts = {username: password}
        if login_with_accounts(trial_accounts):
            break

    with open("accountfile.txt", "ba") as file:
        file.write(b64encode(username.encode()))
        file.write(b' ')
        file.write(b64encode(password.encode()))
        file.write(b'\n')


def authenticate() -> None:
    while True:
        print("My commands are: register, login.")
        user_input = input("What would you like to do?: ").lower()
        if user_input == 'register':
            register()
            break
        elif user_input == 'login' and login():
            break


def game() -> None:
    user_wins = 0
    computer_wins = 0
    draw = 0

    options = ("rock", "paper", "scissors")
    while True:
        user_input = input("Type Rock/Paper/Scissors or Q to quit: ").lower()
        if user_input == "q":
            print("Goodbye!")
            return
        if user_input not in options:
            continue

        computer_pick = random.choice(options)
        print("Computer picked", computer_pick + ".")

        if user_input == computer_pick:
            print('Draw!')
            draw += 1
        elif (
            user_input == "rock" and computer_pick == "scissors" or
            user_input == "paper" and computer_pick == "rock" or
            user_input == "scissors" and computer_pick == "scissors"
        ):
            print("You won!")
            user_wins += 1
        else:
            print("Computer wins")
            computer_wins += 1

        if computer_wins >= 3:
            print("The computer won that game, better luck next time!")
            return
        elif user_wins >= 3:
            print("You won the game, nice job!")
            return


if __name__ == '__main__':
    authenticate()
    game()
\$\endgroup\$
6
  • \$\begingroup\$ Your reasons for using with are good, but turning load_accounts into a generator negates them. If an exception happens in code using the generator, after the generator is started but before it is exhausted, it will be left dangling, unclosed until garbage collection or interpreter exit, meaning so will the file. In both cases, it is fully consumed by a dict() call, but why complicate beginner code with a potentially unsafe generator construct when load_accounts() could directly return a dict? \$\endgroup\$
    – AJNeufeld
    May 7, 2022 at 15:48
  • \$\begingroup\$ @AJNeufeld "Why complicate beginner code" is valid criticism, but your mental model of a context manager is incorrect. Try out this demo gist. \$\endgroup\$
    – Reinderien
    May 7, 2022 at 15:56
  • 1
    \$\begingroup\$ Specifically, the mechanism by which the generator stops and the context manager is closed is GeneratorExit. \$\endgroup\$
    – Reinderien
    May 7, 2022 at 16:02
  • \$\begingroup\$ Your gist demonstrates a GeneratorExit is sent to the generator (due to being garbage collected possibly as late as at interpreter exit), but does not demonstrate how soon it happens which was my concern. My concern was a heavily workload might delay garbage collection. Playing around, I see as long as only local variables are used, the interpreter does seem to ensure the generator is closed expeditiously - when the last local variable holding a reference to the variable goes out of scope. It took intentionally doing something stupid to substantially delay generator exit. \$\endgroup\$
    – AJNeufeld
    May 9, 2022 at 17:35
  • 1
    \$\begingroup\$ Dangling circular references. Dict A with a reference to Dict B which held a reference to Dict A and the generator. No other references to either dictionaries. Garbage collector has to mark-and-sweep to get rid of them. Kept busy with sum(sqrt(range(1,10_000_000)). As I said, intentionally doing something stupid, that should never normally occur. Using with inside a generator is not unsafe, as I feared. \$\endgroup\$
    – AJNeufeld
    May 9, 2022 at 17:49

Your Answer

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

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