3

I'm a fairly newe programmer and this is the first time I develop a game and I wanted to start with something pretty simple, so I chose the snake game. I have coded everything apart from adding the body part when the food is eaten.

import random
import pygame
from pygame import *
import sys
import os
import time

###objects
class snake:
        def __init__(self, win):
                self.score = 1
                self.length = 25
                self.width = 25
                self.win = win
                self.r = random.randint(0,500)
                self.vel = 25
                self.update = pygame.display.update()
                self.right = True
                self.left = False
                self.up = False
                self.down = False
                # 0 = right     1 = left        2 = up          3 = down
                self.can = [True, False, True, True]
                self.keys = pygame.key.get_pressed()

                while True:
                        if self.r % 25 == 0:
                                break
                        else:
                                self.r = random.randint(0,500)
                                continue
                self.x = self.r
                self.y = self.r

                self.r = random.randint(0,500)
                while True:
                        if self.r % 25 == 0:
                                break
                        else:
                                self.r = random.randint(0,500)
                                continue
                self.a = self.r
                self.b = self.r


        def move(self, win):
                win.fill((0,0,0))
                self.keys = pygame.key.get_pressed()

                if self.right == True:
                        self.x += self.vel
                if self.left == True:
                        self.x -= self.vel
                if self.up == True:
                        self.y -= self.vel
                if self.down == True:
                        self.y += self.vel

                if self.x > 475:
                        self.x = 0
                if self.x < 0:
                        self.x = 500
                if self.y > 475:
                        self.y = 0
                if self.y < 0:
                        self.y = 500

                if self.keys[pygame.K_RIGHT] and self.can[0] == True:
                        self.right = True
                        self.left= False
                        self.up = False
                        self.down = False
                        self.can[1] = False
                        self.can[0] = True
                        self.can[2] = True
                        self.can[3] = True

                if self.keys[pygame.K_LEFT] and self.can[1] == True:
                        self.right = False
                        self.left = True
                        self.up = False
                        self.down = False
                        self.can[0] = False
                        self.can[1] = True
                        self.can[2] = True
                        self.can[3] = True

                if self.keys[pygame.K_UP] and self.can[2] == True:
                        self.right = False
                        self.left = False
                        self.up = True
                        self.down = False
                        self.can[3] = False
                        self.can[0] = True
                        self.can[1] = True
                        self.can[2] = True

                if self.keys[pygame.K_DOWN] and self.can[3] == True:
                        self.right = False
                        self.left = False
                        self.up = False
                        self.down = True
                        self.can[2] = False
                        self.can[0] = True
                        self.can[1] = True
                        self.can[3] = True

                self.length = 25 * self.score        
                self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, self.length, self.width))

        def food(self, win):
                pygame.draw.rect(self.win, (255,0,0), (self.a, self.b,25,25))

                if self.a == self.x and self.b == self.y:
                        self.r = random.randint(0,500)
                        while True:
                                if self.r % 25 == 0:
                                        break
                                else:
                                        self.r = random.randint(0,500)
                                        continue
                        self.a = self.r
                        self.b = self.r
                        self.score += 1



###functions



###main game    

##variables
screen = (500,500)
W = 25
L = 25
WHITE = 255,255,255
clock = pygame.time.Clock()

##game
pygame.init()
win = pygame.display.set_mode(screen)
title = pygame.display.set_caption("snake game")
update = pygame.display.update()
snake = snake(win)

run = True
while run:
        clock.tick(10)
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                        run = False
        snake.move(win)
        snake.food(win)
        pygame.display.update()


pygame.quit()

I know the code is a bit messy because I wanted to try to implement OOP, since I never used it. This is also my first time using pygame, so I be doing something wrong.

So far I have made it so that the snake and food spawn in a random location in an invible grid, and when the head of the snake has the same coordinates of the food, the snake becomes longer (I'm just adding 25 pixels to the snake's body, but when it turns, the whole rectangular shaped snake turns). Also, if the snake reaches the edge of the display, the appears from the opposite side.

shmifful
  • 55
  • 6
  • I know this library isn't what you're using, but take a look at the direction-detection and moving in this post: https://stackoverflow.com/questions/61837338/how-to-make-an-image-move-through-a-pyglet-window as I think you'll find it useful to minimize your code. And I don't think it's to complicated for you to grasp. It's just a note and has nothing to do with solving your original issue in this post, would just make your code a lot smaller. – Torxed May 25 '20 at 14:09

3 Answers3

4

The comments below might sound harsh, and I've tried to write them in a neutral way simply pointing out facts and state them as they are. If you are truly a new programmer, this is a pretty good project to learn from and you've done quite good to come this far. So keep an mind that these comments are not meant to be mean, but objective and always comes with a proposed solution to make you an even better programmer, not to bash you.

I also won't go into detail in the whole list as a body thing, others have covered it but I'll use it also in this code.


Here's the result, and blow is the code and a bunch of pointers and tips.

enter image description here


Never re-use variables

First of all, never re-use variable names, as you've overwritten and got lucky with snake = snake() which replaces the whole snake class, and can thus never be re-used again, defeating the whole purpose of OOP and classes. But since you only use it once, it accidentally worked out ok this time. Just keep that in mind for future projects.

Single letter variables

Secondly, I would strongly avoid using single-letter variables unless you really know what you're doing and often that's tied to a math equation or something. I'm quite allergic to the whole concept of self.a and self.b as they don't say anything meaningful, and in a few iterations you probably won't have an idea of what they do either. This is common tho when you're moving quickly and you currently have a grasp on your code - but will bite you in the ass sooner or later (will/should give you bad grades in school or won't land you that dream job you're applying for).

Never mix logic in one function

You've also bundled the food into the player object, which is a big no-no. As well as render logic in the movement logic. So I propose a re-work in the shape of even more OOP where food and player are two separate entities and a function for each logical operation (render, move, eat, etc..).

So I restructured it into this logic:

enter image description here

While I'm at it, I also re-worked the movement mechanics a bit, to use less lines and logic to produce the same thing. I also removed all this logic:

self.r = random.randint(0,500)
while True:
        if self.r % 25 == 0:
            break
        else:
            self.r = random.randint(0,500)
            continue

And replaced it with this, which does the exact same thing, but uses built-ins to produce it. And hopefully the functions/variables are more descriptive than a rogue while loop.

self.r = random.choice(range(0, 500, 25))

And the final result would look something like this:

import random
import pygame
from pygame import *
import sys
import os
import time

# Constants (Used for bitwise operations - https://www.tutorialspoint.com/python/bitwise_operators_example.htm)
UP    = 0b0001
DOWN  = 0b0010
LEFT  = 0b0100
RIGHT = 0b1000

###objects
class Food:
    def __init__(self, window, x=None, y=None):
        self.window = window
        self.width = 25
        self.height = 25
        self.x, self.y = x, y
        if not x or not y: self.new_position()

    def draw(self):
        pygame.draw.rect(self.window, (255,0,0), (self.x, self.y, 25, 25))

    def new_position(self):
        self.x, self.y = random.choice(range(0, 500, 25)), random.choice(range(0, 500, 25))

class Snake:
    def __init__(self, window):
        self.width = 25
        self.width = 25
        self.height = 25
        self.window = window
        self.vel = 25
        self.update = pygame.display.update()

        start_position = random.choice(range(0, 500, 25)), random.choice(range(0, 500, 25))
        self.body = [start_position]
        self.direction = RIGHT

    def move(self, window):
        self.keys = pygame.key.get_pressed()
        # since key-presses are always 1 or 0, we can multiply each key with their respective value from the
        # static map above, LEFT = 4 in binary, so if we multiply 4*1|0 we'll get binary 0100 if it's pressed.
        # We can always safely combine 1, 2, 4 and 8 as they will never collide and thus always create a truth map of
        # which direction in bitwise friendly representation.
        if any((self.keys[pygame.K_UP], self.keys[pygame.K_DOWN], self.keys[pygame.K_LEFT], self.keys[pygame.K_RIGHT])):
            self.direction = self.keys[pygame.K_UP]*1 + self.keys[pygame.K_DOWN]*2 + self.keys[pygame.K_LEFT]*4 + self.keys[pygame.K_RIGHT]*8

        x, y = self.body[0] # Get the head position, which is always the first in the "history" aka body.
        self.body.pop() # Remove the last object from history

        # Use modolus to "loop around" when you hit 500 (or the max width/height desired)
        # as it will wrap around to 0, try for instance 502 % 500 and it should return "2".
        if self.direction & UP:
            y = (y - self.vel)%500
        elif self.direction & DOWN:
            y = (y + self.vel)%500
        elif self.direction & LEFT:
            x = (x - self.vel)%500
        elif self.direction & RIGHT:
            x = (x + self.vel)%500 # window.width
        self.body.insert(0, (x, y))

    def eat(self, food):
        x, y = self.body[0] # The head
        if x >= food.x and x+self.width <= food.x+food.width:
            if y >= food.y and y+self.height <= food.y+food.height:
                self.body.append(self.body[-1])
                return True
        return False

    def draw(self):
        for x, y in self.body:
            pygame.draw.rect(self.window, (0,255,0), (x, y, self.width, self.width))

##variables
clock = pygame.time.Clock()

##game
pygame.init()
window = pygame.display.set_mode((500,500))
pygame.display.set_caption("snake game")

snake = Snake(window)
food = Food(window)
food.new_position()

score = 0

run = True
while run:
    clock.tick(10)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    window.fill((0,0,0)) # Move the render logic OUTSIDE of the player object
    snake.move(window)
    if snake.eat(food):
        score += 1
        food.new_position()

    snake.draw()
    food.draw()
    pygame.display.update()

pygame.quit()
  • draw() now handles all rendering logic within the objects themselves, instead of being entangled in the move().

  • snake.eat() is now a function that returns True or False based on the snake head (first position in history, aka body) being inside a food object. This function also adds to the body if a eat was successful, perhaps this code should be moved outside as well, but it's one line of code so I skipped on my own rule a bit to keep the code simple.

  • food.new_position() is a function that simply moves the food to a new position, called when eat() was successful for instance, or if you want to randomly move the food around at a given interval.

  • move() and finally the move function, which only has one purpose now, and that is to move the snake in a certain direction. It does so by first getting the current head position, then remove the last history item (tail moves with the head) and then adds a new position at the front of the body that is equal to velocity.

The "is inside" logic might look like porridge, but it's quite simple, and the logic is this:

enter image description here

If the snakes head body[0] has it's x greater or equal to the foods x, it means the heads lower upper left corner was at least past or equal to the foods upper left corner. If the heads width (x+width) is less or equal to the foods width, we're at least inside on the X axis. And then we just repeat for the Y axis and that will tell you if the head is inside or outside the boundary of the food.


The movement logic is reworked to make it fractionally faster but also less code and hopefully easier to use once you have a understanding of how it works. I switched to something called bitwise operations. The basic concept is that you can on a "machine level" (bits) do quick operations to determinate if something is true or not with AND operations for instance. To do this, you can compare to bit-sequences and see if at any point two 1 overlap each other, if not, it's False. Here's an overview of the logic used and all the possible combinations of UP, DOWN, LEFT and RIGHT in binary representation:

enter image description here

On a bit level, 1 is simply 0001, 2 would be 0010 and 4 being 0100 and finally 8 being 1000. Knowing this, if we press (right) we want to convert this into the bit representation that is the static variable RIGHT (1000 in binary). To achieve this, we simply multiply the value pygame gives us when a key is pressed, which is 1. We multiply it by the decimal version of 1000 (RIGHT), which is 8.

So if is pressed we do 8*1. Which gives us 1000. And we simply repeat this process for all the keys. If we pressed + it would result in 1001 because 8*1 + 1*1 and since and weren't pressed, they will become 4*0 and 2*0 resulting in two zeroes at binary positions.

We can then use these binary representations by doing the AND operator shown in the picture above, to determinate if a certain direction was pressed or not, as DOWN will only be True if there's a 1 on the DOWN position, being the second number from the right in this case. Any other binary positional number will result in False in the AND comparitor.

This is quite efficient, and once you get the hang of it - it's pretty useful for other things as well. So it's a good time to learn it in a controlled environment where it hopefully makes sense.


The main thing to take away here (other than what other people have already pointed out, keep the tail in a array/list as a sort of history of positions) is that game objects should be individual objects, and main rendering logic shouldn't be in player objects, only player render specifics should be in the player object (as an example).

And actions such as eat() should be a thing rather than being checked inside the function that handles move(), render() and other things.

And my suggestions are just suggestions. I'm not a game developer by trade, just optimizing things where I can. Hope the concepts come to use or spark an idea or two. Best of luck.

Torxed
  • 22,866
  • 14
  • 82
  • 131
  • thanks you so much, i didnt have the energy to read all because i didnt understand much of what you said, i read up to the three last paragraphs. anyways, thank you so much, this will certainly help me in the future!!! – shmifful May 27 '20 at 18:26
3

Yo have to mange the body of the snake in a list. Add the current position of the the head at the head of the body list and remove an element at the tail of the list in ever frame.

Add an attribute self.body:

class snake:
        def __init__(self, win):
                # [...]

                self.body = [] # list of body elements

Add the current head to the bode before the head is moved:

class snake:
        # [...]

        def move(self, win):
                # [...]

                # move snake
                self.body.insert(0, (self.x, self.y))

Remove elements a the end of self.body, as long the length of the snake exceeds the score:

class snake:
        # [...]

        def move(self, win):
                # [...]

                # remove element at end
                while len(self.body) >= self.score:
                    del self.body[-1]

Draw the bode of the snake in a loop:

class snake:
        # [...]

        def move(self, win):
                # [...]

                # draw smake and body
                self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, 25, self.width))
                for pos in self.body:
                    pygame.draw.rect(self.win, (0,255,0), (pos[0], pos[1], 25, self.width))

class snake:

class snake:
        def __init__(self, win):
                self.score = 1
                self.length = 25
                self.width = 25
                self.win = win
                self.r = random.randint(0,500)
                self.vel = 25
                self.update = pygame.display.update()
                self.right = True
                self.left = False
                self.up = False
                self.down = False
                # 0 = right     1 = left        2 = up          3 = down
                self.can = [True, False, True, True]
                self.keys = pygame.key.get_pressed()

                while True:
                        if self.r % 25 == 0:
                                break
                        else:
                                self.r = random.randint(0,500)
                                continue
                self.x = self.r
                self.y = self.r
                self.body = [] # list of body elements

                self.r = random.randint(0,500)
                while True:
                        if self.r % 25 == 0:
                                break
                        else:
                                self.r = random.randint(0,500)
                                continue
                self.a = self.r
                self.b = self.r


        def move(self, win):
                win.fill((0,0,0))
                self.keys = pygame.key.get_pressed()

                # move snake
                self.body.insert(0, (self.x, self.y))

                if self.right == True:
                        self.x += self.vel
                if self.left == True:
                        self.x -= self.vel
                if self.up == True:
                        self.y -= self.vel
                if self.down == True:
                        self.y += self.vel

                if self.x > 475:
                        self.x = 0
                if self.x < 0:
                        self.x = 500
                if self.y > 475:
                        self.y = 0
                if self.y < 0:
                        self.y = 500

                # remove element at end
                while len(self.body) >= self.score:
                    del self.body[-1]

                if self.keys[pygame.K_RIGHT] and self.can[0] == True:
                        self.right = True
                        self.left= False
                        self.up = False
                        self.down = False
                        self.can[1] = False
                        self.can[0] = True
                        self.can[2] = True
                        self.can[3] = True

                if self.keys[pygame.K_LEFT] and self.can[1] == True:
                        self.right = False
                        self.left = True
                        self.up = False
                        self.down = False
                        self.can[0] = False
                        self.can[1] = True
                        self.can[2] = True
                        self.can[3] = True

                if self.keys[pygame.K_UP] and self.can[2] == True:
                        self.right = False
                        self.left = False
                        self.up = True
                        self.down = False
                        self.can[3] = False
                        self.can[0] = True
                        self.can[1] = True
                        self.can[2] = True

                if self.keys[pygame.K_DOWN] and self.can[3] == True:
                        self.right = False
                        self.left = False
                        self.up = False
                        self.down = True
                        self.can[2] = False
                        self.can[0] = True
                        self.can[1] = True
                        self.can[3] = True

                # draw smake and body
                self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, 25, self.width))
                for pos in self.body:
                    pygame.draw.rect(self.win, (0,255,0), (pos[0], pos[1], 25, self.width))

        def food(self, win):
                pygame.draw.rect(self.win, (255,0,0), (self.a, self.b,25,25))

                if self.a == self.x and self.b == self.y:
                        self.r = random.randint(0,500)
                        while True:
                                if self.r % 25 == 0:
                                        break
                                else:
                                        self.r = random.randint(0,500)
                                        continue
                        self.a = self.r
                        self.b = self.r
                        self.score += 1
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
0

I would make a body part object and when the snake gets longer you add a body part. The head does the movement and the body parts follow the head.

Each game turn you just move the head then go over all of the body parts starting from the one closest to the head and move them to their parents location. So head moves 1 block, next part moves the previous head location, third part moves to second parts previous location, ...

stilllearning
  • 163
  • 10