3
\$\begingroup\$

I made a Snake game in Python 3 with pygame. Please do critique it as much as possible.

IMPORTANT NOTE: I updated the code at 3:30 3/23

# main.py
# [My Name]
import pygame
from pygame.locals import *
from vector import *
import better_exceptions
import random
import colr
import sys

SCREENSIZE = (1664, 960)
CELLSIZE = 64

assert not SCREENSIZE[0] % CELLSIZE, 'CELLSIZE must be a multiple of SCREENSIZE[0]'
assert not SCREENSIZE[1] % CELLSIZE, 'CELLSIZE must be a multiple of SCREENSIZE[1]'

TITLE = 'Snake'
FPS = 60

LINECOLOR = colr.white()
LINEWIDTH = 2

BGCOLOR = colr.gray()

SNAKECOLOR = colr.green()
SNAKEHEADCOLOR = colr.blue()
SNAKETICKRATE = 200  # Move the snake once every _ milliseconds
SNAKEGROWRATE = 1  # Amount of places to grow for each food eaten

SCREEN = pygame.display.set_mode(SCREENSIZE)

UP = (0, -1)
RIGHT = (1, 0)
LEFT = (-1, 0)
DOWN = (0, 1)


class Snake(object):
    def __init__(self, headPos, initSize):
        self.spots = [headPos]
        self.direction = UP
        self.currentDirection = self.direction
        self.timer = 0
        self.tickrate = SNAKETICKRATE
        self.headColor = SNAKEHEADCOLOR
        self.bodyColor = SNAKECOLOR
        self.growRate = SNAKEGROWRATE
        self.queue = 1
        self.grow(initSize)

    @property
    def head(self):
        return self.spots[-1]

    @staticmethod
    def getWorldCoords(spot):
        spotWorld = [spot[i] for i in range(2)]
        return spotWorld

    def lose(self):
        leave()

    def isOutsideMap(self, spot=None):
        spot = self.getWorldCoords(spot)
        # Check only one spot
        for i in (0, 1):
            if not 0 <= spot[i] < SCREENSIZE[i]:
                return True
        return False

    def isCollidingWithSelf(self, spot):
        # Check only one spot
        if spot in self.spots:
            return True
        return False

    def redir(self, direction):
        if not tuple((-direction[i] for i in range(2))
                     ) == tuple(self.currentDirection):
            self.direction = direction

    def move(self, keepOld=False):
        newSpot = [self.head[i] + self.direction[i] for i in range(2)]
        self.currentDirection = self.direction
        if self.isOutsideMap(newSpot) or self.isCollidingWithSelf(newSpot):
            self.lose()
        else:
            self.spots.append(newSpot)
            if not keepOld:
                del self.spots[0]

    def smartMove(self):
        if 0 < self.queue:
            self.move(True)
            self.queue -= 1
        else:
            self.move()
        if tuple(self.head) == tuple(foodSpawner.food):
            self.grow(self.growRate)
            foodSpawner.eat()

    def grow(self, amnt=1):
        self.queue += amnt

    def draw(self, deltaTime):
        self.timer += deltaTime
        if self.tickrate < self.timer:
            self.smartMove()
            self.timer = 0
        for spot in self.spots:
            color = self.bodyColor
            if tuple(spot) == tuple(self.head):
                color = self.headColor
            drawCellAtLocation(spot, color)


class FoodSpawner(object):
    def __init__(self, noFoodCheck=lambda: []):
        self.noFoodCheck = noFoodCheck
        self.food = self.genPos()
        self.color = colr.orange()

    def genPos(self):
        return random.choice(list(filter(lambda x: x not in map(tuple, self.noFoodCheck()), allCells())))

    def eat(self):
        self.food = self.genPos()

    def draw(self):
        drawCellAtLocation(self.food, self.color)


def allCells():
    return [(x, y) for y in range(SCREENSIZE[1] // CELLSIZE)
            for x in range(SCREENSIZE[0] // CELLSIZE)]


def drawCellAtLocation(spot, color=None):
    color = color or colr.white()
    spotRect = pygame.Rect([spot[i] * CELLSIZE + LINEWIDTH for i in range(2)],
                           [CELLSIZE - LINEWIDTH for _ in range(2)])
    pygame.draw.rect(SCREEN, color, spotRect)


def leave():
    sys.exit()


def draw(deltaTime):
    SCREEN.fill(BGCOLOR)
    drawGrid()
    snake.draw(deltaTime)
    foodSpawner.draw()


keybindings = {
    K_UP: lambda: snake.redir(UP),
    K_RIGHT: lambda: snake.redir(RIGHT),
    K_LEFT: lambda: snake.redir(LEFT),
    K_DOWN: lambda: snake.redir(DOWN),
}


def event(ev):
    if ev.type == KEYDOWN:
        try:
            keybindings[ev.key]()
        except KeyError:
            pass


def drawGrid():
    for x in range(0, SCREENSIZE[0], CELLSIZE):
        pygame.draw.line(SCREEN, LINECOLOR, (x, 0),
                         (x, SCREENSIZE[1]), LINEWIDTH)
    for y in range(0, SCREENSIZE[1], CELLSIZE):
        pygame.draw.line(SCREEN, LINECOLOR, (0, y),
                         (SCREENSIZE[0], y), LINEWIDTH)


fonts = {}


def getFont(self, size):
    if size not in fonts:
        fonts[size] = pygame.font.Font(None, size)
    return fonts[size]


texts = {}


def getText(self, text, size=12, color=None):
    color = color or colr.black()
    if (text, size, color) not in texts:
        fonts[(text, size, color)] = getFont(size).render(text, True, color)
    return fonts[(text, size, color)]


def roundedRect(surface, rect, color, radius=0.4):
    """ Draw a rounded rectangle """
    rect = pygame.Rect(rect)
    color = pygame.Color(*color)
    alpha = color.a
    color.a = 0
    pos = rect.topleft
    rect.topleft = 0, 0
    rectangle = pygame.Surface(rect.size, SRCALPHA)

    circle = pygame.Surface([min(rect.size) * 3] * 2, SRCALPHA)
    pygame.draw.ellipse(circle, (0, 0, 0), circle.get_rect(), 0)
    circle = pygame.transform.smoothscale(
        circle, [int(min(rect.size) * radius)] * 2)

    radius = rectangle.blit(circle, (0, 0))
    radius.bottomright = rect.bottomright
    rectangle.blit(circle, radius)
    radius.topright = rect.topright
    rectangle.blit(circle, radius)
    radius.bottomleft = rect.bottomleft
    rectangle.blit(circle, radius)

    rectangle.fill((0, 0, 0), rect.inflate(-radius.w, 0))
    rectangle.fill((0, 0, 0), rect.inflate(0, -radius.h))

    rectangle.fill(color, special_flags=BLEND_RGBA_MAX)
    rectangle.fill((255, 255, 255, alpha), special_flags=BLEND_RGBA_MIN)

    return surface.blit(rectangle, pos)


snake = Snake((5, 5), 500)
foodSpawner = FoodSpawner(lambda: snake.spots)

clock = pygame.time.Clock()
while True:
    deltaTime = clock.tick(FPS)

    draw(deltaTime)

    for ev in pygame.event.get():
        if ev.type == QUIT:
            leave()
        else:
            event(ev)

    pygame.display.update()
    pygame.display.flip()
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Nice game. Here are some nits and tips:

  1. For readability and clarity, put each of your classes in a separate file and import them from a runner script or game script.

  2. Why the need to do this?

    def getWorldCoords(spot):
        spotWorld = [spot[i] for i in range(2)]
        return spotWorld
    

    You can write it like so:

    def getWorldCoords(spot):
        return spot[:2]
    

    Be aware that if you are modifying the objects in the list, you should use the copy to make a deep copy of them if they are not primitives.

  3. Try to make your code less verbose.

    def isCollidingWithSelf(self, spot):
        return spot in self.spots
    
  4. Unneeded list comprehensions.

    if not tuple((-direction[i] for i in range(2))
                 ) == tuple(self.currentDirection):
        self.direction = direction
    

    This is extremely verbose and difficult to read.

    if (-direction[0], -direction[1]) != tuple(self.currentDirection):
        self.direction = direction
    
  5. Avoid using sys.exit(). Try to have your program exit normally. You can just set a condition in your while loop.

  6. Comments should not state the obvious. Use comments to document things like complex algorithms.

    def roundedRect(surface, rect, color, radius=0.4):
        """ Draw a rounded rectangle """
    

    This comment doesn't really add much value.

  7. Enclose your main code using an if __name__ == '__main__': guard. Avoid putting it in the global scope.

Happy coding!

\$\endgroup\$

Your Answer

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

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