7
\$\begingroup\$

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()
\$\endgroup\$
9
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Jun 22 '15 at 4:44
  • 1
    \$\begingroup\$ @NineToeNerd, GitHub uses Git which I think is a very good version control to learn. Getting an account on GitHub is probably a very good idea. \$\endgroup\$ – shuttle87 Jun 22 '15 at 4:59
  • \$\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 Jun 22 '15 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 Jun 23 '15 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 Jun 23 '15 at 1:19
1
\$\begingroup\$

BTW: you don't need Frames

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)

\$\endgroup\$
  • \$\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 Sep 20 '15 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 master widget which in the most simple case would be a Tk widget but you can place two Frames side by side in the same window for example. \$\endgroup\$ – Tadhg McDonald-Jensen Feb 12 '16 at 18:58

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.