2
\$\begingroup\$

I've been working on my first real "project" and decided to make the periodic table that can simulate each atom after you press on it. I'm unsure if the code is good and can be used on my resume and therefore need your help :) Please tell me anything I can improve on regarding the code and also if it reflects good knowledge of programming.

import pygame
import multiprocessing
import time
import mysql.connector
import random
import math

#Class for initializing pygame and drawing the Main Window
class Window:
    pygame.init()
    pygame.font.init()
    def __init__(self, Dimensions, Title, Color):
        self.DIMENSIONS = Dimensions
        self.COLOR = Color
        self.TITLE = pygame.display.set_caption(Title)
        self.WINDOW = pygame.display.set_mode(self.DIMENSIONS)
        self.CLOCK = pygame.time.Clock()
        self.ICON = pygame.display.set_icon(pygame.image.load('ICON.png'))
        self.TEXTFONT = pygame.font.Font(None, 26)
        self.NUMBERFONT = pygame.font.Font(None, 20)

    #Sets background color for Window
    def Draw(self):
        self.WINDOW.fill(self.COLOR)

    #Gets user input regarding the program
    def GetUserActions(self):
        for Event in pygame.event.get():
            if Event.type == pygame.QUIT:
                return True

#Fetch data from MYSQL Database (Element properties, Periodic Table layout etc.)
def fetchFromDatabase(command):
    DBLOGIN = {"host": "localhost", "database": "OlijonDB", "user": "root", "passwd": "test123"}
    DB = mysql.connector.connect(host=DBLOGIN["host"], database=DBLOGIN["database"], user=DBLOGIN["user"],
                                 passwd=DBLOGIN["passwd"])
    DBCursor = DB.cursor(prepared=True)
    DBCursor.execute(command)
    return DBCursor.fetchall()

#----------------------------------------------------------------------------------------------------------

def P1():
    #Periodic Table Class
    class Table:
        def __init__(self, CellDimensions=[50, 50, 1], GridSize=18):
            self.CellDimensions = CellDimensions
            self.Colors = [(255, 255, 255), (253, 253, 150), (255, 105, 97), (119, 158, 203), (119, 221, 119), (218, 191, 222), (174, 198, 207)]
            self.GridSize = GridSize
            self.Grid = []
            self.Layout = []
            self.ElementTexts = []
            self.ElementColors = []

        #Generates a Grid
        def GenerateGrid(self):
            for Row in range(self.GridSize):
                self.Grid.append([])
                for Column in range(self.GridSize):
                    self.Grid[Row].append(0)

            #Generate Periodic Table Layout & Colors
            for i in range(len(self.Layout)):
                self.Grid[self.Layout[i][0]][self.Layout[i][1]] = self.ElementColors[i]

        #Gets mouse action and if pressed, launches another process with an animation of the selected Element
        def LaunchSimulation(self):
            if pygame.mouse.get_pressed()[0]:
                MousePosition = pygame.mouse.get_pos()
                Column = MousePosition[0] // (self.CellDimensions[0] + self.CellDimensions[2])
                Row = MousePosition[1] // (self.CellDimensions[1] + self.CellDimensions[2])
                if self.Grid[Row][Column] in self.ElementColors:
                    multiprocessing.Process(target=P2, args=(int(self.Layout.index([Row, Column]) + 1),)).start()
                    time.sleep(1)

        #Draws the Grid onto the Window
        def Draw(self, Window):
            for Row in range(self.GridSize):
                for Column in range(self.GridSize):

                    #Selects each cells color
                    CellColor = self.Grid[Row][Column]
                    Color = self.Colors[CellColor]

                    #Draws each cell with color
                    pygame.draw.rect(Window, Color,
                                     [(self.CellDimensions[2] + self.CellDimensions[0]) * Column + self.CellDimensions[2],
                                      (self.CellDimensions[2] + self.CellDimensions[1]) * Row + self.CellDimensions[2],
                                      self.CellDimensions[0], self.CellDimensions[1]])

                    #Draws the Symbol and Atomic Number for each Element in each Cell
                    if self.Grid[Row][Column] in self.ElementColors:
                        Text = W.TEXTFONT.render(self.ElementTexts[self.Layout.index([Row, Column])], True, (0, 0, 0))
                        Window.blit(Text, [
                            (self.CellDimensions[2] + self.CellDimensions[0]) * Column + self.CellDimensions[2] + 16,
                            (self.CellDimensions[2] + self.CellDimensions[1]) * Row + self.CellDimensions[2] + 20,
                            self.CellDimensions[0], self.CellDimensions[1]])
                        Numerics = W.NUMBERFONT.render(str(self.Layout.index([Row, Column]) + 1), True,
                                                       (0, 0, 0))
                        Window.blit(Numerics,
                                        [(self.CellDimensions[2] + self.CellDimensions[0]) * Column + self.CellDimensions[2] + 2,
                                         (self.CellDimensions[2] + self.CellDimensions[1]) * Row + self.CellDimensions[2] + 4,
                                         self.CellDimensions[0], self.CellDimensions[1]])

    #Function for refreshing the screen
    def refreshScreen():
        W.Draw()
        T.Draw(W.WINDOW)
        pygame.display.update()

    #Object Initialization
    W = Window([919, 919], "The Periodic Table", (255, 255, 255))
    T = Table()

    #Fetch from database
    for Element in fetchFromDatabase("SELECT * FROM periodictable"):
        T.Layout.append([Element[5], Element[6]])
        T.ElementTexts.append(Element[1])
        T.ElementColors.append(Element[7])

    #Main Loop
    Run = True
    while Run:
        # FPS Limiter
        W.CLOCK.tick(30)

        if W.GetUserActions(): Run = False

        T.GenerateGrid()
        T.LaunchSimulation()

        refreshScreen()
    pygame.quit()

#--------------------------------------------------------------------------------------------------------

def P2(AtomicNumber):
    #Element Class
    class Element:
        def __init__(self, Name, Protons, Neutrons, ElectronConfig):
            self.Name = Name
            self.Protons = Protons
            self.Neutrons = Neutrons
            self.CoreColor = (100, 0, 255)
            self.ElectronColor = (150, 0, 100)
            self.ElectronConfig = ElectronConfig
            self.CenterPosition = [W.DIMENSIONS[0] // 2, W.DIMENSIONS[1] // 2]

        #Generates & Draws the Atom
        def GenerateAtom(self, Window):
            #Draws the Atom Core
            for i in range(self.Protons + self.Neutrons):
                print(self.Protons + self.Neutrons)
                pygame.draw.circle(Window, self.CoreColor,
                                   [self.CenterPosition[0] + RandomPosition(0, 10),
                                    self.CenterPosition[1] + RandomPosition(0, 10)], 20)

            #Draws the number of Electron Shells
            for i in range(len(self.ElectronConfig.split(","))):
                radius = [100, 150, 200, 250, 300, 350, 400]
                pygame.draw.circle(Window, (211, 211, 211), self.CenterPosition, radius[i], 2)

                #Draws each Electron on the correct Electron Shell
                for j in range(int(self.ElectronConfig.split(",")[i])):
                    angle = random.randint(0, int((10 - 1) / 0.1)) * 0.1 + 1
                    pygame.draw.circle(Window, self.ElectronColor, [round(radius[i] * math.cos(angle) + self.CenterPosition[0]),
                                                  round(radius[i] * math.sin(angle) + self.CenterPosition[1])], 8)

    def RandomPosition(start, end):
        return random.randint(start, end)

    def renderScreen():
        W.Draw()
        E.GenerateAtom(W.WINDOW)
        pygame.display.update()

    #Object Definition
    W = Window([1000, 1000], "Process 2", (255,255,255))
    for Properties in fetchFromDatabase("SELECT * FROM periodictable WHERE atomicnumber = " + str(AtomicNumber)):
        E = Element(Properties[2], Properties[0], Properties[3] - Properties[0 ], Properties[4])

    #Main Loop
    Run = True
    while Run:
        #FPS Limiter
        W.CLOCK.tick(5)

        #Checks if User has pressed "Close" Button and thereafter exits while loop
        if W.GetUserActions(): Run = False

        renderScreen()
    pygame.quit()

#---------------------------------------------------------------------------------------------------

if __name__ == '__main__':
    P1()
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

PEP8

Your member variables such as DIMENSIONS, methods such as Draw and arguments such as Dimensions should be in lower_snake_case.

Static code

These lines:

pygame.init()
pygame.font.init()

should not go in what's effectively the static section of Window. Move them to a function.

Hard-coded credentials

Do not hard-code these:

DBLOGIN = {"host": "localhost", "database": "OlijonDB", "user": "root", "passwd": "test123"}

into the script, for security purposes. Move them to an (ideally encrypted) configuration file.

Database connection

You don't close your database connection; this is bad. You have a method that receives only a command and has to spin up a new connection every time; you need to avoid this as well. Consider moving a single connection variable somewhere (not a global) where it can be accessed by your fetch function, and close it at the end of the program.

Non-lists

CellDimensions=[50, 50, 1]

likely doesn't deserve to be a list and should be separate variables, since it only ever receives fixed indices.

Inner class

class Element needs to be moved up to global scope. It doesn't need to have any closures and does not rely on AtomicNumber.

Select

for Properties in fetchFromDatabase("SELECT * FROM periodictable WHERE atomicnumber = " + str(AtomicNumber)):
    E = Element(Properties[2], Properties[0], Properties[3] - Properties[0 ], Properties[4])

has a number of significant issues:

Properties should be unpacked to five variables. Your Element arguments are:

Name, Protons, Neutrons, ElectronConfig

Given your indexing of

Properties[2], Properties[0], Properties[3] - Properties[0 ], Properties[4]

then the unpack will be

protons, ???, name, ???, electron_config = properties

where I have no idea what some of those names will be.

Your select * is fragile. Whereas columns are guaranteed to be returned in order of declaration assuming that MySQL adheres to the standard, there is no guarantee that someone won't shuffle those around in the database in the future. Spell out the columns that you're selecting, particularly since it seems you don't use them all.

You have a direct injection vulnerability. Never string-append query parameters. The MySQL connector library has a thing for this - the params kwarg.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you so much for your feedback, I've revised my code according to your comments. However, I'm stuck regarding the database connection, how would I go about keeping the connection open? \$\endgroup\$ Jul 13, 2020 at 13:49
  • \$\begingroup\$ This is complicated by the fact that P2 (which really needs to be renamed) is invoked via multiprocessing. The connection object may or may not be viable across process boundaries. Either way, make something like a database class that is a context manager and has your query as a method. If the connection cannot be shared among processes, each process will need to instantiate one. \$\endgroup\$
    – Reinderien
    Jul 13, 2020 at 14:16

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.