I have just installed Ubuntu and am re-familiarizing myself with Python. I learned the basics in late 2012-early 2013 and I'm practicing with it in order to get better at programming concepts and practice.
'''
Snake Game
implements gameplay of classic snake
game with Tkinter
Author: Tracy Lynn Wesley
'''
import threading
import random
import os.path
from Tkinter import *
WIDTH = 500
HEIGHT = 500
class Snake(Frame):
def __init__(self):
Frame.__init__(self)
#Set up the main window frame as a grid
self.master.title("Snake *** Try to beat the high score! ***")
self.grid()
#Set up main frame for game as a grid
frame1 = Frame(self)
frame1.grid()
#Add a canvas to frame1 as self.canvas member
self.canvas = Canvas(frame1, width = WIDTH, height = HEIGHT, bg ="white")
self.canvas.grid(columnspan = 3)
self.canvas.focus_set()
self.canvas.bind("<Button-1>", self.create)
self.canvas.bind("<Key>", self.create)
#Create a "New Game" button
newGame = Button(frame1, text = "New Game", command = self.new_game)
newGame.grid(row = 1, column = 0, sticky = E)
#Create a label to show user his/her score
self.score_label = Label(frame1)
self.score_label.grid(row = 1, column = 1)
self.high_score_label = Label(frame1)
self.high_score_label.grid(row = 1, column = 2)
#Direction label (for debugging purpose)
#self.direction_label = Label(frame1, text = "Direction")
#self.direction_label.grid(row = 1, column = 2)
self.new_game()
def new_game(self):
self.canvas.delete(ALL)
self.canvas.create_text(WIDTH/2,HEIGHT/2-50,text="Welcome to Snake!"\
+ "\nPress arrow keys or click in the window"\
+ " to start moving!", tag="welcome_text")
rectWidth = WIDTH/25
#Initialize snake to 3 rectangles
rect1 = self.canvas.create_rectangle(WIDTH/2-rectWidth/2, HEIGHT/2-rectWidth/2, WIDTH/2+rectWidth/2\
, HEIGHT/2+rectWidth/2, outline="#dbf", fill="#dbf"\
, tag="rect1")
rect2 = self.canvas.create_rectangle(WIDTH/2-rectWidth/2, HEIGHT/2-rectWidth/2, WIDTH/2+rectWidth/2\
, HEIGHT/2+rectWidth/2, outline="#dbf", fill="#dbf"\
, tag="rect2")
rect3 = self.canvas.create_rectangle(WIDTH/2-rectWidth/2, HEIGHT/2-rectWidth/2, WIDTH/2+rectWidth/2\
, HEIGHT/2+rectWidth/2, outline="#dbf", fill="#dbf"\
, tag="rect3")
#initialize variables that contribute to smooth gameplay below:
#
#set rectangle width and height variables for use with new rectangles on the canvas
self.rectWidth = rectWidth
#lastDirection recorded because first 2 rectangles always overlap while moving,
#but if user goes right then immediately left the snake should run into itself and
#therefore end the game (See below functions self.check_collide and self.end_game)
self.lastDirection = None
self.direction = None
#Used to force snake to expand out on first move
self.started = False
#Used to force game loop to halt when a collision occurs/snake out of bounds
self.game_over = False
#Initialize game score to 0
self.score = 0
#Initialize high score from file
if (os.path.isfile("high_score.txt")):
scoreFile = open("high_score.txt")
self.high_score = int(scoreFile.read())
scoreFile.close()
else:
self.high_score = 0
self.high_score_label["text"] = "High Score: " + str(self.high_score)
self.rectangles = [rect1,rect2,rect3]
#Initialize the "dot" (which the snake "eats")
self.dot = None
#Start thread for snake to move when direction is set
self.move()
def create(self, event):
self.lastDirection = self.direction
if self.game_over == False:
if event.keycode == 111:
self.direction = "up"
elif event.keycode == 114:
self.direction = "right"
elif event.keycode == 116:
self.direction = "down"
elif event.keycode == 113:
self.direction = "left"
elif event.x < WIDTH/2 and HEIGHT/3 < event.y < HEIGHT-HEIGHT/3:
self.direction = "left"
#(Debug)
#self.direction_label["text"] = "LEFT"
elif event.x > WIDTH/2 and HEIGHT/3 < event.y < HEIGHT-HEIGHT/3:
self.direction= "right"
#(Debug)
#self.direction_label["text"] = "RIGHT"
elif WIDTH/3 < event.x < WIDTH-WIDTH/3 and event.y < HEIGHT/2:
self.direction = "up"
#(Debug)
#self.direction_label["text"] = "UP"
elif WIDTH/3 < event.x < WIDTH-WIDTH/3 and event.y > HEIGHT/2:
self.direction= "down"
#(Debug)
#self.direction_label["text"] = "DOWN"
def first_movement(self):
w = self.rectWidth
self.canvas.delete("welcome_text")
#Expand snake in direction chosen
if self.direction == "left":
self.canvas.move("rect1",-w,0)
self.canvas.after(100)
self.canvas.move("rect1",-w,0)
self.canvas.move("rect2",-w,0)
elif self.direction == "down":
self.canvas.move("rect1",0,w)
self.canvas.after(100)
self.canvas.move("rect1",0,w)
self.canvas.move("rect2",0,w)
elif self.direction == "right":
self.canvas.move("rect1",w,0)
self.canvas.after(100)
self.canvas.move("rect1",w,0)
self.canvas.move("rect2",w,0)
elif self.direction == "up":
self.canvas.move("rect1",0,-w)
self.canvas.after(100)
self.canvas.move("rect1",0,-w)
self.canvas.move("rect2",0,-w)
self.canvas.after(100)
def _move(self):
w = self.rectWidth
while True:
self.score_label["text"] = "Score: " + str(self.score)
if self.started == False and self.direction != None:
self.first_movement()
self.started = True
elif self.started == True and self.game_over == False:
if self.dot == None:
self.make_new_dot()
lock = threading.Lock()
lock.acquire()
endRect = self.rectangles.pop()
frontCoords = self.canvas.coords(self.rectangles[0])
endCoords = self.canvas.coords(endRect)
#(Below for Debugging)
#print self.direction
#print "Front: " + str(frontCoords) + " Back: " + str(endCoords)
if self.direction == "left":
self.canvas.move(self.canvas.gettags(endRect), int(frontCoords[0]-endCoords[0])-w,\
int(frontCoords[1]-endCoords[1]))
elif self.direction == "down":
self.canvas.move(self.canvas.gettags(endRect), int(frontCoords[0]-endCoords[0]),\
int(frontCoords[1]-endCoords[1])+w)
elif self.direction == "right":
self.canvas.move(self.canvas.gettags(endRect), int(frontCoords[0]-endCoords[0])+w,\
int(frontCoords[1]-endCoords[1]))
elif self.direction == "up":
self.canvas.move(self.canvas.gettags(endRect), int(frontCoords[0]-endCoords[0]),\
int(frontCoords[1]-endCoords[1])-w)
self.canvas.after(100)
self.rectangles.insert(0, endRect)
lock.release()
self.check_bounds()
self.check_collide()
elif self.game_over == True:
break;
def move(self):
threading.Thread(target=self._move).start()
def make_new_dot(self):
if self.dot != None:
self.canvas.delete(self.dot)
self.dot = None
dotX = random.random()*(WIDTH-self.rectWidth*2) + self.rectWidth
dotY = random.random()*(HEIGHT-self.rectWidth*2) + self.rectWidth
self.dot = self.canvas.create_rectangle(dotX,dotY,dotX+self.rectWidth,dotY+self.rectWidth\
,outline="#ddd", fill="#ddd", tag="dot")
def grow(self):
w = self.rectWidth
lock = threading.Lock()
lock.acquire()
#Increase the score any time the snake grows
self.score += 100
endCoords = self.canvas.coords(self.rectangles[len(self.rectangles)-1])
#(Debug)
#print "endCoords: " + str(endCoords)
thisTag = "rect" + str(len(self.rectangles) + 1)
x1 = int(endCoords[0])
y1 = int(endCoords[1])
x2 = int(endCoords[2])
y2 = int(endCoords[3])
if self.direction == "left":
x1 += w
x2 += w
elif self.direction == "right":
x1 -= w
x2 -= w
elif self.direction == "down":
y1 -= w
y2 -= w
elif self.direction == "up":
y1 += w
y2 += w
#(Debug)
#print self.direction
#print "new coords: " + str(x1) + ", " + str(y1) + ", " + str(x2) + ", " + str(y2)
thisRect = self.canvas.create_rectangle(x1, y1, x2, y2, outline="#dbf",\
fill="#dbf", tag=thisTag)
#print str(self.rectangles)
self.rectangles.append(thisRect)
#print str(self.rectangles)
lock.release()
def check_bounds(self):
coordinates = self.canvas.coords(self.rectangles[0])
if len(coordinates) > 0:
if coordinates[0] < 0 or coordinates[1] < 0 or coordinates[2] > WIDTH\
or coordinates[3] > HEIGHT:
self.end_game()
def check_collide(self):
frontCoords = self.canvas.coords(self.rectangles[0])
#(For Debugging)
#for rect in self.rectangles:
#coords = self.canvas.coords(rect)
#print "Front: " + str(frontCoords) + "coords: " + str(coords)
#Check to see if the snake's head(front) is overlapping anything and handle it below
overlapping = self.canvas.find_overlapping(frontCoords[0],frontCoords[1]\
,frontCoords[2],frontCoords[3])
for item in overlapping:
if item == self.dot:
#Snake collided with dot, grow snake and move dot
self.grow()
self.make_new_dot()
if item in self.rectangles[3:]:
#Snake has collided with its body, end game
self.end_game()
#Snake tried to move backwards (therefore crashing into itself)
if (self.lastDirection == "left" and self.direction == "right") or\
(self.lastDirection == "right" and self.direction == "left") or\
(self.lastDirection == "up" and self.direction == "down") or\
(self.lastDirection == "down" and self.direction == "up"):
self.end_game()
def end_game(self):
self.game_over = True
self.canvas.create_text(WIDTH/2,HEIGHT/2,text="GAME OVER!")
if self.score > self.high_score:
scoreFile = open("high_score.txt", "w")
scoreFile.write(str(self.score))
scoreFile.close()
self.canvas.create_text(WIDTH/2,HEIGHT/2+20,text=\
"You beat the high score!")
#(Debug)
#self.direction_label["text"] = "ENDED"
Snake().mainloop()
2 Answers 2
If you haven't read it already I'd highly recommend that you check out PEP8 as it is a great starting point for a lot of questions about Python code conventions. Some of what I write here will be repeated there.
Tightly coupled functions
One issue with your design is that your functions are designed such that you must call them in a particular sequence in order to get the results you want.
init
calls new_game
calls move
which creates the game loop thread in _move
. While this might be somewhat appropriate in this case it is usually indicative of bad design. Generally speaking the more you can make your functions not depend on side-effects the better as it allows you more effective code reuse opportunities. Additionally if a precondition for a function is that another function must be called you really need to document that clearly, as this could be a large source of confusion (and hence bugs) for other people (including the future you) who read/maintain the codebase in the future.
Commented out code
Use your version control software to manage your changes in code instead of commenting out code. If you are not using version control then you should strongly consider learning how as this is one of the most valuable productivity boosters you can get with development.
If you want logging perhaps look into the standard library logging module.
Documentation
You have used comments fairly extensively in the code here which definitely helps when reading it. Also I noticed you put a docstring for the module which is good to see! Putting some docstrings in the rest of your code will help other people read it as it gives you a standardized place to look for documentation on different functions/classes/methods.
For example:
def new_game(self):
"""Creates a new game. Sets up canvas and 1initial game conditions."""
Prefer named "constants" over multiple variables
"magic variables" are often less clear than explicit named variables.
if event.keycode == 111:
self.direction = "up"
When I'm reading this code I have to guess from the context that the 111
is the keycode for the up key. I have to read ahead and look at the context to figure this out.
The code is much more clear if you give a named variable:
UP_KEY_CODE = 111
if event.keycode == UP_KEY_CODE:
I notice a similar situation with the directions, create a variable for the various different directions instead of hardcoding strings everywhere. For example in a lot of places in the code you have lines such as:
if self.direction == "left":
Personally I'd prefer to define something such as LEFT_DIRECTION = "left"
then compare like so:
if self.direction == LEFT_DIRECTION:
This makes means if you change the type of value stored in self.direction
in the future it's very easy to make changes. Currently you would have to track down all the different strings and change them. Having used other languages I think this is a very good example of where an enumeration type is useful but others might consider that somewhat un-pythonic, so do whatever you think is most readable.
Going further I'd consider making some sort dictionary to store the keycode along with the associated action that needs to be done if you start getting more keyboard keys being used in your program. I'll explain this in more depth if you make a follow up question.
Checking for non-empty sequences
The pythonic way to do this is explained in PEP8.
instead of:
if len(coordinates) > 0:
if self.dot != None:
elif self.game_over == True:
do:
if coordinates:
if self.dot:
elif self.game_over:
This makes your code shorter and improves readability.
duplicated code
Any time you see code that does essentially the same thing you should consider re-writing it. Python is highly amenable to the don't-repeat-yourself idea so keep that in mind.
if self.direction == "left":
self.canvas.move("rect1",-w,0)
self.canvas.after(100)
self.canvas.move("rect1",-w,0)
self.canvas.move("rect2",-w,0)
elif self.direction == "down":
self.canvas.move("rect1",0,w)
self.canvas.after(100)
self.canvas.move("rect1",0,w)
self.canvas.move("rect2",0,w)
elif self.direction == "right":
self.canvas.move("rect1",w,0)
self.canvas.after(100)
self.canvas.move("rect1",w,0)
self.canvas.move("rect2",w,0)
elif self.direction == "up":
self.canvas.move("rect1",0,-w)
self.canvas.after(100)
self.canvas.move("rect1",0,-w)
self.canvas.move("rect2",0,-w)
First of all I'd clean up the indentation here to be consistent, but once we have done that we see that all of this is essentially doing the same thing. I would therefore break this into a function:
def expand_snake(self, x_direction, y_direction):
"""Expand the snake in the given directions as per the parameters."""
self.canvas.move("rect1", x_direction, y_direction)
self.canvas.after(100)
self.canvas.move("rect1", x_direction, y_direction)
self.canvas.move("rect2", x_direction, y_direction)
Then the code just becomes:
if self.direction == "left":
self.expand_snake(-w, 0):
elif self.direction == "down":
self.expand_snake(0, w):
elif self.direction == "right":
self.expand_snake(w, 0):
elif self.direction == "up":
self.expand_snake(0, -w):
Less duplication and less chances for something to go wrong. Additionally as mentioned before you probably want to make named constants for the directions or use an enumeration instead of strings such as "up" "down" etc for keeping track of the directions.
-
\$\begingroup\$ Didn't notice that indentation problem. Oops! As for version control, I used one that was already installed on my work laptop during an internship (can't remember it's name, sadly) and I don't have much experience with it. What would you recommend? Would gitHub repositories do what I need? \$\endgroup\$NineToeNerd– NineToeNerd2015年06月22日 04:44:24 +00:00Commented Jun 22, 2015 at 4:44
-
1
-
\$\begingroup\$ Another question, will it make a difference if I use "if self.dot" as you mentioned above, rather than "if self.dot is not None" (it's initialized to None)? Or should I initialize it to something else? \$\endgroup\$NineToeNerd– NineToeNerd2015年06月22日 21:31:16 +00:00Commented Jun 22, 2015 at 21:31
-
\$\begingroup\$ It shouldn't make a difference to the logic of the code here, it is just a clearer way of writing the code. Formatting code here uses backticks ``` to render code snippets. \$\endgroup\$shuttle87– shuttle872015年06月23日 00:33:39 +00:00Commented Jun 23, 2015 at 0:33
-
\$\begingroup\$ Can I do this with all data types? Like does any type evaluate to True if it's not empty or unassigned? \$\endgroup\$NineToeNerd– NineToeNerd2015年06月23日 01:19:09 +00:00Commented Jun 23, 2015 at 1:19
BTW: you don't need Frame
s
You can use self
in place of frame1
and Snake(Tk)
in place of Snake(Frame)
class Snake(Tk):
def __init__(self):
Tk.__init__(self)
self.title("Snake *** Try to beat the high score! ***")
# Add a canvas to frame1 as self.canvas member
self.canvas = Canvas(self, width=WIDTH, height=HEIGHT, bg="white")
self.canvas.grid(columnspan=3)
self.canvas.focus_set()
self.canvas.bind("<Button-1>", self.create)
self.canvas.bind("<Key>", self.create)
# Create a "New Game" button
newGame = Button(self, text="New Game", command=self.new_game)
newGame.grid(row=1, column=0, sticky=E)
# Create a label to show user his/her score
self.score_label = Label(self)
self.score_label.grid(row=1, column=1)
self.high_score_label = Label(self)
self.high_score_label.grid(row=1, column=2)
self.new_game()
(Tk
creates main window)
-
\$\begingroup\$ Thank you! I was wondering if inheriting from Frame was completely necessary just the other day or if there was a better way to do it. I actually copied the NameOfProgram(Frame) and that initialization from the way we learned to implement a Tk Frame in college. Can you maybe tell me why the inheritance of Frame instead of Tk would have been recommended in any situation? \$\endgroup\$NineToeNerd– NineToeNerd2015年09月20日 16:20:48 +00:00Commented Sep 20, 2015 at 16:20
-
\$\begingroup\$
Tk
is the very root of the application so inheriting from it means you can only make one instance for the application.Frame
can (needs to be) placed on a masterwidget
which in the most simple case would be aTk
widget but you can place twoFrame
s side by side in the same window for example. \$\endgroup\$Tadhg McDonald-Jensen– Tadhg McDonald-Jensen2016年02月12日 18:58:19 +00:00Commented Feb 12, 2016 at 18:58
Explore related questions
See similar questions with these tags.