4
\$\begingroup\$

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.

vars.py

import random
import math

width = 800
height = 600
BG = 255, 255, 255
FOOD_C = 128, 0, 0
BODY_C = 0, 0, 0
sqr_size = 10
SPEED = sqr_size

def dist(a, b):
    return math.sqrt((b.pos[0] - a.pos[0])**2 + (b.pos[1] - a.pos[1])**2)

def check_food(snake, food): #Check if food is eaten
    if dist(snake, food) > sqr_size:
        return False
    else:
        return True

def loser(snake, food): #Check if lost the game
    if snake.pos[0]<sqr_size or snake.pos[0]>width-sqr_size:
        return True
    if 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 game_speed(snake):
    if (10 + snake.score()//2)  < 30:
        return 10 + snake.score()//2
    else:
        return 30          

class Snake(object):
    def __init__(self):
        self.pos = [random.randint(1, (width-sqr_size)/10)*10,
                    random.randint(1, (height-sqr_size)/10)*10]
        self.mov = "UP"
        self.body = [self.pos[:]]

    def change_mov(self, key): #Decide where to move
        if key == "UP" and self.mov != "DOWN":
            self.mov = key
        if key == "DOWN" and self.mov != "UP":
            self.mov = key
        if key == "RIGHT" and self.mov != "LEFT":
            self.mov = key
        if key == "LEFT" and self.mov != "RIGHT":
            self.mov = key

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

    def move(self, eat): #Snake movement
        if self.mov == "UP": self.pos[1] = self.pos[1] - SPEED
        if self.mov == "DOWN": self.pos[1] = self.pos[1] + SPEED
        if self.mov == "LEFT": self.pos[0] = self.pos[0] - SPEED
        if self.mov == "RIGHT": self.pos[0] = self.pos[0] + SPEED
        self.body.insert(0, self.pos[:])
        if not eat:
           self.body.pop()


class Food(object):
    def __init__(self):
        self.pos = [random.randint(1, (width-sqr_size)/10)*10,
                    random.randint(1, (height-sqr_size)/10)*10]

snake.py

import pygame, sys
import vars

#Initialising pygame 
pygame.init()
pygame.font.init()
myfont = pygame.font.SysFont('Times New Roman', 30)
clock = pygame.time.Clock()
screen = pygame.display.set_mode((vars.width,vars.height))
pygame.display.update()

#Initialising variables
lost = False
eat = False
snake = vars.Snake()
food = vars.Food()
screen.fill(vars.BG)
key1 = "0"

def whatkey(event):
    if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                return "LEFT"
            if event.key == pygame.K_RIGHT:
                return "RIGHT"
            if event.key == pygame.K_UP:
                return "UP"
            if event.key == pygame.K_DOWN:
                return "DOWN"

while not lost:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: 
            sys.exit()
        key1 = whatkey(event)

    #How the game works   
    snake.change_mov(key1)
    eat = vars.check_food(snake, food)
    snake.move(eat)
    if eat:
        food = vars.Food()
    lost = vars.loser(snake, food)

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

    #Control of the game speed via fps
    #Not related to the SPEED variable. That is for movement  
    msElapsed = clock.tick(vars.game_speed(snake))            

#Lose screen
pygame.display.update()
screen.fill(vars.BG)
textsurface1 = myfont.render('You lost. Your score is:', False, (0, 0, 0))
textsurface2 = myfont.render("{}".format(snake.score()), False, (0, 0, 0))
screen.blit(textsurface1,(250, 200))
screen.blit(textsurface2,(380,280))
pygame.display.update()
while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: sys.exit()
\$\endgroup\$
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Mast Feb 17 at 19:45
  • \$\begingroup\$ @Mast I'm sorry, I didn't know. However, the edits I made weren't to incorporate the answers. I changed circles to squares, for example and colours I believe but didn't know I shouldn't. I won't do it again. \$\endgroup\$ – André Rocha Feb 17 at 20:17
  • \$\begingroup\$ Feel free to post a follow-up question with improved code if your code has changed significantly enough in the meantime. You can save such edits for then. No problem. \$\endgroup\$ – Mast Feb 17 at 23:00
7
\$\begingroup\$

For efficiency reasons, you should always do x1**2 + y1**2 < r**2 rather than sqrt(x1**2 + y1**2) < r, because sqrt is much much slower than pow. Because You don't need a square root to compare distances. This is the special case of x1**2 + y1**2 < x2**2 + y2**2.

Sometimes sqrt distances computing when you have a bunch of things on your screen becomes the slowest thing in your program. And there is absolutely no reason to do it.

My suggestion is to keep everything squared, until you really need to compute sqrt (which you don't)


Also, storing a direction in a string works ("DOWN"), but isn't very practical, it is a beginner pattern called 'stringly typed code'. You could instead make a constant called DOWN which corresponds to the vector pointing down ((0, -1) to make it simple or 0 - 1j if you like complex numbers (best imo) or a custom object if you like OOP). You can then replace:

if self.mov == "UP": 
    self.pos[1] = self.pos[1] - SPEED
if self.mov == "DOWN": 
    self.pos[1] = self.pos[1] + SPEED
if self.mov == "LEFT": 
    self.pos[0] = self.pos[0] - SPEED
if self.mov == "RIGHT": 
    self.pos[0] = self.pos[0] + SPEED

# becomes

self.pos += self.mov

with constants, math becomes easier:

UP = 0 + 1j
DOWN = 0 - 1j
LEFT = -1 + 0j
RIGHT = 1 + 0j

2*DOWN + 5*LEFT # means move 2 cases down and 5 left how intuitive

if key == "UP" and self.mov != "DOWN":
            self.mov = key
if key == "DOWN" and self.mov != "UP":
            self.mov = key
if key == "RIGHT" and self.mov != "LEFT":
            self.mov = key
if key == "LEFT" and self.mov != "RIGHT":
            self.mov = key

# becomes

if key - self.mov:
    self.mov = key

It takes time getting used to but it is worth it, you can always use tuples but they are the same except math doesn't work with them. The fact is Complex numbers are awesome it generalises nicely for arbitrary directions and distances. And if you assign to constants, you don't ever need to see them.

\$\endgroup\$
  • \$\begingroup\$ I'll also suggest looking into numpy for direction vectors. It's not strictly necessary for 2D vectors (I actually really like that idea of using complex numbers, I might borrow that), but it'll certainly show up once you start looking into AI code, so you might as well get used to it now. \$\endgroup\$ – scnerd Feb 18 at 0:33
  • \$\begingroup\$ Nice use of complex numbers. I would use Enums instead of constants. They are even more clear \$\endgroup\$ – Maarten Fabré Feb 18 at 9:20
3
\$\begingroup\$

I'm on my phone so I'll just mention something minor I noticed.

check_food's return is redundant. dist(snake, food) > sqr_size already evaluates to a boolean value. You just need to negate that:

def check_food(snake, food): #Check if food is eaten
    return not dist(snake, food) > sqr_size

Or simply:

def check_food(snake, food): #Check if food is eaten
    return dist(snake, food) <= sqr_size

And there's a similar situation in loser. The first two conditions are just returning True. They could be "connected" via or to be simplified.

\$\endgroup\$
  • \$\begingroup\$ You are absolutely right. I deleted the check_food function altogether and just used eat = vars.dist(snake, food) < vars.sqr_size \$\endgroup\$ – André Rocha Feb 17 at 18:46
  • \$\begingroup\$ @AndréRocha Unless you've double checked that logic, you may want to use <= instead, as that would be the opposite of >. \$\endgroup\$ – Carcigenicate Feb 17 at 18:55
  • \$\begingroup\$ I did, it was eating the food while it was walking on an adjacent space. This way that won't happen \$\endgroup\$ – André Rocha Feb 17 at 19:08
3
\$\begingroup\$

This is a general comment on the structure of the code, and in particular how you may want to think about redesigning some of it.

Given your end goal is to have AI learn to play snake, you may wish to abstract away the input source, the graphics, and also anything relating to time.

Rather than formulate the game of snake as checking which key the user has selected at certain times, you may want to structure it as a decision each turn. Yes, this is a bit of a modification to snake, but it may be easier for you to get started. So this basically eliminates the time factor.

Yes, time is of major importance when it comes to human players, but developing ML that's sensitive to time constraints is more complicated. Maybe you can make it a stretch goal? :)

So, in general, I would have the structure as this:

[User] <-> [Game] <-> [Graphics]

When developing your AI, you swap out the user module for the AI. You also may want to have a graphics module you can swap in and out. You don't want graphics to limit how fast your AI can learn, but you may want to turn it on to see what your AI is doing from time to time.

\$\endgroup\$

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.