8
\$\begingroup\$

I wrote a program in pygame that basically acts as a physics engine for a ball. You can hit the ball around and your strokes are counted, as well as an extra stroke for going out of bounds. If I do further develop this, I'd make the angle and power display toggleable, but I do like showing them right now:

import pygame as pg
import math

SCREEN_WIDTH = 1500
SCREEN_HEIGHT = 800
WINDOW_COLOR = (100, 100, 100)
BALL_COLOR = (255, 255, 255)
BALL_OUTLINE_COLOR = (255, 0, 0)
LINE_COLOR = (0, 0, 255)
ALINE_COLOR = (0, 0, 0)
START_X = int(.5 * SCREEN_WIDTH)
START_Y = int(.99 * SCREEN_HEIGHT)
POWER_MULTIPLIER = .85
SPEED_MULTIPLIER = 2
BALL_RADIUS = 10

pg.init()
pg.display.set_caption('Golf')
window = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pg.event.set_grab(True)
pg.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0))

strokeFont = pg.font.SysFont("monospace", 50)
STROKECOLOR = (255, 255, 0)

powerFont = pg.font.SysFont("arial", 15, bold=True)
POWERCOLOR = (0, 255, 0)

angleFont = pg.font.SysFont("arial", 15, bold=True)
ANGLECOLOR = (0, 255, 0)

penaltyFont = pg.font.SysFont("georgia", 40, bold=True)
PENALTYCOLOR = (255, 0, 0)


class Ball(object):
    def __init__(self, x, y, rad, c, oc):
        self.x = x
        self.y = y
        self.radius = rad
        self.color = c
        self.outlinecolor = oc

    def show(self, window):
        pg.draw.circle(window, self.outlinecolor, (self.x, self.y), self.radius)
        pg.draw.circle(window, self.color, (self.x, self.y), self.radius - int(.4 * self.radius))

    @staticmethod
    def path(x, y, p, a, t):
        vx, vy = p * math.cos(a), p * math.sin(a)  #Velocities
        dx, dy = vx * t, vy * t - 4.9 * t ** 2 #Distances Traveled
        print('     x-pos: %spx' % str(round(dx + x)))
        print('     y-pos: %spx' % str(round(abs(dy - y))))

        return round(dx + x), round(y - dy)

    @staticmethod
    def quadrant(x,y,xm,ym):
        if ym < y and xm > x:
            return 1
        elif ym < y and xm < x:
            return 2
        elif ym > y and xm < x:
            return 3
        elif ym > y and xm > x:
            return 4
        else:
            return False


def draw_window():
    window.fill(WINDOW_COLOR)
    ball.show(window)
    if not shoot:
        arrow(window, ALINE_COLOR, ALINE_COLOR, aline[0], aline[1], 5)
        arrow(window, LINE_COLOR, LINE_COLOR, line[0], line[1], 5)

    stroke_text = 'Strokes: %s' % strokes
    stroke_label = strokeFont.render(stroke_text, 1, STROKECOLOR)
    if not strokes:
        window.blit(stroke_label, (SCREEN_WIDTH - .21 * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))
    else:
        window.blit(stroke_label, (SCREEN_WIDTH - (.21+.02*math.floor(math.log10(strokes))) * SCREEN_WIDTH, SCREEN_HEIGHT - .985 * SCREEN_HEIGHT))

    power_text = 'Shot Strength: %sN' % power_display
    power_label = powerFont.render(power_text, 1, POWERCOLOR)
    if not shoot: window.blit(power_label, (cursor_pos[0] + .008 * SCREEN_WIDTH, cursor_pos[1]))

    angle_text = 'Angle: %s°' % angle_display
    angle_label = angleFont.render(angle_text, 1, ANGLECOLOR)
    if not shoot: window.blit(angle_label, (ball.x - .06 * SCREEN_WIDTH, ball.y - .01 * SCREEN_HEIGHT))

    if Penalty:
        penalty_text = 'Out of Bounds! +1 Stroke'
        penalty_label = penaltyFont.render(penalty_text, 1, PENALTYCOLOR)
        penalty_rect = penalty_label.get_rect(center=(SCREEN_WIDTH/2, .225*SCREEN_HEIGHT))
        window.blit(penalty_label, penalty_rect)

    pg.display.flip()


def angle(cursor_pos):
    x, y, xm, ym = ball.x, ball.y, cursor_pos[0], cursor_pos[1]
    if x-xm:
        angle = math.atan((y - ym) / (x - xm))
    elif y > ym:
        angle = math.pi/2
    else:
        angle = 3*math.pi/2

    q = ball.quadrant(x,y,xm,ym)
    if q: angle = math.pi*math.floor(q/2) - angle

    if round(angle*180/math.pi) == 360:
        angle = 0

    if x > xm and round(angle*180/math.pi) == 0:
        angle = math.pi

    return angle


def arrow(screen, lcolor, tricolor, start, end, trirad):
    pg.draw.line(screen, lcolor, start, end, 2)
    rotation = math.degrees(math.atan2(start[1] - end[1], end[0] - start[0])) + 90
    pg.draw.polygon(screen, tricolor, ((end[0] + trirad * math.sin(math.radians(rotation)),
                                        end[1] + trirad * math.cos(math.radians(rotation))),
                                       (end[0] + trirad * math.sin(math.radians(rotation - 120)),
                                        end[1] + trirad * math.cos(math.radians(rotation - 120))),
                                       (end[0] + trirad * math.sin(math.radians(rotation + 120)),
                                        end[1] + trirad * math.cos(math.radians(rotation + 120)))))


def distance(x,y):
    return math.sqrt(x**2 + y**2)


x, y, time, power, ang, strokes = 0, 0, 0, 0, 0, 0
xb, yb = None, None
shoot, Penalty = False, False
p_ticks = 0

ball = Ball(START_X, START_Y, BALL_RADIUS, BALL_COLOR, BALL_OUTLINE_COLOR)
quit = False
BARRIER = 1

try:
    while not quit:
        seconds=(pg.time.get_ticks()-p_ticks)/1000
        if seconds > 1.2: Penalty = False

        cursor_pos = pg.mouse.get_pos()
        line = [(ball.x, ball.y), cursor_pos]
        line_ball_x, line_ball_y = cursor_pos[0] - ball.x, cursor_pos[1] - ball.y

        aline = [(ball.x, ball.y), (ball.x + .015 * SCREEN_WIDTH, ball.y)]

        if not shoot:
            power_display = round(
                distance(line_ball_x, line_ball_y) * POWER_MULTIPLIER / 10)

            angle_display = round(angle(cursor_pos) * 180 / math.pi)

        if shoot:
            if ball.y < SCREEN_HEIGHT:
                if BARRIER < ball.x < SCREEN_WIDTH:
                    time += .3 * SPEED_MULTIPLIER
                    print('\n   time: %ss' % round(time, 2))
                    po = ball.path(x, y, power, ang, time)
                    ball.x, ball.y = po[0], po[1]
                else:
                    print('Out of Bounds!')
                    Penalty = True
                    p_ticks = pg.time.get_ticks()
                    strokes += 1
                    shoot = False
                    if BARRIER < xb < SCREEN_WIDTH:
                        ball.x = xb
                    else:
                        ball.x = START_X
                    ball.y = yb
            else:
                shoot = False
                ball.y = START_Y

        for event in pg.event.get():
            if event.type == pg.QUIT:
                quit = True
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_ESCAPE:
                    quit = True
            if event.type == pg.MOUSEBUTTONDOWN:
                if not shoot:
                    shoot = True
                    x, y = ball.x, ball.y
                    xb, yb = ball.x, ball.y
                    time, power = 0, (
                        distance(line_ball_x, line_ball_y)) * POWER_MULTIPLIER / 10
                    print('\n\nBall Hit!')
                    print('\npower: %sN' % round(power, 2))
                    ang = angle(cursor_pos)
                    print('angle: %s°' % round(ang * 180 / math.pi, 2))
                    print('cos(a): %s' % round(math.cos(ang), 2)), print('sin(a): %s' % round(math.sin(ang), 2))
                    strokes += 1

        draw_window()

    print("\nShutting down...")
    pg.quit()

except Exception as error:
    print(f'A fatal error ({error}) has occurred. The program is shutting down.')
    pg.quit()

Feedback of any kind is very welcome!

\$\endgroup\$
7
\$\begingroup\$

Some of this is nit-pickery, some is more fundamental:

Import Order

PEP-8 suggests an ordering to imports. No reason not to use it:

Imports should be grouped in the following order:

Standard library imports.
Related third party imports.
Local application/library specific imports.

You should put a blank line between each group of imports.

Code Organization: Constants

You have a bunch of "constants" defined. They're all-caps, which is good. They're declared together and at the top of the file, which is good. But they really shouldn't be global constants.

For example, you have a Ball class. Yet there are global constants named BALL_COLOR and BALL_OUTLINE_COLOR and BALL_RADIUS. Why is that? If they're related to your class, make them class constants.

class Ball:
    BODY_COLOR = (255, 255, 255)
    OUTLINE_COLOR = (255, 0, 0)
    RADIUS = 10

Code Organization: Types

In the same vein, you make a lot of use of tuples. But you just create them in-line and rely on convention to access them. Why not go ahead and use a collections.namedtuple or even two?

import collections

Size = collections.namedtuple('Size', 'width height')
Position = collections.namedtuple('Position', 'x y')

WINDOW_SIZE = Size(width=1500, height=800)
START_POS = Position(x=0.5 * WINDOW_SIZE.width, y=0.99 * WINDOW_SIZE.height)

Code Organization: Functions

You have a lot of stuff at module scope. Sooner or later you'll want to either write a unit test, or run the debugger, or load your code into the command-line Python REPL. All of this is made easier if you move the module-scope statements into a main function, or some other-named function.

def main():
    pg.init()
    pg.display.set_caption('Golf')
    ... etc ...

You have a set of font/color variables that you create at module scope. There aren't currently enough drawing functions to create a Window class or anything, but you might consider putting them into a Config class. (And using snake_case names.)

Also, you have a lot of pygame boilerplate mixed in with your game logic. I'd suggest separating the boilerplate into separate functions, something like:

while still_playing:
    handle_events()
    update()
    render()  # You call this "draw_window()" which is fine.

Most of your logic, of course, will be in update(). In fact, since it mostly has to do with updating the position of the Ball object, it should mostly be in a call to ball.update_position(delay) (or some such name).

You make use of a pair of temporaries x and y, but it seems like you could replace those with an old-position attribute on the Ball, or a second Ball object, or something.

\$\endgroup\$
6
\$\begingroup\$

Overall it isn't bad.

Direct imports for common symbols

Based on your discretion, certain often-used and unambiguous symbols can be imported without their module namespace, i.e.

from pg.font import SysFont
# ...
strokeFont = SysFont("monospace", 50)

snake_case

i.e. stroke_font for variables and function names. Also, Penalty should be lower-case because it isn't a class.

debug printing

This kind of thing:

print('     x-pos: %spx' % str(round(dx + x)))

can be improved in a few ways. Firstly, it looks like a debugging output and not actual game content, so typically you won't want to print this at all. That doesn't mean that you have to delete it, though - you can use actual Python logging at level debug to be able to select at the top level whether these statements are printed.

Also: do you really need round? Could you instead go

print(f'    x-pos: {dx + x:.0f}px')

f-strings

As in the previous example, you should consider using the new syntactical sugar of f-strings instead of the % operator.

Global clutter

It's tempting in Python to add a bunch of stuff (x, y, time, power, etc.) to the global scope. Don't give in! Put these into a game state object. Break up your global code into multiple subroutines, potentially in methods of the game state object.

Shadowing

Don't call something time. time is already a thing in Python.

Math

I kind of had to jump through some hoops to take advantage of atan2. I don't recommend doing this, but here's a one-liner alternative to your quadrant function:

return int(4 + 2/pi*atan2(y - ym, xm - x)) % 4 + 1
\$\endgroup\$
  • \$\begingroup\$ Note: f-strings only work in Python 3. Using print(...) could be seen as indication that the OP works in Python 3, but the tags itself don't show what Python version is actually used. \$\endgroup\$ – AlexV Mar 26 at 8:38
  • \$\begingroup\$ I'm using python 3. I use f-strings for sentences with many variables but the %s formatting is just by habit \$\endgroup\$ – alec_a Mar 26 at 8:41
  • \$\begingroup\$ I would disagree on the "shadowing". If we didn't allow to use names that are also standard library modules we wouldn't be able to use these as well: code, warnings, cmd, email, signal, queue, symbol. And there is probably a pypi package for every convenient variable name. I understand that time is a very well known module, but where do we draw the line? My main point is, that OP doesn't even use the module so they don't shadow anything at all. If they did import that module i would totally agree with you. \$\endgroup\$ – Wombatz Mar 26 at 12:12
  • 2
    \$\begingroup\$ An easy place to draw the line is - do what PyCharm does. It has this check built-in. Obviously pip package names are not a problem unless they're in your requirements, but the more common Python library names should be avoided. \$\endgroup\$ – Reinderien Mar 26 at 12:35

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.