Python Forum
[PyGame] Made my first Python program: Snake. Please help me improve it
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Made my first Python program: Snake. Please help me improve it
#1
Hello,

I've started learning Python yesterday with the intention of studying machine learning. Before this, my experience with programming was exclusive to my first semester where I had a C course.

I decided to work towards a final objective: have an AI learn how to win the snake game. For that goal, I had to make the actual game. I didn't want to copy an already made game as I'm still learning. So with that in mind, I spent an entire night building my little snake game.

I would like the game to be as good as possible before I started the machine learning part, which is why I'm posting it here. My biggest problem right now is controlling the speed of the game. I achieved this by limiting the FPS number based on the current score but it seems like a cheap way to do it.

I divided the game in two files: vars.py where I define most variables and functions and snake.py with the actual game and a few other things. This is the way I was taught to program so if it's wrong or not done please feel free to point that out.

My code is inside the spoiler tags. I have no idea how to attach files here.

vars.py
snake.py
Reply
#2
You have a few bad habits that i see. Ill start with those.

first of all vars is a bad name to name a module. You are overwriting the built-in vars. Whether you use it or not, its not a good habit to get into as it can cause you a lot of headaches later.

You should only ever have one statement in your entire game of pygame.display.update(). By having more than one i know your structure is fragmented. Your trying to make two game states (playing and end screen with score) incorrectly. The proper (or better) way is to use classes and inheritance. More info here.

You are not using pygame's built-in Rect. Which is a simple way to handle collision (such as collision with snake and food). Python's 3rd party libraries have methods and built-in ways to handle things. This is to create simplicity in code, so others can easily read it. But it also helps as the 3rd party libraries perfect the code to make it fast and correct. Instead of checking the distance between the two, i would just check for rect collision.

(Feb-17-2019, 05:29 PM)andrerocha1998 Wrote: I achieved this by limiting the FPS number based on the current score but it seems like a cheap way to do it.
Yes you are right. It should be set at 60 for almost any game and should not change. You can move an object by making it jump X number of pixels or use delta time. More described here.

For changing the speed based on how long the snake is you could limit the snake movement with a delay
timer = 0
delay = 50
...

    if pygame.time.get_ticks()-timer > delay:
        timer = pygame.time.get_ticks()
        snake.move(eat)
The shorter delay the faster the snake moves. You can decrease delay every time the snake gets longer.

However then you come upon another problem. A function should only ever do one thing. So snake.move() should only move, not move and eat, not move and change its size, just move.

Also you might want to set a large delay in the beginning because sometimes the snake runs off the screen at the start of the game giving no chance of success to the user.
Recommended Tutorials:
Reply
#3
Thanks for the response!

So, after simplifying the logic some more, I scrapped the vars.py file and kept it all in the same place. After also posting this in stackechange, a reply prompted me to change how I handled the keys. Rather than strings, I started using a list with 2 elements. It could be a tuple, but I don't really like those much.

After reading your reply @metulburr, I learned how to properly change game states with the control class. I decided that since I didn't really need the end screen, I could just close and print the score in the console.

What is considered spaghetti code by the way? I had to study your functions for a while to be able to understand what was going on. I know with practice it will become much easier, but I tried coding the same way I thought. Is that the problem, that since was the case it might be disorganized for others?

I also changed the speed of the game using the delay. Moving it with pixels instead wouldn't work the intended way, from what I understood. I only want it to move multiples of sqr_size and moving it by x,y would depend on the time, right?

I also check the Rect functions. My understanding is that for the Rect collision functions to work, snake.body should be a string of Rects, instead of center.points. This would mean I'd have to change the whole generation and movement functions.

I ended up with this:

import pygame, sys, random, copy

#Initialising variables
lost = False
eat = False
key1 = [0, -1]
timer = 0
width = 800
height = 600
BG = 60, 60, 60
FOOD_C = 200, 0, 0
BODY_C = 255, 255, 255
sqr_size = 20
SPEED = sqr_size

#Define functions
def dist(a, b):
    return ((b.pos[0] - a.pos[0])**2 + (b.pos[1] - a.pos[1])**2)

def loser(snake, food): #Check if lost the game
    if snake.pos[0]<sqr_size or snake.pos[0]>width-sqr_size or snake.pos[1]<sqr_size or snake.pos[1]>height-sqr_size:
        return True
    for i in snake.body[1:]:
        if i == snake.pos:
            return True          

def delay(snake):
    if 125 - snake.score() > 35:
        return 125-snake.score()
    else:
        return 35

def whatkey(event, key):
    if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                return [-1, 0]
            if event.key == pygame.K_RIGHT:
                return [1, 0]
            if event.key == pygame.K_UP:
                return [0, -1]
            if event.key == pygame.K_DOWN:
                return [0, 1]
    return key
            
#Define classes
class Snake(object):
    def __init__(self):
        self.pos = [random.randint(1, (width-sqr_size)/sqr_size)*sqr_size,
                    random.randint(10, (height-sqr_size)/sqr_size)*sqr_size]
        self.mov = [0, -1]
        self.body = [self.pos[:]]

    def score(self):
        return len(self.body)

    def move(self, key): #Snake movement
        if key[0] + self.mov[0] != 0 and key[1] + self.mov[1] != 0:
            self.mov = key
        self.pos[0] += self.mov[0]*SPEED
        self.pos[1] += self.mov[1]*SPEED
        self.body.insert(0, self.pos[:])
       
class Food(object):
    def __init__(self):
        self.pos = [random.randint(1, (width-sqr_size)/sqr_size)*sqr_size,
                    random.randint(1, (height-sqr_size)/sqr_size)*sqr_size]

#Game setup
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((width,height))
snake = Snake()
food = Food()
screen.fill(BG)

#Game Loop
while not lost:
    #Event loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT: 
            lost = True
        key1 = whatkey(event, key1)
 
    #Logic loop
    if pygame.time.get_ticks()-timer > delay(snake):
        timer = pygame.time.get_ticks()  
        snake.move(key1)
        eat = dist(snake, food) < sqr_size**2
        if eat:
            food = Food()
        else:    
            snake.body.pop()
        lost = loser(snake, food)

    #Screen drawings
    screen.fill(BG)
    for i in snake.body:
        pygame.draw.rect(screen, BODY_C, (i[0], i[1], sqr_size, sqr_size), 0)
    pygame.draw.rect(screen, FOOD_C, (food.pos[0], food.pos[1], sqr_size, sqr_size), 0)
    pygame.display.set_caption("Snake. Your score is: {}".format(snake.score()))
    pygame.display.update()
    clock.tick(60)            

print("Your score is: {}".format(snake.score()))
sys.exit()
The controls seem unresponsive at times because of the delay, however. This didn't happen when I controlled the speed via FPS. Any way to fix this?

On an unrelated note, from the documentation you linked, sometimes classes are defined with (object), (something) or (). (something) I understand is to make a subclass from something. However, I was under the impression that (object) and () worked the same way.
Reply
#4
(Feb-18-2019, 09:14 PM)andrerocha1998 Wrote: I also check the Rect functions. My understanding is that for the Rect collision functions to work, snake.body should be a string of Rects, instead of center.points. This would mean I'd have to change the whole generation and movement functions.
Rect have center attributes.
Quote:x,y
top, left, bottom, right
topleft, bottomleft, topright, bottomright
midtop, midleft, midbottom, midright
center, centerx, centery
size, width, height
w,h
You wouldnt really have to do it for food as the head of the snake would be the only thing colliding first with food. So you would only have to worry about the snake intersecting on itself. Ive never made a snake game, so i am not sure how the best approach would be. If its a small game, the way it works is good enough.

(Feb-18-2019, 09:14 PM)andrerocha1998 Wrote: The controls seem unresponsive at times because of the delay, however. This didn't happen when I controlled the speed via FPS. Any way to fix this?
I do not know what you mean? When i run it it seems fine. Can you explain?

(Feb-18-2019, 09:14 PM)andrerocha1998 Wrote: On an unrelated note, from the documentation you linked, sometimes classes are defined with (object), (something) or (). (something) I understand is to make a subclass from something. However, I was under the impression that (object) and () worked the same way.
object is for python2.x to use new classes, while python3.x automatically uses new classes. Python2.x is almost dead now anyways, so there is not much point anymore except for habit. Anything within these parenthesis is that class' super class. In the example of making a state machine...States is the super class and Menu and Game are the sub classes. All of States attributes are passes to each subclass. And anything that needs to remain between states can be in the super class as well. There is more detailed info on multiple parts class tutorials here.

(Feb-18-2019, 09:14 PM)andrerocha1998 Wrote: What is considered spaghetti code by the way?
Your remake is a little cleaner. It drove me nuts that you had 3 separate functions to handle movement in your first code.
Recommended Tutorials:
Reply
#5
Just saw this. I would only put movement within this if condition. You only want to limit movement, not eating or changing it size. This could be a result in bugs.

    if pygame.time.get_ticks()-timer > delay(snake):
        timer = pygame.time.get_ticks()  
        snake.move(key1)
        eat = dist(snake, food) < sqr_size**2
        if eat:
            food = Food()
        else:    
            snake.body.pop()
        lost = loser(snake, food)
Recommended Tutorials:
Reply
#6
(Feb-18-2019, 10:53 PM)metulburr Wrote: I do not know what you mean? When i run it it seems fine. Can you explain?
It only moves in 1 cicle out of delay+1. If you press the direction keys only during cycles when the snake doesn't move, the snake won't turn. That isn't usually a problem except for when you want 180ยบ turns like UP->LEFT->DOWN with no space in between. In the beginning levels it's quite hard to achieve.

(Feb-18-2019, 11:03 PM)metulburr Wrote: I would only put movement within this if condition. You only want to limit movement, not eating or changing it size.
Can't do that or eventually you'll start popping from an empty list.
Reply
#7
(Feb-18-2019, 11:15 PM)andrerocha1998 Wrote: Can't do that or eventually you'll start popping from an empty list.
ah i see now how you are moving it. That might not work then or might have to be re-moduled.

I havent used pygame in years and havent written a game in a long time. I dont remember a whole lot. I would normally at this point rewrite your game to work, but i dont have time. I probably would have to read my own tutorials at this point. I would say you can get current pygame users on reddit. Aside form that, the only users i know on this forum that use pygame; and are know what they are talking about; are @[Mekire] and @[Windspar] for pygame structure.
Recommended Tutorials:
Reply
#8
You might get some ideas off of this code.
Still small bug. Push key direction to fast will collide with own body.
import pygame
from random import randint, choice


pygame.init()
SQR_SIZE = 20

# Simple Tick Timer
class Timer:
    ticks = 0

    def __init__(self, interval, callback):
        self.tick = Timer.ticks + interval
        self.interval = interval
        self.callback = callback

    def elapse(self):
        trip = False

        while Timer.ticks > self.tick:
            self.tick += self.interval
            trip = True

        if trip:
            self.callback(self)

# simple interface
class SceneInterface:
    def on_draw(self, surface):
        pass

    def on_event(self, event):
        pass

    def on_quit(self, game):
        game.running = False

    def on_update(self):
        pass

class Game:
    def __init__(self):
        # Basic pygame setup
        pygame.display.set_caption('Snake Example')
        self.rect = pygame.Rect(0, 0, 800, 600)
        self.surface = pygame.display.set_mode(self.rect.size)
        self.clock = pygame.time.Clock()

        # scene basic
        self.flip_scene = None

    def mainloop(self, start_scene):
        self.delta = 0
        self.running = True
        self.scene = start_scene
        while self.running:
            if self.flip_scene:
                self.scene = self.flip_scene
                self.flip_scene = None

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.scene.on_quit(self)
                else:
                    self.scene.on_event(event)

            Timer.ticks = pygame.time.get_ticks()

            self.scene.on_update()
            self.scene.on_draw(self.surface)
            pygame.display.update()
            self.delta = self.clock.tick(60)

class Snake:
    def __init__(self, rect, eats, dies):
        self.head = pygame.Rect(*rect.center, SQR_SIZE, SQR_SIZE)
        self.body = [tuple(self.head.topleft) for x in range(3)]
        self.direction = choice([pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT])
        self.color = pygame.Color('Lawngreen')
        self.timer = Timer(200, self.moving)
        self.bounds = rect.copy()
        self.eaten = eats
        self.dies = dies

    def draw(self, surface):
        for body in self.body:
            rect = pygame.Rect(body[0], body[1], SQR_SIZE, SQR_SIZE)
            surface.fill(self.color, rect)

    def update(self):
        self.timer.elapse()

    def move(self, x, y):
        pos = self.body[0]
        self.head.topleft = pos[0] + x * SQR_SIZE, pos[1] + y * SQR_SIZE
        if self.head.topleft in self.body:
            self.dies()
        elif not self.bounds.contains(self.head):
            self.dies()
        else:
            if self.eaten(self.head.topleft):
                self.body = [self.head.topleft] + self.body
            else:
                self.body = [self.head.topleft] + self.body[:-1]

    def moving(self, timer):
        if self.direction == pygame.K_UP:
            self.move(0, -1)
        elif self.direction == pygame.K_DOWN:
            self.move(0, 1)
        elif self.direction == pygame.K_LEFT:
            self.move(-1, 0)
        elif self.direction == pygame.K_RIGHT:
            self.move(1, 0)

class DeathScene(SceneInterface):
    def __init__(self, game, play):
        font = pygame.font.Font(None, 32)
        self.death = font.render('You Have Died', 1, pygame.Color('Firebrick'))
        self.rect = self.death.get_rect()
        self.rect.center = game.rect.center
        self.play = play
        self.game = game

    def on_draw(self, surface):
        self.play.on_draw(surface)
        surface.blit(self.death, self.rect)

    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                self.play.new_game()
                self.game.flip_scene = self.play
            elif event.key == pygame.K_ESCAPE:
                self.on_quit(self.game)

class PlayScene(SceneInterface):
    def __init__(self, game):
        self.game = game
        self.new_game()
        self.food_image = pygame.Surface((SQR_SIZE, SQR_SIZE))
        self.food_image.fill(pygame.Color('Yellow'))

    def new_game(self):
        self.snake = Snake(self.game.rect, self.snake_eats, self.snake_dies)
        self.create_food()

    def create_food(self):
        g = self.game.rect
        pos = randint(0, g.w / SQR_SIZE - 1), randint(0, g.h / SQR_SIZE - 1)
        while pos in self.snake.body:
            pos = randint(0, g.w / SQR_SIZE), randint(0, g.h / SQR_SIZE)

        self.food = pos[0] * SQR_SIZE, pos[1] * SQR_SIZE

    def snake_eats(self, head):
        if self.food == head:
            self.create_food()
            return True
        return False

    def snake_dies(self):
        self.game.flip_scene = DeathScene(self.game, self)

    def on_draw(self, surface):
        surface.fill(pygame.Color('Black'))
        self.snake.draw(surface)
        surface.blit(self.food_image, self.food)

    def on_update(self):
        self.snake.update()

    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                if self.snake.direction != pygame.K_DOWN:
                    self.snake.direction = event.key
            elif event.key == pygame.K_DOWN:
                if self.snake.direction != pygame.K_UP:
                    self.snake.direction = event.key
            elif event.key == pygame.K_LEFT:
                if self.snake.direction != pygame.K_RIGHT:
                    self.snake.direction = event.key
            elif event.key == pygame.K_RIGHT:
                if self.snake.direction != pygame.K_LEFT:
                    self.snake.direction = event.key
            elif event.key == pygame.K_ESCAPE:
                self.on_quit(self.game)

def main():
    game = Game()
    play_scene = PlayScene(game)
    game.mainloop(play_scene)
    pygame.quit()

if __name__ == '__main__':
    main()
99 percent of computer problems exists between chair and keyboard.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  how to add segments to the snake body blacklight 1 2,822 Sep-13-2023, 07:33 AM
Last Post: angelabarrios
  [PyGame] Snake game: how to get an instance for my snake's body multiple times? hajebazil 2 2,113 Jan-30-2022, 04:58 AM
Last Post: hajebazil
  help with snake game blacklight 3 2,551 Jul-30-2020, 01:13 AM
Last Post: nilamo
  Snake Game - obstacle problem Samira 3 5,424 Oct-31-2019, 02:58 PM
Last Post: Samira
  Creating Snake game in Turtle Shadower 1 8,617 Feb-11-2019, 07:00 PM
Last Post: woooee
  [PyGame] Basic Snake game (using OOP) PyAlex 1 12,445 Sep-10-2018, 09:02 PM
Last Post: Mekire
  [PyGame] Snake not changing directions in Snake Game Bjdamaster 4 4,853 Aug-13-2018, 05:09 AM
Last Post: Bjdamaster
  [PyGame] Snake controls not working jakegold98 5 6,297 Dec-12-2017, 01:45 AM
Last Post: Windspar

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020