4
\$\begingroup\$

Since I bought a professional steel-darts-kit two weeks ago, I created this little python console program that aids me with track-keeping of the game score and further statistical data I may need in the future.

The program essentially takes the game mode (from 301 or 501 down, but that's free choice), the two player names and then goes into the darts game loop where it stays until one of the two players reached 0 score. every now and then, there will be sqlite3 query's to a little database which stores the checkout possibilities (e.g. for 55 points left it'd be 15 + Double 20) and game statictics. After a player wins, the game data goes into another table in my database.

Essentially, the code works fine, takes care of bad user input, has a nice colored output to the console and keeps track of the game accurately. After every game, it is easy to start another one where the other player begins, it is also possible to switch players after a finished game by inputting new user names.

The reason I write this post is that althought I have all the necessary functionalities I want, it really is full of dirty workarounds and looks pretty complex already. I would like to have some advice on which best practises I missed, which lines of code are just obsolete or easy to rephrase. For example, I had to fiddle around with it for numerous hours until I came up with the ugly workaround for alternating player game starts. Also there are quite a lot of repetitions in the code (the whole db-query reformatting stuff) and especially the game loop for player 1 and player 2 (which are essentially the same), so I'd like some suggestions how I could improve the code.

Also I'd happily take any suggestions on how to calculate a average score per 3 darts. I know I would need to implement some sort of input which says with how many darts the inputted points were scored. Then I could just append all the thrown score and darts into a table and do a easy average calculation, but I'm lacking a idea of how to implement a darts thrown into the code without making it more complicated and longer to input the data. And when taking a default for 3 darts per inputted score it would be vastly inaccuate when it comes to the finishes which may take a while and sometimes only need 1 dart thrown until you fail and have to try again... :-D

#necessary imports
import sqlite3
import re
import time
import datetime
from pandas import read_excel
from colorama import init

#setup db connection
darts_db = sqlite3.connect('darts_db.db')
cursor = darts_db.cursor()

#get checkout value list
checkout_table = read_excel('checkout.xlsx')
values = checkout_table["value"].tolist()

#basic setup
init(autoreset=True) #autoreset of coloring after every printout
game_loop = True
points_default = int(input("enter starting score (301, 501, ...): "))

P1_Game_Wins = 0 
P2_Game_Wins = 0

Player_1 = input("Name of Player 1: ")
Player_2 = input("Name of Player 2: ")

#show stats for each player before the game starts
cursor.execute("SELECT count(winner) FROM game_overview WHERE winner == ?", (Player_1,))
P1_Stats = str(cursor.fetchall()[0]) 
P1_Stats = re.sub('''[()',]''', '', P1_Stats) 
cursor.execute("SELECT max(cast(checkout as int)) FROM game_overview WHERE winner == ?", (Player_1,))
P1_Checkout = str(cursor.fetchall()[0]) 
P1_Checkout = re.sub('''[()',]''', '', P1_Checkout)  
print("\n\x1b[1;33;40mTotal Wins of {0}: {1}. Highest Checkout: {2}".format(Player_1, P1_Stats, P1_Checkout)) 
cursor.execute("SELECT count(winner) FROM game_overview WHERE winner == ?", (Player_2,))
P2_Stats = str(cursor.fetchall()[0]) 
P2_Stats = re.sub('''[()',]''', '', P2_Stats)
cursor.execute("SELECT max(cast(checkout as int)) FROM game_overview WHERE winner == ?", (Player_2,))
P2_Checkout = str(cursor.fetchall()[0]) 
P2_Checkout = re.sub('''[()',]''', '', P2_Checkout)  
print("\x1b[1;33;40mTotal Wins of {0}: {1}. Highest Checkout: {2}".format(Player_2, P2_Stats, P2_Checkout)) 

while game_loop == True:

    P1_Score = points_default
    P2_Score = points_default

    #switch players after every match so that they alternate --> dirty but works
    if (P1_Game_Wins + P2_Game_Wins) == 0:
        pass
    else:
        Player_dummy = Player_1
        Player_1 = Player_2
        Player_2 = Player_dummy
        Win_History_dummy = P1_Game_Wins
        P1_Game_Wins = P2_Game_Wins
        P2_Game_Wins = Win_History_dummy
                                                             #ANSI Green       ANSi escape
    print("\n{0} is playing vs {1}. We play down from \x1b[1;32;40m{2} \x1b[0m. {3} begins!".format(Player_1, Player_2, points_default, Player_1))                                                  
    #playing loop
    while P1_Score or P2_Score != 0:


        while True: #player 1 playing loop
            print("\nPoints of\x1b[1;32;40m {0} \x1b[0mbefore his turn: \x1b[1;32;40m{1}".format(Player_1, P1_Score))
            if P1_Score in values:

                cursor.execute("SELECT checkout FROM checkout_table WHERE value == ?", (P1_Score,)) #select checkout from db
                checkout = str(cursor.fetchall()[0])                                                #store select in variable
                checkout = re.sub('''[()',]''', '', checkout)                                       #reformatting to printable form
                print("Checkout: {}".format(checkout))                                              #and finally: print
            else:
                pass
            try:
                P1_Hit = int(input("Points scored: "))
            except ValueError:
                print("\x1b[1;31;40mPlease enter a number between 0 and 180!")
            else:
                if 0 <= P1_Hit <= 180:
                    break
                else:
                    print("\x1b[1;31;40mPlease enter a number between 0 and 180!")

        P1_Score = P1_Score - int(P1_Hit) #player 1 scoring function
        if P1_Score == 0:
            print("Points of {0} after his turn: {1}\n".format(Player_1, P1_Score))
            print("\x1b[1;33;40m===============***WIN! WIN! WIN!***===============")
            print("\x1b[1;33;40m===============***{0} IS A GOD!!!***===============\n".format(Player_1.upper()))
            P1_Game_Wins += 1
            unix = int(time.time())
            date = str(datetime.datetime.fromtimestamp(unix).strftime('%Y-%m-%d %H:%M:%S'))
            cursor.execute("INSERT INTO game_overview(game_id, player1, player2, winner, gamemode, checkout) VALUES (?, ?, ?, ?, ?, ?)",
                           (date, Player_1, Player_2, Player_1, points_default, P1_Hit))

            darts_db.commit()
            break
        elif P1_Score >= 2: 
            print("Points of {0} after his turn: {1}".format(Player_1, P1_Score))
        elif P1_Score <= 1:    
            print("\x1b[1;31;40mYou scored too much! Noob.")
            P1_Score = P1_Score + int(P1_Hit)


        while True: #player 2 playing loop -> essentially the same again for player 2
            print("\nPoints of\x1b[1;32;40m {0} \x1b[0mbefore his turn: \x1b[1;32;40m{1}".format(Player_2, P2_Score))
            if P2_Score in values:
                cursor.execute("SELECT checkout FROM checkout_table WHERE value == ?", (P2_Score,))
                checkout = str(cursor.fetchall()[0])
                checkout = re.sub('''[()',]''', '', checkout)
                print("Checkout: {}".format(checkout))
            else:
                pass
            try:
                P2_Hit = int(input("Points scored: "))
            except ValueError:
                print("\x1b[1;31;40mPlease enter a number between 0 and 180!")
            else:
                if 0 <= P2_Hit <= 180:
                    break
                else:
                    print("\x1b[1;31;40mPlease enter a number between 0 and 180!")

        P2_Score = P2_Score - int(P2_Hit) #player 2 scoring function
        if P2_Score == 0:
            print("Points of {0} after his turn: {1}\n".format(Player_2, P2_Score))
            print("\x1b[1;33;40m===============***WIN! WIN! WIN!***===============")
            print("\x1b[1;33;40m===============***{0} IS A GOD!!!***===============\n".format(Player_2.upper()))
            P2_Game_Wins += 1
            unix = int(time.time())
            date = str(datetime.datetime.fromtimestamp(unix).strftime('%Y-%m-%d %H:%M:%S'))
            cursor.execute("INSERT INTO game_overview(game_id, player1, player2, winner, gamemode, checkout) VALUES (?, ?, ?, ?, ?, ?)",
                           (date, Player_1, Player_2, Player_2, points_default, P2_Hit))
            darts_db.commit()
            break
        elif P2_Score >= 2: 
            print("Points of {0} after his turn: {1}".format(Player_2, P2_Score))
        elif P2_Score <= 1:    
            print("\x1b[1;31;40mYou scored too much! Noob.")
            P2_Score = P2_Score + int(P2_Hit)

    print("Total Wins of {0}: {1} \nTotal Wins of {2}: {3}\n".format(Player_1, P1_Game_Wins, Player_2, P2_Game_Wins))

    another_round = input("Another Match? Press[Enter]. Or [N] for new players.")
    if another_round == '':
        game_loop = True
    elif another_round.upper() == 'N':
        P1_Game_Wins = 0 
        P2_Game_Wins = 0

        Player_1 = input("\nName of Player 1: ")
        Player_2 = input("Name of Player 2: ")
        game_loop = True
    else:
        game_loop = False

#close db connection            
cursor.close()    
darts_db.close()
\$\endgroup\$
  • 1
    \$\begingroup\$ Since this is an English-speaking website, you might want to translate the German strings to English. As it stands, you already have a mixture between German and English user-prompts, which is probably not the best usability, depending on the target audience. \$\endgroup\$ – Graipher Feb 6 '18 at 11:34
1
\$\begingroup\$

There are quite a few things you could be doing differently. The first step would be separating out constants and functions that do something with your database.

The next step is bundling functionality into classes, where necessary.

Here I did this once with a DBConnection class, that contains all those SQL executions and a Player class that just holds a player's name, score and wins. The DBConnection class is a contextmanager, so we can use it with the with statement, which takes care of closing the database connection even in case your code raises an exception somewhere else.

I basically replaced all redundancies which were special cases for Player 1 and Player 2 with a for loop over players and used the new f-strings introduced in Python 3.6 to make all the printing easier.

Some more notes:

  • "[()',]" is easier to understand than '''[()',]'''
  • Take a look at Python's official style-guide, PEP8. It recommends using lower_case for variables and functions, PascalCase for classes and UPPER_CASE for constants. It also recommends adhering to a maximum line length of 80 characters (which I did not quite follow here...).
  • cursor.fetchone() is easier than cursor.fetchall()[0].
  • Use the Python idiom a, b = b, a to swap two variables.
  • Use a if __name__ == "__main__: guard to allow importing this modules objects from another script.
  • datetime has the datetime.datetime.now() classmethod that gets you the current time.
  • score in values takes \$\mathcal{O}(n)\$ time. If values is a set, it only takes \$\mathcal{O}(1)\$ time. Read up on the different time complexities of the built-in data types.
# necessary imports
import sqlite3
import re
import time
import datetime
from pandas import read_excel
from colorama import init

# autoreset of coloring after every printout
init(autoreset=True)

# some constants for nice colors in printing
BLUE = "\x1b[1;33;40m"
GREEN = "\x1b[1;32;40m"
RED = "\x1b[1;31;40m"
CLEAR = "\x1b[0m"

# get checkout value list
VALUES = set(read_excel('checkout.xlsx')["value"].tolist())


class DBConnection:
    """Manages a db connection and defines the needed queries.
       Needs to be used as a contextmanager."""

    def __init__(self, file_name):
        self.file_name = file_name
        self.db = None
        self.cursor = None

    def __enter__(self):
        self.db = sqlite3.connect(self.file_name)
        self.cursor = self.db.cursor()
        return self

    def __exit__(self, *args):
        self.cursor.close()
        self.db.close()

    def get_wins(self, player):
        self.cursor.execute(
            "SELECT count(winner) FROM game_overview WHERE winner == ?", (player,))
        return re.sub("[()',]", "", str(self.cursor.fetchone()))

    def get_max_checkout(self, player):
        self.cursor.execute(
            "SELECT max(cast(checkout as int)) FROM game_overview WHERE winner == ?", (player,))
        return re.sub("[()',]", "", str(self.cursor.fetchone()))

    def get_checkout(self, score):
        self.cursor.execute(
            "SELECT checkout FROM checkout_table WHERE value == ?", (score,))
        return re.sub("[()',]", "", str(self.cursor.fetchone()))

    def record_game(self, player_1, player_2, winner, points_default, checkout):
        date = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        self.cursor.execute("INSERT INTO game_overview(game_id, player1, player2, winner, gamemode, checkout) VALUES (?, ?, ?, ?, ?, ?)",
                            (date, player_1, player_2, winner, points_default, checkout))
        self.db.commit()


class Player:

    def __init__(self, name, score=301):
        self.name = name
        self.score = score
        self.wins = 0

    def __str__(self):
        """`print(player)` just prints the name of the player."""
        return self.name


def get_player_hit():
    """Ask the user until he supplies a valid hit."""
    while True:
        try:
            hit = int(input("Points scored: "))
        except ValueError:
            print(f"{RED}Please enter a number between 0 and 180!")
        else:
            if 0 <= P1_Hit <= 180:
                return hit
            else:
                print(f"{RED}Please enter a number between 0 and 180!")

# Put main code under this guard to allow importing from another module
if __name__ == "__main__":
    # game setup
    points_default = int(input("enter starting score (301, 501, ...): "))
    players = [Player(input("Name of Player 1: "), points_default),
               Player(input("Name of Player 2: "), points_default)]

    # setup db connection
    with DBConnection('darts_db.db') as db:
        while True:
            # show stats for each player before the game starts
            for player in players:
                wins = db.get_wins(player.name)
                checkout = db.get_max_checkout(player.name)
                print(f"\n{BLUE}Total Wins of {player}: {wins}. Highest Checkout: {checkout}")

            # switch players after every match so that they alternate
            if (players[0].wins + players[1].wins) != 0:
                players[0], players[1] = players[1], players[0]

            print(f"\n{players[0]} is playing vs {players[1]}. We play down from {GREEN}{points_default}{CLEAR}. {players[0]} begins!")

            # playing loop
            play = True
            while play:
                for player in players:
                    print(f"\nPoints of {GREEN}{player}{CLEAR} before his turn: {GREEN}{player.score}")
                    if player.score in VALUES:
                        print("Checkout:", db.get_checkout(player.score))

                    hit = get_player_hit()
                    player.score -= hit

                    if player.score == 0:
                        print(f"Points of {player} after his turn: {player.score}\n")
                        print(f"{BLUE}===============***WIN! WIN! WIN!***===============")
                        print(f"{BLUE}===============***{player.name.upper()} IS A GOD!!!***===============\n")
                        player.wins += 1
                        db.record_game(players[0].name, players[
                                       1].name, player.name, points_default, hit)
                        play = False
                        break
                    elif player.score >= 2:
                        print(f"Points of {player} after his turn: {player.score}")
                    elif player.score <= 1:
                        print(f"{RED}You scored too much! Noob.")
                        player.score += hit

            for player in players:
                print(f"Total Wins of {player}: {player.wins}")

            another_round = input(
                "Another Match? Press[Enter]. Or [N] for new players.")
            if another_round == '':
                continue
            elif another_round.upper() == 'N':
                players = [Player(input("Name of Player 1: "), points_default),
                           Player(input("Name of Player 2: "), points_default)]
            else:
                break
\$\endgroup\$
  • 1
    \$\begingroup\$ Wow, thank you alot! This is exaclty what I was looking for! I added a few new stats to the DB and thus put all the stats showing at the beginning of a game in a function in a new module. Now i just import the function from there. But I like the idea of your context manager much more! there is a lot to learn from your answer, I'll need a week to really understand it all. Thanks again! \$\endgroup\$ – Cut7er Feb 27 '18 at 18:34

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.