Posts: 3
Threads: 1
Joined: Feb 2019
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
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()
Posts: 5,151
Threads: 396
Joined: Sep 2016
Feb-17-2019, 09:59 PM
(This post was last modified: Feb-17-2019, 09:59 PM by metulburr.)
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:
Posts: 3
Threads: 1
Joined: Feb 2019
Feb-18-2019, 09:14 PM
(This post was last modified: Feb-18-2019, 09:15 PM by andrerocha1998.)
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.
Posts: 5,151
Threads: 396
Joined: Sep 2016
Feb-18-2019, 10:53 PM
(This post was last modified: Feb-18-2019, 10:54 PM by metulburr.)
(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:
Posts: 5,151
Threads: 396
Joined: Sep 2016
Feb-18-2019, 11:07 PM
(This post was last modified: Feb-18-2019, 11:07 PM by metulburr.)
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:
Posts: 3
Threads: 1
Joined: Feb 2019
(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.
Posts: 5,151
Threads: 396
Joined: Sep 2016
Feb-19-2019, 12:06 AM
(This post was last modified: Feb-19-2019, 12:06 AM by metulburr.)
(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:
Posts: 544
Threads: 15
Joined: Oct 2016
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.
|