So I've been working on a few games in Python (battleships, tic-tac-toe etc.) and this week's project is Snake. I've got a basic set-up going; the snake can move and eats the food but I haven't programmed in collision detection or going off the edge yet. The problem is response time. If you run the code below, you'll see that the snake responds to key presses, but not for a couple of - I'll call them frames - after the press. I don't quite understand how the listen() method works; am I using it properly? If not, how should I use it, and if so, how can I fix the delay? I know about Pygame, but a) I can't find an easy to install 64 bit version for python 3.4 (this one http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame is not easy to install, what the heck is a whl file?) and b) I want to challenge myself anyway. Any help would be appreciated.
import random
import turtle
import time
class Square:
def __init__(self, x, y):
self.x = x
self.y = y
def drawself(self, turtle):
# draw a black box at its coordinates, leaving a small gap between cubes
turtle.goto(self.x - 9, self.y - 9)
turtle.begin_fill()
for i in range(4):
turtle.forward(18)
turtle.left(90)
turtle.end_fill()
class Food:
def __init__(self, x, y):
self.x = x
self.y = y
self.state = "ON"
def changelocation(self):
# I haven't programmed it to spawn outside the snake's body yet
self.x = random.randint(0, 20)*20 - 200
self.y = random.randint(0, 20)*20 - 200
def drawself(self, turtle):
# similar to the Square drawself, but blinks on and off
if self.state == "ON":
turtle.goto(self.x - 9, self.y - 9)
turtle.begin_fill()
for i in range(4):
turtle.forward(18)
turtle.left(90)
turtle.end_fill()
def changestate(self):
# controls the blinking
self.state = "OFF" if self.state == "ON" else "ON"
class Snake:
def __init__(self):
self.headposition = [20, 0] # keeps track of where it needs to go next
self.body = [Square(-20, 0), Square(0, 0), Square(20, 0)] # body is a list of squares
self.nextX = 1 # tells the snake which way it's going next
self.nextY = 0
self.crashed = False # I'll use this when I get around to collision detection
self.nextposition = [self.headposition[0] + 20*self.nextX,
self.headposition[1] + 20*self.nextY]
# prepares the next location to add to the snake
def moveOneStep(self):
if Square(self.nextposition[0], self.nextposition[1]) not in self.body:
# attempt (unsuccessful) at collision detection
self.body.append(Square(self.nextposition[0], self.nextposition[1]))
# moves the snake head to the next spot, deleting the tail
del self.body[0]
self.headposition[0], self.headposition[1] = self.body[-1].x, self.body[-1].y
# resets the head and nextposition
self.nextposition = [self.headposition[0] + 20*self.nextX,
self.headposition[1] + 20*self.nextY]
else:
self.crashed = True # more unsuccessful collision detection
def moveup(self): # pretty obvious what these do
self.nextX = 0
self.nextY = 1
def moveleft(self):
self.nextX = -1
self.nextY = 0
def moveright(self):
self.nextX = 1
self.nextY = 0
def movedown(self):
self.nextX = 0
self.nextY = -1
def eatFood(self):
# adds the next spot without deleting the tail, extending the snake by 1
self.body.append(Square(self.nextposition[0], self.nextposition[1]))
self.headposition[0], self.headposition[1] = self.body[-1].x, self.body[-1].y
self.nextposition = [self.headposition[0] + 20*self.nextX,
self.headposition[1] + 20*self.nextY]
def drawself(self, turtle): # draws the whole snake when called
for segment in self.body:
segment.drawself(turtle)
class Game:
def __init__(self):
# game object has a screen, a turtle, a basic snake and a food
self.screen = turtle.Screen()
self.artist = turtle.Turtle()
self.artist.up()
self.artist.hideturtle()
self.snake = Snake()
self.food = Food(100, 0)
self.counter = 0 # this will be used later
self.commandpending = False # as will this
def nextFrame(self):
while True: # now here's where it gets fiddly...
game.screen.listen()
game.screen.onkey(game.snakedown, "Down")
game.screen.onkey(game.snakeup, "Up")
game.screen.onkey(game.snakeleft, "Left")
game.screen.onkey(game.snakeright, "Right")
turtle.tracer(0) # follow it so far?
self.artist.clear()
if self.counter == 5:
# only moves to next frame every 5 loops, this was an attempt to get rid of the turning delay
if (self.snake.nextposition[0], self.snake.nextposition[1]) == (self.food.x, self.food.y):
self.snake.eatFood()
self.food.changelocation()
else:
self.snake.moveOneStep()
self.counter = 0
else:
self.counter += 1
self.food.changestate() # makes the food flash
self.food.drawself(self.artist) # show the food and snake
self.snake.drawself(self.artist)
turtle.update()
self.commandpending = False
time.sleep(0.05)
def snakeup(self):
print("going up") # put this in for debugging purposes
if not self.commandpending:
# should allow only one turn each frame; I don't think it's working
self.snake.moveup()
self.commandpending = True
def snakedown(self):
print("going down")
if not self.commandpending:
self.snake.movedown()
self.commandpending = True
def snakeleft(self):
print("going left")
if not self.commandpending:
self.snake.moveleft()
self.commandpending = True
def snakeright(self):
print("going right")
if not self.commandpending:
self.snake.moveright()
self.commandpending = True
game = Game()
game.nextFrame()
print("game over!")
game.screen.mainloop()
Whenever you use while True:
(sans break
) in turtle code, you're defeating the event hander. You should instead use an ontimer()
event to run your code compatibly with the event handler. Below is my rewrite of your code to do this along with some other functional and style tweaks:
from turtle import Turtle, Screen
import random
import time
SIZE = 20
class Square:
def __init__(self, x, y):
self.x = x
self.y = y
def drawself(self, turtle):
""" draw a black box at its coordinates, leaving a small gap between cubes """
turtle.goto(self.x - SIZE // 2 - 1, self.y - SIZE // 2 - 1)
turtle.begin_fill()
for _ in range(4):
turtle.forward(SIZE - SIZE // 10)
turtle.left(90)
turtle.end_fill()
class Food:
def __init__(self, x, y):
self.x = x
self.y = y
self.is_blinking = True
def changelocation(self):
# I haven't programmed it to spawn outside the snake's body yet
self.x = random.randint(0, SIZE) * SIZE - 200
self.y = random.randint(0, SIZE) * SIZE - 200
def drawself(self, turtle):
# similar to the Square drawself, but blinks on and off
if self.is_blinking:
turtle.goto(self.x - SIZE // 2 - 1, self.y - SIZE // 2 - 1)
turtle.begin_fill()
for _ in range(4):
turtle.forward(SIZE - SIZE // 10)
turtle.left(90)
turtle.end_fill()
def changestate(self):
# controls the blinking
self.is_blinking = not self.is_blinking
class Snake:
def __init__(self):
self.headposition = [SIZE, 0] # keeps track of where it needs to go next
self.body = [Square(-SIZE, 0), Square(0, 0), Square(SIZE, 0)] # body is a list of squares
self.nextX = 1 # tells the snake which way it's going next
self.nextY = 0
self.crashed = False # I'll use this when I get around to collision detection
self.nextposition = [self.headposition[0] + SIZE * self.nextX, self.headposition[1] + SIZE * self.nextY]
# prepares the next location to add to the snake
def moveOneStep(self):
if Square(self.nextposition[0], self.nextposition[1]) not in self.body:
# attempt (unsuccessful) at collision detection
self.body.append(Square(self.nextposition[0], self.nextposition[1]))
# moves the snake head to the next spot, deleting the tail
del self.body[0]
self.headposition[0], self.headposition[1] = self.body[-1].x, self.body[-1].y
# resets the head and nextposition
self.nextposition = [self.headposition[0] + SIZE * self.nextX, self.headposition[1] + SIZE * self.nextY]
else:
self.crashed = True # more unsuccessful collision detection
def moveup(self): # pretty obvious what these do
self.nextX, self.nextY = 0, 1
def moveleft(self):
self.nextX, self.nextY = -1, 0
def moveright(self):
self.nextX, self.nextY = 1, 0
def movedown(self):
self.nextX, self.nextY = 0, -1
def eatFood(self):
# adds the next spot without deleting the tail, extending the snake by 1
self.body.append(Square(self.nextposition[0], self.nextposition[1]))
self.headposition[0], self.headposition[1] = self.body[-1].x, self.body[-1].y
self.nextposition = [self.headposition[0] + SIZE * self.nextX, self.headposition[1] + SIZE * self.nextY]
def drawself(self, turtle): # draws the whole snake when called
for segment in self.body:
segment.drawself(turtle)
class Game:
def __init__(self):
# game object has a screen, a turtle, a basic snake and a food
self.screen = Screen()
self.artist = Turtle(visible=False)
self.artist.up()
self.artist.speed("slowest")
self.snake = Snake()
self.food = Food(100, 0)
self.counter = 0 # this will be used later
self.commandpending = False # as will this
self.screen.tracer(0) # follow it so far?
self.screen.listen()
self.screen.onkey(self.snakedown, "Down")
self.screen.onkey(self.snakeup, "Up")
self.screen.onkey(self.snakeleft, "Left")
self.screen.onkey(self.snakeright, "Right")
def nextFrame(self):
self.artist.clear()
if (self.snake.nextposition[0], self.snake.nextposition[1]) == (self.food.x, self.food.y):
self.snake.eatFood()
self.food.changelocation()
else:
self.snake.moveOneStep()
if self.counter == 10:
self.food.changestate() # makes the food flash slowly
self.counter = 0
else:
self.counter += 1
self.food.drawself(self.artist) # show the food and snake
self.snake.drawself(self.artist)
self.screen.update()
self.screen.ontimer(lambda: self.nextFrame(), 100)
def snakeup(self):
if not self.commandpending:
self.commandpending = True
self.snake.moveup()
self.commandpending = False
def snakedown(self):
if not self.commandpending:
self.commandpending = True
self.snake.movedown()
self.commandpending = False
def snakeleft(self):
if not self.commandpending:
self.commandpending = True
self.snake.moveleft()
self.commandpending = False
def snakeright(self):
if not self.commandpending:
self.commandpending = True
self.snake.moveright()
self.commandpending = False
game = Game()
screen = Screen()
screen.ontimer(lambda: game.nextFrame(), 100)
screen.mainloop()
Does this provide the kind of response for which you're looking?
It probably is a challenge to make a speedy game this way (but that's not all bad). I think that listen()
should go after the onkey()
s.
You should also look at getting rid of all the duplicated code. It might seem easy short term to just copy/paste then alter. But if you have to make major changes (like after asking on a forum) it will be tedious and error prone to alter.
PS (EDIT) also your Snake.moveOneStep() method makes a new instance of Square just to check for self collision, this seems extravagant for the sake of elegance. Better to just keep a list of locations that python (ho, ho) can check through. (Quite apart from this probably not working. Try print(Square(1,2) in [Square(1,2)])
)
def check_self_collision(self, x, y):
for s in self.body:
if s.x == x and s.y == y:
return False
return True
def moveOneStep(self):
if self.check_self_collision(self.nextposition[0], self.nextposition[1]):
# attempt (unsuccessful) at collision detection
self.body.append(Square(self.nextposition[0], self.nextposition[1]))
My version:
#coding: utf-8
from Tkinter import *
import random
import time
class Levely:
def __init__(self):
self.urovne=[
[[0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 0, 0], [0, 0, 1, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1, 1]],
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [0, 1, 1, 1, 0, 1, 1, 1, 0, 1], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 0, 0, 1, 0, 0, 0], [1, 1, 1, 0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 0, 1, 1, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 1, 1, 1, 1, 0, 1], [0, 0, 1, 0, 0, 0, 1, 0, 0, 1], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 1, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1], [0, 0, 1, 0, 0, 0, 0, 0, 1, 1], [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 1], [0, 0, 0, 1, 0, 0, 1, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 1, 1, 0], [0, 0, 1, 1, 1, 0, 0, 0, 0, 0]],
]
self.data=[[400,13],[400,10],[400,13],[400,13],[400,13],[400,13]]
print "Choose from", len(self.urovne), "levels"
self.vyber=input("Level: ")
self.vyber-=1
h=Had(self)
class Had:
def __init__(self,Levely):
self.l=Levely
self.level=self.l.urovne[self.l.vyber]
self.mrizka=len(self.level[0])
self.velikost=self.l.data[self.l.vyber][0]
self.vtelo=100
self.r=self.l.data[self.l.vyber][1]
self.x=0
self.y=0
self.u=0
self.k=self.velikost
self.c=(self.velikost/self.mrizka)
self.poprve=0
self.neco=[[0,0],0,0,0]
self.ukonceni=None
self.aakce1=None
self.aakce2=None
self.aakce3=None
self.aakce4=None
self.s=[0,0,0,0]
self.j=[]
self.konec=0
self.score=0
self.pocet_zelenych=0
self.okno=Tk()
self.platno=Canvas(self.okno,width=self.velikost,height=self.velikost,bg="white")
self.platno.pack()
self.tl=Button(self.okno, text="Restart", command=self.start)
self.tl.pack(fill=BOTH)
self.start()
self.okno.bind("<Key-d>", self.akce1)
self.okno.bind("<Key-w>", self.akce2)
self.okno.bind("<Key-s>", self.akce3)
self.okno.bind("<Key-a>", self.akce4)
self.okno.bind("<Key-r>", self.start1)
def akce1(self, klik):
self.akce11()
def akce2(self, klik):
self.akce21()
def akce3(self, klik):
self.akce31()
def akce4(self, klik):
self.akce41()
def start1(self, klik):
self.start()
def akce11(self):
if int(self.s[1])%self.c!=0:
self.aakce1=self.okno.after(9,self.akce11)
if int(self.s[1])%self.c==0:
self.x=self.c
self.y=0
self.u=0
if self.poprve==1:
self.okno.after_cancel(self.aakce1)
self.stop()
self.pohyb()
def akce21(self):
if int(self.s[0])%self.c!=0:
self.aakce1=self.okno.after(9,self.akce21)
if int(self.s[0])%self.c==0:
self.x=0
self.y=-self.c
self.u=0
if self.poprve==1:
self.okno.after_cancel(self.aakce2)
self.stop()
self.pohyb()
def akce31(self):
if int(self.s[0])%self.c!=0:
self.aakce1=self.okno.after(9,self.akce31)
if int(self.s[0])%self.c==0:
self.x=0
self.y=self.c
self.u=1
if self.poprve==1:
self.okno.after_cancel(self.aakce3)
self.stop()
self.pohyb()
def akce41(self):
if int(self.s[1])%self.c!=0:
self.aakce1=self.okno.after(9,self.akce41)
if int(self.s[1])%self.c==0:
self.x=-self.c
self.y=0
self.u=0
if self.poprve==1:
self.okno.after_cancel(self.aakce4)
self.stop()
self.pohyb()
def pohyb(self):
self.smrt()
if self.konec==1:
return None
self.test()
s=self.platno.coords(self.hlava)
self.s=self.platno.coords(self.hlava)
self.platno.delete(ALL)
self.hlava=self.platno.create_rectangle(s[0],s[1],s[2],s[3], fill="green4", outline="white")
self.jablko=self.platno.create_rectangle(self.j[0],self.j[1],self.j[2],self.j[3], fill="red", outline="red")
for x in range(self.mrizka):
for y in range(self.mrizka):
if self.level[x][y]==0:
continue
if self.level[x][y]==1:
#KURVVAAAAA x,y,x,y
self.block=self.platno.create_rectangle(y*self.c,(x*self.c),(y*self.c)+self.c,(x*self.c)+self.c, fill="black")
self.test()
s=self.platno.coords(self.hlava)
self.poloha.append(s)
self.delka=len(self.poloha)
if s[self.u]<=self.k:
self.dx=self.x
self.dy=self.y
self.platno.move(self.hlava,self.dx/10,self.dy/10)
s=self.platno.coords(self.hlava)
self.nahrada=self.platno.create_rectangle(s[0],s[1],s[2],s[3], fill="green4", outline="green4")
if s[self.u]>=self.k:
self.dx=0
self.dy=0
bla="Restart-Score:", int(self.score)
self.tl.config(text=bla)
for a in range(self.delka):
if 1==1:
self.ocas=self.platno.create_rectangle(self.poloha[a][0],self.poloha[a][1],self.poloha[a][2],self.poloha[a][3], fill="green2", outline="green2")
self.poloha_zeleny=self.platno.coords(self.ocas)
self.zeleny.append(self.poloha_zeleny)
self.pocet_zelenych=len(self.zeleny)
if self.pocet_zelenych>=self.delka:
del self.zeleny[0]
if self.delka>=self.vtelo:
self.neco=self.poloha[0]
del self.poloha[0]
self.s=self.platno.coords(self.hlava)
self.nahrada=self.platno.create_rectangle(s[0],s[1],s[2],s[3], fill="green4", outline="green4")
self.ukonceni=self.okno.after(self.r,self.pohyb)
def smrt(self):
s=self.platno.coords(self.hlava)
bla="Restart-Score:", int(self.score)
if self.level[int(s[1]/self.c)][int(s[0]/self.c)]==1:
self.platno.delete(self.hlava)
self.tl.config(text=bla)
self.konec=1
self.smrtak=self.platno.create_rectangle(s[0],s[1],s[2],s[3], fill="brown", outline="brown")
for b in range(len(self.zeleny)):
if s==self.zeleny[(b-1)]:
self.platno.delete(self.hlava)
self.tl.config(text=bla)
self.konec=1
self.smrtak=self.platno.create_rectangle(s[0],s[1],s[2],s[3], fill="brown", outline="brown")
def stop(self):
if self.poprve==1:
self.okno.after_cancel(self.ukonceni)
self.poprve=1
def start(self):
self.vtelo=60
self.platno.delete("all")
self.tl.config(text="Restart")
self.poloha=[]
self.zeleny=[]
self.konec=0
self.pocet_zelenych=0
self.score=0
self.poprve=0
self.dx=0
self.dy=0
if self.aakce1!=None:
self.okno.after_cancel(self.aakce1)
self.aakce1=None
if self.aakce2!=None:
self.okno.after_cancel(self.aakce2)
self.aakce2=None
if self.aakce3!=None:
self.okno.after_cancel(self.aakce3)
self.aakce3=None
if self.aakce4!=None:
self.okno.after_cancel(self.aakce4)
self.aakce4=None
for x in range(self.mrizka):
for y in range(self.mrizka):
if self.level[x][y]==0:
continue
if self.level[x][y]==1:
#KURVVAAAAA x,y,x,y
self.block=self.platno.create_rectangle(y*self.c,(x*self.c),(y*self.c)+self.c,(x*self.c)+self.c, fill="black")
self.hlava=self.platno.create_rectangle(0,0,self.c,self.c, fill="green4", outline="green4")
self.generace()
s=self.platno.coords(self.hlava)
self.dx=self.c
self.dy=self.c
def generace(self):
self.nx=random.randint(0,self.mrizka-1)
self.ny=random.randint(0,self.mrizka-1)
for x in self.zeleny:
if int(x[0]/self.c)==self.nx and int(x[1]/self.c)==self.ny:
self.generace()
if self.level[self.ny][self.nx]==1:
self.generace()
if self.level[self.ny][self.nx]!=1:
self.jablko=self.platno.create_rectangle(self.nx*self.c,self.ny*self.c,self.nx*self.c+self.c,self.ny*self.c+self.c, fill="red", outline="red")
def test(self):
s=self.platno.coords(self.hlava)
self.j=self.platno.coords(self.jablko)
if s==self.j:
self.vtelo+=5
self.score+=0.5
self.generace()
def mezery(self):
for x in range(30):
print ""
levliky=Levely()
mainloop()
来源:https://stackoverflow.com/questions/30050194/snake-game-in-python-using-turtle-graphics