[PyGame] Made my first Python program: Snake. Please help me improve it - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: Game Development (https://python-forum.io/forum-11.html) +--- Thread: [PyGame] Made my first Python program: Snake. Please help me improve it (/thread-16178.html) |
Made my first Python program: Snake. Please help me improve it - andrerocha1998 - Feb-17-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 snake.py RE: Made my first Python program: Snake. Please help me improve it - metulburr - Feb-17-2019 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. RE: Made my first Python program: Snake. Please help me improve it - andrerocha1998 - Feb-18-2019 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. RE: Made my first Python program: Snake. Please help me improve it - metulburr - Feb-18-2019 (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,yYou 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. RE: Made my first Python program: Snake. Please help me improve it - metulburr - Feb-18-2019 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) RE: Made my first Python program: Snake. Please help me improve it - andrerocha1998 - Feb-18-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. RE: Made my first Python program: Snake. Please help me improve it - metulburr - Feb-19-2019 (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. RE: Made my first Python program: Snake. Please help me improve it - Windspar - Feb-19-2019 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() |