I have written a Snake game using Python's Tkinter library.
I am a beginner to programming. I learned some python alone and in school. However, most of what we did in school is algorithms/data structures and problem-solving (school program geared towards mathematics). Not much coding.
Lately, I picked up Python again. I was introduced to OOP in Python and used Tkinter before deciding to give Snake a try. This is my first quasi-real application.
Any comments, recommendations, bad/good habits are welcome.
from tkinter import*
from random import randint
# constants that go in the making of the grid used for the snake's movment
GRADUATION = 40
PIXEL = 10
STEP = 2 * PIXEL
WD = PIXEL * GRADUATION
HT = PIXEL * GRADUATION
# constants that go into specifying the shapes' sizes
OB_SIZE_FACTOR = 1
SN_SIZE_FACTOR = 0.9
OB_SIZE = PIXEL * OB_SIZE_FACTOR
SN_SIZE = PIXEL * SN_SIZE_FACTOR
# color constants
BG_COLOR = 'black'
OB_COLOR = 'red'
SN_COLOR = 'white'
# a dictionary to ease access to a shape's type in the Shape class
SN = 'snake'
OB = 'obstacle'
SIZE = {SN: SN_SIZE, OB: OB_SIZE}
# constants for keyboard input
UP = 'Up'
DOWN = 'Down'
RIGHT = 'Right'
LEFT = 'Left'
# a dictionary to ease access to 'directions'
DIRECTIONS = {UP: [0, -1], DOWN: [0, 1], RIGHT: [1, 0], LEFT: [-1, 0]}
AXES = {UP: 'Vertical', DOWN: 'Vertical', RIGHT: 'Horizontal', LEFT: 'Horizontal'}
# refresh time for the perpetual motion
REFRESH_TIME = 100
class Master(Canvas):
"""create the game canvas, the snake, the obstacle, keep track of the score"""
def __init__(self, boss=None):
super().__init__(boss)
self.configure(width=WD, height=HT, bg=BG_COLOR)
self.running = 0
self.snake = None
self.obstacle = None
self.direction = None
self.current = None
self.score = Scores(boss)
def start(self):
"""start snake game"""
if self.running == 0:
self.snake = Snake(self)
self.obstacle = Obstacle(self)
self.direction = RIGHT
self.current = Movement(self, RIGHT)
self.current.begin()
self.running = 1
def clean(self):
"""restarting the game"""
if self.running == 1:
self.score.reset()
self.current.stop()
self.running = 0
self.obstacle.delete()
for block in self.snake.blocks:
block.delete()
def redirect(self, event):
"""taking keyboard inputs and moving the snake accordingly"""
if 1 == self.running and \
event.keysym in AXES.keys() and\
AXES[event.keysym] != AXES[self.direction]:
self.current.flag = 0
self.direction = event.keysym
self.current = Movement(self, event.keysym) # a new instance at each turn to avoid confusion/tricking
self.current.begin() # program gets tricked if the user presses two arrow keys really quickly
class Scores:
"""Objects that keep track of the score and high score"""
def __init__(self, boss=None):
self.counter = StringVar(boss, '0')
self.maximum = StringVar(boss, '0')
def increment(self):
score = int(self.counter.get()) + 1
maximum = max(score, int(self.maximum.get()))
self.counter.set(str(score))
self.maximum.set(str(maximum))
def reset(self):
self.counter.set('0')
class Shape:
"""This is a template to make obstacles and snake body parts"""
def __init__(self, can, a, b, kind):
self.can = can
self.x, self.y = a, b
self.kind = kind
if kind == SN:
self.ref = Canvas.create_rectangle(self.can,
a - SN_SIZE, b - SN_SIZE,
a + SN_SIZE, b + SN_SIZE,
fill=SN_COLOR,
width=2)
elif kind == OB:
self.ref = Canvas.create_oval(self.can,
a - OB_SIZE, b - OB_SIZE,
a + SN_SIZE, b + SN_SIZE,
fill=OB_COLOR,
width=2)
def modify(self, a, b):
self.x, self.y = a, b
self.can.coords(self.ref,
a - SIZE[self.kind], b - SIZE[self.kind],
a + SIZE[self.kind], b + SIZE[self.kind])
def delete(self):
self.can.delete(self.ref)
class Obstacle(Shape):
"""snake food"""
def __init__(self, can):
"""only create the obstacles where there is no snake body part"""
self.can = can
p = int(GRADUATION/2 - 1)
n, m = randint(0, p), randint(0, p)
a, b = PIXEL * (2 * n + 1), PIXEL * (2 * m + 1)
while [a, b] in [[block.x, block.y] for block in self.can.snake.blocks]:
n, m = randint(0, p), randint(0, p)
a, b = PIXEL * (2 * n + 1), PIXEL * (2 * m + 1)
super().__init__(can, a, b, OB)
class Block(Shape):
"""snake body part"""
def __init__(self, can, a, y):
super().__init__(can, a, y, SN)
class Snake:
"""a snake keeps track of its body parts"""
def __init__(self, can):
"""initial position chosen by me"""
self.can = can
a = PIXEL + 2 * int(GRADUATION/4) * PIXEL
self.blocks = [Block(can, a, a), Block(can, a, a + STEP)]
def move(self, path):
"""an elementary step consisting of putting the tail of the snake in the first position"""
a = (self.blocks[-1].x + STEP * path[0]) % WD
b = (self.blocks[-1].y + STEP * path[1]) % HT
if a == self.can.obstacle.x and b == self.can.obstacle.y: # check if we find food
self.can.score.increment()
self.can.obstacle.delete()
self.blocks.append(Block(self.can, a, b))
self.can.obstacle = Obstacle(self.can)
elif [a, b] in [[block.x, block.y] for block in self.blocks]: # check if we hit a body part
self.can.clean()
else:
self.blocks[0].modify(a, b)
self.blocks = self.blocks[1:] + [self.blocks[0]]
class Movement:
"""object that enters the snake into a perpetual state of motion in a predefined direction"""
def __init__(self, can, direction):
self.flag = 1
self.can = can
self.direction = direction
def begin(self):
"""start the perpetual motion"""
if self.flag > 0:
self.can.snake.move(DIRECTIONS[self.direction])
self.can.after(REFRESH_TIME, self.begin)
def stop(self):
"""stop the perpetual movement"""
self.flag = 0
if __name__ == "__main__":
root = Tk()
root.title("Snake Game")
game = Master(root)
game.grid(column=1, row=0, rowspan=3)
root.bind("<Key>", game.redirect)
buttons = Frame(root, width=35)
Button(buttons, text='Start', command=game.start).grid()
Button(buttons, text='Stop', command=game.clean).grid()
Button(buttons, text='Quit', command=root.destroy).grid()
buttons.grid(column=0, row=0)
scoreboard = Frame(root, width=35, height=2 * HT / 5)
Label(scoreboard, text='Game Score').grid()
Label(scoreboard, textvariable=game.score.counter).grid()
Label(scoreboard, text='High Score').grid()
Label(scoreboard, textvariable=game.score.maximum).grid()
scoreboard.grid(column=0, row=2)
root.mainloop()
-
1\$\begingroup\$ You might be interested in this question and its answer. \$\endgroup\$Gareth Rees– Gareth Rees2018年01月25日 23:06:43 +00:00Commented Jan 25, 2018 at 23:06
1 Answer 1
Overview
The code layout is good, and you used meaningful names for classes, functions and variables.
UX
It is not obvious what the user should do when the GUI opens up. You should display some simple instructions in the GUI, such as:
Use arrow keys on keyboard to move right, left, etc.
Naming
Generally-speaking, you used meaningful names for many variables.
However, some names are ambiguous or don't convey much meaning.
For example, can
should be canvas
.
Docstrings
No need to mention "objects" when describing a class:
class Scores:
"""Objects that keep track of the score and high score"""
The docstring is simpler as:
"""Keep track of the score and high score"""
Boolean
The variable named running
would be better as a boolean type since it only takes on values of 0 and 1.
Also, conventionally, booleans use an "is_" prefix in the name.
Checks of the variable:
if self.running == 1:
can be simplified as:
if self.is_running:
Consider making flag
a boolean as well. Also, the name flag
is not very descriptive;
perhaps change it to something like is_moving
.
Documentation
Add a docstring to the top of the file to summarize what the code does.
"""
Snake game using Python's Tkinter
"""
Namespace
The following line imports a lot of unused items into the current scope:
from tkinter import*
It is best to import only what is needed to avoid namespace pollution.
One common option is to use the following import line:
import tkinter as tk
This does not import any items by defualt, and you will need to
prefix everything that is needed with tk.
. For example,
change:
root = Tk()
to:
root = tk.Tk()
The advantage to this approach is that it is obvious from reading the code
where things like Canvas
come from when referenced as tk.Canvas
.
Explore related questions
See similar questions with these tags.