Need help on snake program about user's input - python

So, I'm writing a snake program using the tkinter Library. The program is globally working but I have a little problem with the inputs' treatment indeed if i give two input too quickly only the last one will be interpret. And i don't really know how to solve this i try to force the update after every player's input but it's clearly not the good solution because it force the snake to move and make it able to teleport so I'm would be glad if someone has an idea to solve this issue. There is my code I'm sure that it could be improved but for now I would like to focus on the first issue.
import tkinter as tk
import numpy.random as rd
class snake:
def __init__(self,n,m):
self.n = n
self.m = m
self.body = [(n//2,m//2),(n//2,m//2-1)]
self.lenght = 2
self.food = (0,0)
self.relocate_food()
self.Game = -2
self.vector = (0,1) #(0,-1) = up, (0,1) = right, (0,1) = down, (-1,0) = left
self.speed = 120
def up(self):
self.vector = (-1,0)
def right(self):
self.vector = (0,1)
def down(self):
self.vector = (1,0)
def left(self):
self.vector = (0,-1)
def relocate_food(self):
x = rd.randint(0,self.n)
y = rd.randint(0,self.m)
i = 0
test = True
while i<self.lenght and test:
if (x,y) == self.body[i]:
test = False
self.relocate_food()
else:
i += 1
if i == self.lenght:
self.food = (x,y)
def collide(self):
head = self.body[0]
for i in range(1,self.lenght):
if head == self.body[i]:
self.Game = -1
break
x,y = head
if x>=self.n or y>=self.m or x<0 or y<0:
self.Game = -1
def eat(self):
head = self.body[0]
if head == self.food:
self.lenght +=1
x0, y0 = self.body[-1]
x1, y1 = self.body[-2]
x = x0 - x1
y = y0 - y1
self.body.append((x0+x,y0+y))
self.relocate_food()
if self.lenght%5 == 0:
self.speed = int(self.speed * 0.90)
def move(self):
dx, dy = self.vector
last_x, last_y = self.body[0]
new_x = last_x + dx
new_y = last_y + dy
self.body[0] = (new_x, new_y)
for k in range(1, self.lenght):
x, y = self.body[k]
self.body[k] = (last_x,last_y)
last_x, last_y = x, y
return
class screen(snake):
def __init__(self,root,n,m):
snake.__init__(self,n,m)
root.minsize(n*20,m*20)
root.maxsize(n*20,m*20)
root.configure(background='white')
self.root = root
self.n = n
self.m = m
self.speed = 130
self.canvas = tk.Canvas(root, width = n*20, height =m*20,bg='black')
self.canvas.bind_all("<Key-Up>",self.move_up)
self.canvas.bind_all("<Key-Down>",self.move_down)
self.canvas.bind_all("<Key-Left>",self.move_left)
self.canvas.bind_all("<Key-Right>",self.move_right)
self.canvas.grid(row=1,column=0)
self.draw_snake()
self.draw_food()
def draw_snake(self):
y,x = self.body[0]
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'red4')
for k in range(1,self.lenght):
y,x = self.body[k]
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'red')
def draw_food(self):
y,x =self.food
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'green')
def move_up(self,event):
if self.Game == -2:
self.Game =0
self.up()
self.update()
else:
self.up()
def move_down(self,event):
if self.Game == -2:
self.Game =0
self.down()
self.update()
else:
self.down()
def move_left(self,event):
if self.Game == -2:
self.Game =0
self.left()
self.update()
else:
self.left()
def move_right(self,event):
if self.Game == -2:
self.Game =0
self.right()
self.update()
else:
self.right()
def update(self):
if self.Game == -2:
return
self.move()
self.eat()
self.collide()
if self.Game == -1:
self.root.destroy()
return
self.canvas.delete("all")
self.draw_snake()
self.draw_food()
self.root.after(self.speed,self.update)
window = tk.Tk()
snake = screen(window,35,35)
snake.update()
window.mainloop()

This is not really a bug. Your animation uses an 'update' function that is executed every 120ms. So if you hit 2 arrow keys within 120ms (i.e. between two successive calls of 'update'), only the last hit is considered, because only one translation vector can be considered for each snake update. Nobody can blame you on that point, as time controlled animation is a discrete process with a given time window. It's the only solution to get fluid and regular animation (all video games are based on such a process), so that's clearly correct.
However, your code may still be improved on several aspects. For instance, at each animation frame, you delete all Canvas items and create a whole new set of items ('create_rectangle') for the snake elements and the food. This is not very efficient. It would be better to simply change the coordinates of the items (check the Canvas.coords function from the doc). Note that animating a snake simply requires to move the previous tail position to the new head position, to give the illusion of a moving beast. So moving only 1 item (2 items when eating food) is necessary at each frame, which is must faster to process.

Thank Furas for the basic idea it was what i needed. New code with my correction :
def __init__(self, root,n,m):
"""
"""
self.input = []
"""
"""
def move_up(self,event):
if self.Game == -2:
self.Game =0
self.up()
self.update()
else:
self.input.append(0)
"""
Same for all the move
"""
def update(self):
if self.Game == -2:
return
if len(self.input)>3: #Make sure that the player doesn't stack instruction
self.pop()
try:
input = self.input.pop(0)
except:
input = -1
if input == 0:
self.up()
elif input == 1:
self.right()
elif input == 2:
self.down()
elif input == 3:
self.left()
self.move()
self.eat()
self.collide()
if self.Game == -1:
self.root.destroy()
return
self.canvas.delete("all")
self.draw_snake()
self.draw_food()
self.root.after(self.speed,self.update)

Related

Python - easily converting float to rounded number for set checking (turtle graphics)

I'm having an issue with my snakegame on the collision detection side. What I wrote originally to do so was this:
snakecollisionchecklist = len(snake.snake_coord_list)
snakecollisionchecklistset = len(set(snake.snake_coord_list))
if snakecollisionchecklist != snakecollisionchecklistset:
return
The idea being that if any segment position in the snake was equal to another segment position it would abort the program. The problem I found was that the .position() function I was using to find the position of each segment returned a 10 decimal float value, and for whatever reason this wasn't consistent even if the snake was sharing the same place. Converting it to int doesnt work either as that sometimes will make 60 59 etc. if the variance is high/low. To deal with this I put this together, but I feel like this isn't the most efficient way to handle this:
values_to_convert = segments.position()
xvaluestoconvert = round(values_to_convert[0],2)
yvaluestoconvert = round(values_to_convert[1],2)
self.snake_coord_list[segloc] = (xvaluestoconvert, yvaluestoconvert)
This just takes the individual pieces of the position() and forces it to be rounded. I also had a similar issue with trying to make food not spawn inside the snake, which ended up looking like this:
newloc= positionlist[0]
while newloc in positionlist:
randomx = round((random.randint(-7,7) * 20))
randomy = round((random.randint(-7,7) * 20))
newloc = [randomx, randomy]
self.goto(float(randomx),float(randomy))
print(f"{randomx} and {randomy}")
But then I still get overlap.
Is there a better way to do this? If you're curious about full code it's here:
gamesetup.py
import turtle as t
class FullSnake:
def __init__(self) -> None:
self.snake_part_list = []
self.snake_coord_list = [(-40,0),(-60,0), (-80,0)]
self.moment_prior_coord_list = [(-40,0),(-60,0), (-80,0)]
for nums in range(3):
new_segment = t.Turtle("circle")
new_segment.penup()
new_segment.color("white")
self.snake_part_list.append(new_segment)
new_segment.goto(self.snake_coord_list[nums])
self.snake_head = self.snake_part_list[0]
self.headingverification = 0
def add_segment(self,):
"""This adds a segment to the snake. the segment added should be initialized to be add the end of the list."""
new_segment = t.Turtle("circle")
new_segment.penup()
new_segment.color("white")
self.snake_part_list.append(new_segment)
#self.snake_coord_list.append((self.snake_part_list[0].xcor(),self.snake_part_list[0].ycor()))
#self.snake_coord_list.append(new_segment)
# current_final_seg = self.snake_part_list[-2]
current_final_seg_pos = self.moment_prior_coord_list[-1]
#self.move_snake()
new_segment.goto(current_final_seg_pos[0],current_final_seg_pos[1])
self.snake_coord_list.append(current_final_seg_pos)
def right(self):
if self.headingverification != 180:
self.snake_head.setheading(0)
# time.sleep(0.031)
# self.move_snake()
#ime.sleep(0.05)
def up(self):
if self.headingverification != 270:
self.snake_head.setheading(90)
# time.sleep(0.031)
#self.move_snake()
def left(self):
if self.headingverification != 0:
self.snake_head.setheading(180)
# time.sleep(0.031)
# self.move_snake()
def down(self):
if self.headingverification != 90:
self.snake_head.setheading(270)
#time.sleep(0.031)
# self.move_snake()
def move_snake(self):
"""moves snake. snake moves forward 20 units, and prior units get updated"""
self.moment_prior_coord_list = list(self.snake_coord_list)
for seg_num in range(len(self.snake_part_list)-1,0,-1):
new_x = round(self.snake_part_list[seg_num-1].xcor(),2)
new_y = round(self.snake_part_list[seg_num-1].ycor(),2)
self.snake_part_list[seg_num].goto(new_x, new_y)
self.snake_head.forward(20)
#print(self.snake_head.position())
for segments in self.snake_part_list:
segloc = self.snake_part_list.index(segments)
#for some reason segments.position() a varied float, so this just forces it to be samesies
values_to_convert = segments.position()
xvaluestoconvert = round(values_to_convert[0],2)
yvaluestoconvert = round(values_to_convert[1],2)
self.snake_coord_list[segloc] = (xvaluestoconvert, yvaluestoconvert)
print(self.snake_coord_list)
main.py:
import turtle as t
from gamesetup import FullSnake
import time
import food
import score_board as sb
screen = t.Screen()
screen.setup(food.screensize[0],food.screensize[1])
screen.bgcolor("Black")
screen.title("My Snake Game")
screen.tracer(0)
snakefood = food.Food()
snake = FullSnake()
scoreboard = sb.ScoreBoard()
screen.listen()
screen.onkey(snake.up,"Up")
screen.onkey(snake.down,"Down")
screen.onkey(snake.right,"Right")
screen.onkey(snake.left,"Left")
#game_is_on = True
#while game_is_on:
def snakemovefct():
snake.move_snake()
screen.update()
#what happens when you hit food, add to length of snake, increase score and move pellet to place that snake isnt
if snake.snake_head.distance(snakefood) <5:
snake.add_segment()
snakefood.refresh(snake.snake_coord_list)
scoreboard.score_event()
screen.update()
time.sleep(0.1)
#set gameover if you hit boundary
if snake.snake_head.xcor() > 150 or snake.snake_head.xcor() < -150 or snake.snake_head.ycor() > 150 or snake.snake_head.ycor() < -150:
scoreboard.game_over()
return
#check collision
snakecollisionchecklist = len(snake.snake_coord_list)
snakecollisionchecklistset = len(set(snake.snake_coord_list))
if snakecollisionchecklist != snakecollisionchecklistset:
scoreboard.game_over()
return
#this makes sure you cant press up and left when moving right to go left
snake.headingverification = snake.snake_head.heading()
#keep snake moving in loop, if no recursion it only moves once
screen.ontimer(snakemovefct,150)
screen.update()
screen.ontimer(snakemovefct,150)
screen.mainloop()
food.py
from turtle import Turtle
import random
screensize = (340, 340)
class Food(Turtle):
def __init__(self) -> None:
super().__init__()
self.shape("square")
self.penup()
#self.shapesize(stretch_len=0.5, stretch_wid=0.5)
self.color("red")
self.speed("fastest")
self.refresh([(20,0),(0,0), (-20,0)])
def refresh(self, positionlist):
newloc= positionlist[0]
while newloc in positionlist:
randomx = "{:.2f}".format(random.randint(-7,7) * 20)
randomy = "{:.2f}".format(random.randint(-7,7) * 20)
newloc = [randomx, randomy]
self.goto(float(randomx),float(randomy))
print(f"{randomx} and {randomy}")
scoreboard.py
import turtle as t
class ScoreBoard(t.Turtle):
def __init__(self) -> None:
super().__init__()
self.hideturtle()
self.penup()
self.pencolor("white")
self.speed("fastest")
self.score = 0
self.goto(0,120)
self.write(f"Current score: {self.score}", False, align="center")
def score_event(self):
self.score +=1
self.clear()
self.write(f"Current score: {self.score}", False, align="center")
def game_over(self):
self.goto(0,0)
self.write("GAME OVER", False, align="center")

Lag while drawing lines with the Python Arcade Library

I've been migrating from Pygame to Arcade and overall it's much better. That said, the method I was using to draw the lines of my track for my car game in Pygame is exorbitantly laggy in Arcade. I know it's lagging from drawing all the lines, I'm just wondering if there's a better way to do it that doesn't cause so much lag.
import arcade
import os
import math
import numpy as np
SPRITE_SCALING = 0.5
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Move Sprite by Angle Example"
MOVEMENT_SPEED = 5
ANGLE_SPEED = 5
class Player(arcade.Sprite):
""" Player class """
def __init__(self, image, scale):
""" Set up the player """
# Call the parent init
super().__init__(image, scale)
# Create a variable to hold our speed. 'angle' is created by the parent
self.speed = 0
def update(self):
# Convert angle in degrees to radians.
angle_rad = math.radians(self.angle)
# Rotate the ship
self.angle += self.change_angle
# Use math to find our change based on our speed and angle
self.center_x += -self.speed * math.sin(angle_rad)
self.center_y += self.speed * math.cos(angle_rad)
def upgraded_distance_check(player_sprite, point_arrays, distance_cap):
center_coord = player_sprite.center
intersect_array = []
distance_array = []
sensor_array = player_sprite.points
for sensor in sensor_array:
intersect_array.append([-10000, -10000])
for point_array in point_arrays:
for i in range(len(point_array[:-1])):
v = line_intersection(
[sensor, center_coord], [point_array[i], point_array[i + 1]]
)
if v == (None, None):
continue
if (
(point_array[i][0] <= v[0] and point_array[i + 1][0] >= v[0])
or (point_array[i][0] >= v[0] and point_array[i + 1][0] <= v[0])
) and (
(point_array[i][1] <= v[1] and point_array[i + 1][1] >= v[1])
or (point_array[i][1] >= v[1] and point_array[i + 1][1] <= v[1])
):
if intersect_array[-1] is None or math.sqrt(
(intersect_array[-1][0] - center_coord[0]) ** 2
+ (intersect_array[-1][1] - center_coord[1]) ** 2
) > math.sqrt(
(v[0] - center_coord[0]) ** 2
+ (v[1] - center_coord[1]) ** 2
):
if not is_between(sensor, center_coord, v):
intersect_array[-1] = v
for i in range(len(sensor_array)):
if distance(sensor_array[i], intersect_array[i]) > distance_cap:
intersect_array[i] = None
distance_array.append(None)
else:
distance_array.append(distance(sensor_array[i], intersect_array[i]))
return intersect_array
class MyGame(arcade.Window):
"""
Main application class.
"""
def __init__(self, width, height, title):
"""
Initializer
"""
# Call the parent class initializer
super().__init__(width, height, title)
# Set the working directory (where we expect to find files) to the same
# directory this .py file is in. You can leave this out of your own
# code, but it is needed to easily run the examples using "python -m"
# as mentioned at the top of this program.
file_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(file_path)
# Variables that will hold sprite lists
self.player_list = None
# Set up the player info
self.player_sprite = None
# Set the background color
arcade.set_background_color(arcade.color.WHITE)
def setup(self):
""" Set up the game and initialize the variables. """
# Sprite lists
self.player_list = arcade.SpriteList()
# Set up the player
self.player_sprite = Player(":resources:images/space_shooter/playerShip1_orange.png", SPRITE_SCALING)
self.player_sprite.center_x = SCREEN_WIDTH / 2
self.player_sprite.center_y = SCREEN_HEIGHT / 2
self.player_list.append(self.player_sprite)
#Setup all the array BS
self.track_arrays = []
self.drawing = False
def on_draw(self):
"""
Render the screen.
"""
# This command has to happen before we start drawing
arcade.start_render()
# Draw all the sprites.
# for i, ele in enumerate(self.player_sprite.points):
# arcade.draw_circle_filled(ele[0], ele[1], 7, arcade.color.AO)
self.player_list.draw()
if len(self.track_arrays) > 0 and len(self.track_arrays[0]) > 2:
for track_array in self.track_arrays:
for i in range(len(track_array[:-1])):
arcade.draw_line(track_array[i][0], track_array[i][1], track_array[i+1][0], track_array[i+1][1], arcade.color.BLACK, 1)
def on_update(self, delta_time):
""" Movement and game logic """
# Call update on all sprites (The sprites don't do much in this
# example though.)
self.player_list.update()
# print(self.drawing)
# print(self.player_sprite.points)
def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed. """
# Forward/back
if key == arcade.key.UP:
self.player_sprite.speed = MOVEMENT_SPEED
elif key == arcade.key.DOWN:
self.player_sprite.speed = -MOVEMENT_SPEED
# Rotate left/right
elif key == arcade.key.LEFT:
self.player_sprite.change_angle = ANGLE_SPEED
elif key == arcade.key.RIGHT:
self.player_sprite.change_angle = -ANGLE_SPEED
def on_key_release(self, key, modifiers):
"""Called when the user releases a key. """
if key == arcade.key.UP or key == arcade.key.DOWN:
self.player_sprite.speed = 0
elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
self.player_sprite.change_angle = 0
def on_mouse_press(self, x, y, button, modifiers):
"""
Called when the user presses a mouse button.
"""
self.track_arrays.append([])
self.drawing = True
def on_mouse_release(self, x, y, button, modifiers):
"""
Called when a user releases a mouse button.
"""
self.drawing = False
def on_mouse_motion(self, x, y, dx, dy):
""" Called to update our objects. Happens approximately 60 times per second."""
if self.drawing:
self.track_arrays[-1].append([x,y])
def main():
""" Main method """
window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()
if __name__ == "__main__":
main()

Managing when Python turtle checks for events

I'm working on creating a Connect 4 game using Python turtle graphics. The main problem that has arisen is that when you use events like onkey or onclick, they are checked instantaneously followed by the remainder of the code. Is there a way to pause the code until an event happens, then continue on and, after looping, wait for another event to happen?
In the following code, the game gets setup and then the play() function is run. Within the play() function the program listens for onkey() events that allow the user to change which column it is over and drop the piece when ready. Then, it begins checking for 4 in a row either horizontally, vertically, or diagonally. It creates an error because the column lists are empty until I press the down key to drop a piece into a column of the board and append the piece to that column. I could create the columns already filled with None, '', or zeros but then I would have to change how my drop function works, as it currently bases the y value of where it is dropped to off of the number of items in the list. Is there a way to only run the check function once after each piece is dropped?
P.S. I'm relatively new to coding and this is my first time using this site. I have copy and pasted the code below:
import turtle
class Connect4:
"A connect 4 game"
def __init__(self):
self.pen = turtle.Turtle()
self.scr = turtle.Screen()
self.board = Connect4Board(self.pen, self.scr)
self.moves = 0
self.playing = True
self.piece = Connect4Piece(self.scr, self.board, self)
self.setup()
self.play()
def setup(self):
self.board.draw_board()
def play(self):
if self.moves == self.board.rows*self.board.columns:
game_over()
self.piece.st()
self.piece.goto(0, self.board.board_height/2)
while True:
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
self.check()
"Check if there is 4 pieces in a line horizontally, vertically or diagonally"
def check(self):
self.check_horizontal()
self.check_vertical()
self.check_diagonal()
def check_horizontal(self):
print("Checking horizontally")
for rows in range(self.board.rows):
for columns in range(self.board.columns - 3):
if self.board.squares[columns][rows] == 0:
continue
elif (self.board.squares[columns][rows] == self.board.squares[columns+1][rows] == self.board.squares[columns+2][rows] == self.board.squares[columns+3][rows]):
print(self.board.squares[columns][rows].color())
if self.board.squares[columns][rows].color() == ('red','red'):
print("Red wins!")
if self.board.squares[columns][rows].color() == ('black','black'):
print("Black wins!")
def check_vertical(self):
print("Checking vertically")
def check_diagonal(self):
print("Checking diagonally")
def reset(self):
self.board.reset()
self.piece.clear()
self.moves = 0
self.play()
def game_over(self):
self.pen.pu()
self.pen.goto(0, board_height/2 + 20)
self.pen.pd()
self.pen.write("Black wins!", align='center', font = ('Arial', 24, 'normal'))
self.pen.pu()
to.goto(0, board_height/2 + 10)
self.pen.write("Play Again?", align='center', font = ('Arial', 24, 'normal'))
self.playing = False
class Connect4Board:
def __init__(self, pen, screen):
#Used to create the board
self.square_size = 60
self.rows = 6
self.columns = 7
self.pen = pen
self.frame_color = 'blue'
self.board_length = self.square_size*self.columns
self.board_height = self.square_size*self.rows
self.squares = [[] for cols in range(self.columns)]
"""for cols in range(self.columns):
empty = []
self.squares.append(empty)"""
self.pen.speed(0)
self.pen.ht()
def _draw_square(self, x, y):
self.pen.pu()
self.pen.goto(x-self.square_size/2, y-self.square_size/2)
self.pen.pd()
self.pen.fillcolor(self.frame_color)
self.pen.begin_fill()
for sides in range(4):
self.pen.fd(self.square_size)
self.pen.left(90)
self.pen.end_fill()
def _draw_circle(self, x, y):
self.pen.pu()
self.pen.goto(x, y)
self.pen.pd()
self.pen.fillcolor('white')
self.pen.begin_fill()
self.pen.circle(self.square_size/2)
self.pen.end_fill()
def draw_board(self):
for row in range(self.rows):
for col in range(self.columns):
x = col*self.square_size - self.board_length/2 + self.square_size/2
y = row*self.square_size - self.board_length/2
self._draw_square(x, y)
self._draw_circle(x, y - self.square_size/2)
def reset(self):
self.squares = []
for cols in range(self.columns):
empty = []
self.squares.append(empty)
class Connect4Piece(turtle.Turtle):
def __init__(self, screen, board, game):
turtle.Turtle.__init__(self, screen)
self.board = board
self.speed(0)
self.pu()
self.shape('turtle')
self.cnum = 3
self.game = game
self.ht()
"Moves the piece to the left and updates it's column number"
def prev_col(self):
if self.xcor() - self.board.square_size > -self.board.board_length/2:
self.setx(self.xcor() - self.board.square_size)
self.cnum -= 1
"Moves the piece to the right and updates it's column number"
def next_col(self):
if self.xcor() + self.board.square_size < self.board.board_length/2:
self.setx(self.xcor() + self.board.square_size)
self.cnum += 1
def drop(self):
"Make sure the column isn't full. If it's not then move the turtle to the next available space in the row."
if len(self.board.squares[self.cnum]) != self.board.rows:
self.sety(len(self.board.squares[self.cnum]) *self.board.square_size - self.board.board_height/2 - self.board.square_size/2 )
"Stamp an image of the turtle to represent placing a piece"
self.stamp()
self.board.squares[self.cnum].append(self.color())
"Move the piece back above the middle column and set it's column back to 3"
self.goto(0, self.board.board_height/2)
self.cnum = 3
"Change the piece's color"
if self.color() == ('red','red'):
self.color('black')
else:
self.color('red')
self.game.moves += 1
print(self.game.moves, "moves")
game = Connect4()
Your program is structured incorrectly, as epitomized by this loop:
while True:
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
self.check()
First, while True: has no place in an event-driven environment like turtle. Second, these onkey calls only need to be done once during initialization -- they don't do anything at runtime. (Ditto listen())
I've restructured your code below to be event-based. You need to add (back) the checking code to determine if there's a winner or not:
from turtle import Turtle, Screen
class Connect4:
"A connect 4 game"
def __init__(self, screen):
self.pen = Turtle()
self.scr = screen
self.board = Connect4Board(self.pen)
self.moves = 0
self.playing = False
self.piece = Connect4Piece(self.board, self)
self.scr.tracer(False)
self.board.draw_board()
self.scr.tracer(True)
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
def play(self):
self.piece.showturtle()
self.piece.goto(0, self.board.board_height/2)
self.playing = True
def check(self):
"Check if there are 4 pieces in a line horizontally, vertically or diagonally"
if self.moves == self.board.rows * self.board.columns:
self.game_over()
if self.check_horizontal():
self.game_over()
if self.check_vertical():
self.game_over()
if self.check_diagonal():
self.game_over()
def check_horizontal(self):
print("Checking horizontally")
# implement this correctly
return False
def check_vertical(self):
print("Checking vertically")
# implement this
return False
def check_diagonal(self):
print("Checking diagonally")
# implement this
return False
def reset(self):
self.playing = False
self.board.reset()
self.piece.clear()
self.moves = 0
self.play()
def game_over(self):
self.playing = False
self.pen.penup()
self.pen.goto(0, self.board.board_height/2 + 20)
self.pen.pendown()
self.pen.write("Black wins!", align='center', font=('Arial', 24, 'normal'))
self.pen.penup()
self.pen.goto(0, self.board.board_height/2 + 10)
self.pen.write("Play Again?", align='center', font=('Arial', 24, 'normal'))
class Connect4Board:
def __init__(self, pen):
# Used to create the board
self.square_size = 60
self.rows = 6
self.columns = 7
self.pen = pen
self.frame_color = 'blue'
self.board_length = self.square_size * self.columns
self.board_height = self.square_size * self.rows
self.squares = [[] for _ in range(self.columns)]
self.pen.speed('fastest')
self.pen.hideturtle()
def _draw_square(self, x, y):
self.pen.penup()
self.pen.goto(x - self.square_size/2, y - self.square_size/2)
self.pen.pendown()
self.pen.fillcolor(self.frame_color)
self.pen.begin_fill()
for _ in range(4):
self.pen.forward(self.square_size)
self.pen.left(90)
self.pen.end_fill()
def _draw_circle(self, x, y):
self.pen.penup()
self.pen.goto(x, y)
self.pen.pendown()
self.pen.fillcolor('white')
self.pen.begin_fill()
self.pen.circle(self.square_size/2)
self.pen.end_fill()
def draw_board(self):
for row in range(self.rows):
for col in range(self.columns):
x = col * self.square_size - self.board_length/2 + self.square_size/2
y = row * self.square_size - self.board_length/2
self._draw_square(x, y)
self._draw_circle(x, y - self.square_size/2)
def reset(self):
self.squares = [[] for _ in range(self.columns)]
class Connect4Piece(Turtle):
def __init__(self, board, game):
super().__init__(shape='turtle', visible=False)
self.board = board
self.game = game
self.speed('fastest')
self.penup()
self.cnum = 3
def prev_col(self):
"Moves the piece to the left and updates it's column number"
if self.xcor() - self.board.square_size > -self.board.board_length/2:
self.setx(self.xcor() - self.board.square_size)
self.cnum -= 1
def next_col(self):
"Moves the piece to the right and updates it's column number"
if self.xcor() + self.board.square_size < self.board.board_length/2:
self.setx(self.xcor() + self.board.square_size)
self.cnum += 1
def drop(self):
"Make sure the column isn't full. If it's not then move the turtle to the next available space in the row."
if len(self.board.squares[self.cnum]) != self.board.rows:
self.sety(len(self.board.squares[self.cnum]) * self.board.square_size - self.board.board_height/2 - self.board.square_size/2)
# Stamp an image of the turtle to represent placing a piece
self.stamp()
self.board.squares[self.cnum].append(self.color())
# Move the piece back above the middle column and set it's column back to 3
self.goto(0, self.board.board_height/2)
self.cnum = 3
# Change the piece's color
self.color('black' if self.pencolor() == 'red' else 'red')
self.game.moves += 1
print(self.game.moves, "moves")
self.game.check()
screen = Screen()
game = Connect4(screen)
game.play()
screen.mainloop()
The graphics behave as expected but when you drop a turtle you'll see the stub checking functions get invoked and the player switches.
Also, read about Python comments vs. document strings -- you're mixing them up.

How to detect if a key is being held down in Tkinter?

As a novice when it comes to Python, I've tried programming my own game to start, with the advice of a guidebook. However, for this game, I'm trying to detect when a key is held down consistently instead of just pressed. The current code I'm using doesn't make the character move, and without the halt(self, evt) code being implemented, causes the ship to speed up uncontrollably after the button is held down for long enough.
from tkinter import *
import random
import time
class Game:
def __init__(self):
self.tk = Tk()
self.tk.title("Shooter")
self.tk.resizable(0, 0)
self.tk.wm_attributes("-topmost", 1)
self.canvas = Canvas(self.tk, width=500, height=1000, highlightthickness=0)
self.canvas.pack()
self.tk.update()
self.canvas_height = 1000
self.canvas_width = 500
self.bg = PhotoImage(file="background.gif")
w = self.bg.width()
h = self.bg.height()
for x in range(0, 5):
for y in range(0, 10):
self.canvas.create_image(x * w, y * h, \
image=self.bg, anchor='nw')
self.sprites = []
self.running = True
def mainloop(self):
while 1:
if self.running == True:
for sprite in self.sprites:
sprite.move()
self.tk.update_idletasks()
self.tk.update()
time.sleep(0.01)
class Coords:
def __init__(self, x1=0, y1=0, x2=0, y2=0):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
class Sprite:
def __init__(self, game):
self.game = game
self.endgame = False
self.coordinates = None
def move(self):
pass
def coords(self):
return self.coordinates
class PlayerSprite(Sprite):
def __init__(self, game):
Sprite.__init__(self, game)
self.renderimage = [
PhotoImage(file="player_1.gif"),
PhotoImage(file="player_2.gif"),
PhotoImage(file="player_3.gif"),
PhotoImage(file="player_4.gif"),
]
self.image = game.canvas.create_image(225, 900, \
image=self.renderimage[0], anchor='nw')
self.x = 0
self.y = 0
self.velx = 0
self.current_image = 0
self.current_image_add = 1
self.shoot_timer = 0
self.last_time = time.time()
self.coordinates = Coords()
x_move = None
y_move = None
game.canvas.bind_all('<KeyPress-Left>', self.move_left)
game.canvas.bind_all('<KeyPress-Right>', self.move_right)
game.canvas.bind_all('<KeyPress-Up>', self.move_up)
game.canvas.bind_all('<KeyPress-Down>', self.move_down)
game.canvas.bind_all('<KeyPress-Left>', self.halt)
game.canvas.bind_all('<KeyPress-Right>', self.halt)
game.canvas.bind_all('<KeyPress-Up>', self.halt)
game.canvas.bind_all('<KeyPress-Down>', self.halt)
game.canvas.bind_all('<space>', self.shoot)
def move_left(self, evt):
x_move = self.x - 1.5
self.x = x_move
def move_right(self, evt):
x_move = self.x + 1.5
self.x = x_move
def move_up(self, evt):
y_move = self.y - 1.5
self.y = y_move
def move_down(self, evt):
y_move = self.y + 1.5
self.y = y_move
def halt(self, evt):
time.sleep(0.01)
if x_move < 0:
x_move = -1.5
elif x_move > 0:
x_move = 1.5
elif y_move < 0:
y_move = -1.5
elif y_move > 0:
y_move = 1.5
def shoot(self, evt):
print("Placeholder")
def move(self):
self.game.canvas.move(self.image, self.x, self.y)
def coords(self):
xy = self.game.canvas.coords(self.image)
self.coordinates.x1 = xy[0]
self.coordinates.y1 = xy[1]
self.coordinates.x2 = xy[0] + 24
self.coordinates.y2 = xy[1] + 32
return self.coordinates
g = Game()
sp = PlayerSprite(g)
g.sprites.append(sp)
g.mainloop()
My goal is to have my character move at a constant rate (as opposed to uncontrollably fast after a while) when the respective key is pressed.
The most straightforward solution to your question would be to avoid adding a value at every keypress, but rather set a constant value.
def move_left(self, evt):
x_move = -5
self.x = x_move
The movement would however lose its dynamic, but it will be constant. Otherwise, you could create a max value. Something like this:
def move_left(self, evt):
int max_val_left = -10
if( self.x < max_val_left):
x_move = self.x - 1.5
self.x = x_move
Thereby forcing self.x to remain capped and constant if it has reached the max_val.
Holding down a key is essentially the same as pressing that key repeatedly. What you're doing by adding to/subtracting from the self.x/self.y attributes in your move_* functions is you're increasing the amount that the canvas will move your player sprite in each function call (e.g. from 1.5 to 3 to 4.5 to 6, etc. as you hold down a direcitonal key).
Since the canvas will be moving your player by (self.x, self.y) units every time "move" is called under the "PlayerSprite" class, we want self.x and self.y to be either 0 or whatever speed you desire (1.5 in the following code). So instead of adding to self.x and self.y, we should assign it to a constant value:
def move_left(self, evt):
self.x = -1.5
def move_right(self, evt):
self.x = 1.5
def move_up(self, evt):
self.y = -1.5
def move_down(self, evt):
self.y = -1.5
Also, instead of using "halt", what you could do is include 'KeyRelease-*' bindings to stop your player once you've stopped holding down a directional key:
game.canvas.bind_all('KeyRelease-Left'>, self.stop_horz_move)
game.canvas.bind_all('KeyRelease-Right'>, self.stop_horz_move)
game.canvas.bind_all('KeyRelease-Up'>, self.stop_vert_move)
game.canvas.bind_all('KeyRelease-Down'>, self.stop_vert_move)
(I've generalized the left and right directions to horz as well as up and down to vert to save on the number of function definitions.)
Then you can create functions that assign your self.x value or self.y value to 0, so that your player doesn't move once "move" is called.
def stop_move_horz(self, evt):
self.x = 0
def stop_move_vert(self, evt):
self.y = 0

Method body not executing?

I'm running this python code and having a problem with the accel function. The rotate method works fine when left and right are pressed however when up is pressed nothing happens. I've stepped through the code in a debugger and the my_ship.accel line is executed but it doesn't go to method body, it just continues as if that line isn't there. Idk what's wrong please help. Also my_ship is the name of a Ship object and it is defined properly lower in my code.
import simplegui
WIDTH = 800
HEIGHT = 600
class ImageInfo:
def __init__(self, center, size, radius = 0, lifespan = None, animated = False):
self.center = center
self.size = size
self.radius = radius
if lifespan:
self.lifespan = lifespan
else:
self.lifespan = float('inf')
self.animated = animated
def get_center(self):
return self.center
def get_size(self):
return self.size
def get_radius(self):
return self.radius
def get_lifespan(self):
return self.lifespan
def get_animated(self):
return self.animated
def change_center(self, new_center):
self.center = new_center
# ship image
ship_info = ImageInfo([45, 45], [90, 90], 35)
ship_image = simplegui.load_image("http://commondatastorage.googleapis.com/codeskulptor-assets/lathrop/double_ship.png")
class Ship:
def __init__(self, pos, vel, angle, image, info):
self.pos = [pos[0],pos[1]]
self.vel = [vel[0],vel[1]]
self.thrust = False
self.angle = angle
self.angle_vel = 0
self.image = image
self.image_center = info.get_center()
self.image_size = info.get_size()
self.radius = info.get_radius()
self.info = info
self.accel = 10
self.angle_accel = .1
def draw(self,canvas):
if not self.thrust:
self.info.change_center(ship_center)
canvas.draw_image(self.image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
else:
self.info.change_center(thrust_ship_center)
canvas.draw_image(self.image, self.image_center, self.image_size, self.pos, self.image_size, self.angle)
def update(self):
self.pos[0] += self.vel[0]
self.pos[1] += self.vel[1]
self.angle += self.angle_vel
def accel(self):
self.thrust = True
self.vel[0] += self.accel
self.vel[1] += self.accel
def rotate(self, direction):
if direction == "left":
self.angle_vel -= self.angle_accel
elif direction == "right":
self.angle_vel += self.angle_accel
else:
print "error"
def keydown_handler(key):
if key == simplegui.KEY_MAP['left']:
my_ship.rotate("left")
elif key == simplegui.KEY_MAP['right']:
my_ship.rotate("right")
elif key == simplegui.KEY_MAP['up']:
my_ship.accel
elif key == simplegui.KEY_MAP['space']:
self.angle_vel += self.angle_accel
def keyup_handler(key):
if key == simplegui.KEY_MAP['left']:
my_ship.rotate("right")
elif key == simplegui.KEY_MAP['right']:
my_ship.rotate("left")
my_ship = Ship([WIDTH / 2, HEIGHT / 2], [0, 0], 1, ship_image, ship_info)
This:
my_ship.accel
Doesn't call the method my_ship.accel, any more than 2 calls the number 2. To call something in Python, you need parentheses. So:
my_ship.accel()
(If you're wondering why Python does it this way when other languages, like Ruby, don't… well, this means that you can use the method object my_ship.accel as a value—store it to call later, pass it to map, etc.)
But you've got another problem on top of that.
You define a method accel on Ship objects. But you also assign an integer value 10 to self.accel on Ship objects. There's no way self.accel can mean two different things at once, both the method and the number. So, which one "wins"? In this case, the self.accel = 10 happens at the time you constructed your Ship, which is later, so it wins.
So, when you write my_ship.accel, you're just referring to the number 10. And when you write my_ship.accel(), you're trying to call the number 10 as if it were a function. Hence the TypeError.
The solution is to not reuse the same name for two different things. Often, naming functions after verbs and attributes after nouns is a good way to avoid this problem—although you also have to avoid gratuitous abbreviations, because otherwise you're probably going to abbreviate acceleration and accelerate to the same accel, as you did here.

Categories