Tkinter Tic Tac Toe Drawing a shape in a certain box - python

I'm a newbie in programming, trying to learn python and I decided to make Tic Tac Toe for my first project. I've made functions for drawing X and O, but I have trouble drawing them in the box I'm clicking. Here's the code so far:
ttt = Tk()
ttt.title("Tic Tac Toe")
w = Canvas(ttt, width = 902, height = 902)
w.configure (bg = "white")
w.pack()
m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
def drawx(event):
x, y = event.x, event.y
w.create_line(49, 49, 249, 249, fill = "black")
w.create_line(49, 249, 249, 49, fill = "black")
def drawo(event):
x2, y2 = event.x, event.y
x0 = x2 - 100
y0 = y2 - 100
x1 = x2 + 100
y1 = y2 + 100
return w.create_oval(x0, y0, x1, y1)
w.create_line(0, 300, 901, 300, fill = "black")
w.create_line(0, 601, 901, 601, fill = "black")
w.create_line(300, 0, 300, 901, fill = "black")
w.create_line(601, 0, 601, 901, fill = "black")
Here, the shapes will be drawn based on my cursor coordinates, which I know should be modified. Any help or suggestion would be appreciated.

You can "discretize" the x and y values by integer-dividing and then multiplying with the width of the individual cells (and adding some offset for the center).
def drawo(event):
x2, y2 = event.x, event.y
x2 = x2 // 300 * 300 + 150
y2 = y2 // 300 * 300 + 150
...
Same for drawx.
Alternatively, you could use different canvas elements (or buttons, labels or similar) for the different cells in the grid and get the clicked widget from the event.

You need to bind the mouse click event to canvas:
w.bind('<Button-1>', on_click)
Then determine which cell is clicked in on_click handler:
SIZE = 300
player = 1 # 1 for O, 2 for X
def on_click(event):
global m
global player
row = event.y // SIZE
col = event.x // SIZE
# check whether the cell is not filled yet
if m[row][col] == 0:
# calculate the center of the cell
cx = col * SIZE + SIZE // 2
cy = row * SIZE + SIZE // 2
# draw X or O based on current player
if player == 1:
draw_O(cx, cy)
else:
draw_X(cx, cy)
# set cell is filled
m[row][col] = player
# now you need to check whether current player wins
...
# if no one wins, toggle player
player = 2 if player == 1 else 1
def draw_O(x, y):
radius = SIZE // 3
w.create_oval(x-radius, y-radius, x+radius, y+radius, width=5, tag='cell') # tag is used for resetting the game
def draw_X(x, y):
radius = SIZE // 3
w.create_line(x-radius, y-radius, x+radius, y+radius, width=5, tag='cell')
w.create_line(x+radius, y-radius, x-radius, y+radius, width=5, tag='cell')
If you want to reset the game:
def reset_game():
global m
global player
# remove all 'O' and 'X'
w.delete('cell')
m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
player = 1
For better design, put all the above logic in a class (inherited from Canvas). Then you can use instance variables instead of global variables.

To add more detail to tobias_k's answer, the below code will draw an X when the right mouse button is clicked and an O when the left mouse button is clicked.
An improvement in the code could be to use a variable to define the size of the canvas and each X/O rather than hard coding 200 or 300.
from tkinter import *
ttt = Tk()
ttt.title("Tic Tac Toe")
w = Canvas(ttt, width = 902, height = 902)
w.configure (bg = "white")
w.pack()
m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
def drawx(event):
print("drawx")
x, y = event.x, event.y
x0 = x // 300 * 300 + 50
y0 = y // 300 * 300 + 50
x1 = x0 + 200
y1 = y0 + 200
#return w.create_oval(x0, y0, x1, y1)
w.create_line(x0,y0,x1,y1, fill = "black")
w.create_line(x0,y0+200,x1,y1-200, fill = "black")
def drawo(event):
print("drawo")
x, y = event.x, event.y
x0 = x // 300 * 300 + 50
y0 = y // 300 * 300 + 50
x1 = x0 + 200
y1 = y0 + 200
return w.create_oval(x0, y0, x1, y1)
w.create_line(0, 300, 901, 300, fill = "black")
w.create_line(0, 601, 901, 601, fill = "black")
w.create_line(300, 0, 300, 901, fill = "black")
w.create_line(601, 0, 601, 901, fill = "black")
w.bind('<Button-1>',drawo)
w.bind('<Button-3>',drawx)
ttt.mainloop()

Related

how to design a board in tkinter canvas

I want to design a 9x9 board in tkinter canvas. Each rectangle should have a width and height of 30 (pixels?). Do I always have to use the pixel coordinates for drawing shapes onto the canvas or is there a more relative way possible? For example, my board looks like this:
class TkCanvas(tk.Canvas):
RECT_WIDTH = 30
def __init__(self, parent, width=600, height=600, columns=9, rows=9):
super().__init__(parent, width=width, height=height)
self.columns=columns
self.rows=rows
self.board = [[None for col in range(columns)] for row in range(rows)]
def draw_board(self, x1=0, x2=0,y1=RECT_WIDTH,y2=RECT_WIDTH):
for col in range(self.columns):
for row in range(self.rows):
x1 = col * self.RECT_WIDTH
y1 = (self.rows-1-row) * self.RECT_WIDTH
x2 = x1 + self.RECT_WIDTH
y2 = y1 + self.RECT_WIDTH
tag = f"tile{col}{row}"
self.board[row][col] = self.create_rectangle(x1, y1, x2, y2, fill="white", tags=tag, outline="black")
self.tag_bind(tag,"<Button-1>", lambda e, i=col, j=row: self.get_location(e,i,j))
def get_location(self, event, i, j):
print (i, j)
def get_x_coord(self, x):
return x * self.RECT_WIDTH
def get_y_coord(self, y):
return y * self.RECT_WIDTH
Now when I want to draw a shape I get the exact coordinates x0,y0 first with get_x_coord and get_y_coord and then calculate x1 and y1 by adding the RECT_WIDTH.
Is there a cleaner way to draw the shapes onto the canvas? Something where I would only have to pass in the coordinates, eg. (4,5) and it would automatically draw it in the right rectangle or do I always have to make these calculations?
There are many ways to produce a grid board and yours works fine.
Using relative offsets to position squares and pieces is easy in tkinter Canvas,
just use canvas.move(itemID, xoffset, yoffset). You can also move or scale the
entire board by using canvas.addtag_all('somename') then canvas.scale('somename', 0, 0, s, s). Where s is a float s > 0
The following code creates class drawBoard that can be instantiated using
just two values or by using many control values and demonstrates how to use
relative coordinates to build a scalable board.
The board is created by drawing all rectangles at (0, 0, w, h) then moving them
to location via relative values (x, y).
Once all squares have been created the entire board is scaled to size.
Method get_location displays user input.
import tkinter as tk
back, fore, draw, wall, light = "white", "blue", "red", "black", "yellow"
class drawBoard(tk.Tk):
def __init__(
self, w, h, columns = 9, rows = 9, scale = 1, line = 1, border = 1):
super().__init__()
self.withdraw()
self.configure(background = light, borderwidth = border)
# pre calculate sizes and set geometry
self.store = dict()
# small change to w, h and wide, high
x, y, w, h = 0, 0, w + line, h + line
wide = w * columns * scale + (line==1)
high = h * rows * scale + (line==1)
self.geometry(f"{wide + border * 2 }x{high + border * 2}")
# minimal Canvas
self.canvas = tk.Canvas(
self, width = wide, height = high, background = back,
borderwidth = 0, highlightthickness = 0, takefocus = 1)
self.canvas.grid(sticky = tk.NSEW)
# draw the board
for row in range(rows):
for column in range(columns):
item = self.canvas.create_rectangle(
0, 0, # removed line, line
w, h, width = line,
fill = back, outline = fore)
self.canvas.move(item, x, y)
self.store[item] = (row, column)
x = x + w
x, y = 0, y + h
# tag all items and scale them
self.canvas.addtag_all("A")
self.canvas.scale("A", 0, 0, scale, scale)
# bind user interaction
self.bind("<Button-1>", self.get_location)
self.after(500, self.complete)
def complete(self):
self.resizable(0, 0)
self.deiconify()
def get_location(self, ev):
# find user selection
item = self.canvas.find_closest(
self.canvas.canvasx(ev.x), self.canvas.canvasy(ev.y))[0]
# flip color for demo
fill = self.canvas.itemcget(item, "fill")
self.canvas.itemconfig(item, fill = [draw, back][fill == draw])
# extract and display info
row, column = self.store[item]
x, y, w, h = self.canvas.coords(item)
print(f"{row}, {column} >> {x}, {y}, {w-x}, {h-y}")
if True:
# the easiest way
app = drawBoard(30, 30)
else:
# Or with lots of control
app = drawBoard(
30, 30, columns = 40, rows = 20, scale = 1, line = 1, border = 1)

tKinter switch color on hotkey click

I have a program that opens a window and a circle at the center that is filled green at the start. I want this circle to change from red to green and green to red each time I press a button (r in this code) however I can only change the color once
from tkinter import *
import keyboard
if __name__ == '__main__':
window = Tk()
window.title("On Record")
window.configure(width=500, height=300)
window.configure(bg='lightgray')
myCanvas = Canvas(window)
myCanvas.pack()
def daire(x, y, r, canvasName, color): # center coordinates, radius
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, fill=color)
# move window center
winWidth = window.winfo_reqwidth()
winwHeight = window.winfo_reqheight()
posRight = int(window.winfo_screenwidth() / 2 - winWidth / 2)
posDown = int(window.winfo_screenheight() / 2 - winwHeight / 2)
window.geometry("+{}+{}".format(posRight, posDown))
color = "green"
def onKeyPress(event):
print("pressed")
counter = 0
counter = counter + 1
print(counter)
if counter % 2 == 0:
color = "green"
else:
color = "red"
daire(190, 135, 120, myCanvas, color)
window.bind("r", onKeyPress)
daire(190, 135, 120, myCanvas, "green")
window.mainloop()
The problem was the event function is reliant on counter being odd or even, but it auto resets it to 0 every time is is run. One solution to this is to place the counter outside of the function and call it as a global.
from tkinter import *
import keyboard
if __name__ == '__main__':
window = Tk()
window.title("On Record")
window.configure(width=500, height=300)
window.configure(bg='lightgray')
myCanvas = Canvas(window)
myCanvas.pack()
def daire(x, y, r, canvasName, color): # center coordinates, radius
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, fill=color)
# move window center
winWidth = window.winfo_reqwidth()
winwHeight = window.winfo_reqheight()
posRight = int(window.winfo_screenwidth() / 2 - winWidth / 2)
posDown = int(window.winfo_screenheight() / 2 - winwHeight / 2)
window.geometry("+{}+{}".format(posRight, posDown))
color = "green"
counter = 0
def onKeyPress(event):
global counter
print("pressed")
counter += 1
print(counter)
if counter % 2 == 0:
color = "green"
else:
color = "red"
daire(190, 135, 120, myCanvas, color)
window.bind("r", onKeyPress)
daire(190, 135, 120, myCanvas, color)
window.mainloop()
Out of interest do you actually need to be able to track how many times r is pressed? Its implied by your script but not explicitly stated. Otherwise theres more streamlined ways of doing this by referring directly to tkinter elements, as is often done with button toggles as displayed in this post.

How to move objects in loops?

I want to move the pipes and the ground of my "Flappy Bird" in a loop but it doesn't work. What do I have to do?
I've tried to move the pipes and the ground with an "if" but it doesn't work.
I expect the pipes and the ground to move in a loop.
def deplacement():
global tuyx,tuyx2,h,H,oisx,oisy,solx,sol2x
x0, y0, x1, y1 = canvas.bbox(image_oiseau)
if y1 < 510:
canvas.move(image_oiseau, 0, DY)
canvas.coords(image_sol,solx,512)
if solx >= -144:
solx=solx-5
else:
solx=144
canvas.coords(image_sol2,sol2x,512)
if sol2x >= -144:
sol2x=sol2x-5
else:
sol2x=432
canvas.coords(image_tuyau_haut,tuyx,h)
canvas.coords(image_tuyau_bas,tuyx,h-241)
h = randint(128,385)
if tuyx>=-28:
tuyx=tuyx-5
else:
tuyx=316
canvas.coords(image_tuyau_haut2,tuyx2,H)
canvas.coords(image_tuyau_bas2,tuyx2,H-241)
H = randint(128,385)
if tuyx2>=-28:
tuyx2=tuyx-5
else:
tuyx2=488
canvas.after(40,deplacement)
You can use the canvas.move method to change the position of a canvas item by dx, dy; With the help of after, this move can be called repeatedly, creating a continuous movement.
Here is an example where the images you did not provide were replaced with a canvas item, but the principle for moving objects on the canvas remains the same:
import random
import tkinter as tk
WIDTH, HEIGHT = 500, 500
def create_pipes():
pipes = []
for x in range(0, WIDTH, 40):
y1 = random.randrange(50, HEIGHT - 50)
y0 = y1 + 50
pipes.append(canvas.create_line(x, 0, x, y1))
pipes.append(canvas.create_line(x, y0, x, HEIGHT))
return pipes
def move_pipes():
for pipe in pipes:
canvas.move(pipe, -2, 0)
x, y0, _, y1 = canvas.coords(pipe)
if x < 0: # reset pipe to the right of the canvas
canvas.coords(pipe, WIDTH+20, y0, WIDTH+20, y1)
root.after(40, move_pipes)
root = tk.Tk()
tk.Button(root, text='start', command=move_pipes).pack()
canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg="cyan")
canvas.pack()
pipes = create_pipes()
root.mainloop()

How to make a circle follow the mouse pointer?

I am trying to make 2D shooter with Python tkinter.
Here are my progress:
from tkinter import *
root = Tk()
c = Canvas(root, height=500, width=500, bg='blue')
c.pack()
circle1x = 250
circle1y = 250
circle2x = 250
circle2y = 250
circle1 = c.create_oval(circle1x, circle1y, circle1x + 10, circle1y + 10, outline='white')
circle2 = c.create_rectangle(circle2x, circle2y,circle2x + 10, circle2y + 10)
pos1 = c.coords(circle1)
pos2 = c.coords(circle2)
c.move(circle1, 250 - pos1[0], 250 - pos1[2])
c.move(circle2, 250 - pos1[0], 250 - pos1[2])
beginWall = c.create_rectangle(0, 200, 500, 210, outline='white')
def move_circle(event):
pass
c.bind('<Motion>', move_circle)
root.mainloop()
But I am trying to make the function called move_circle make circle1 and circle2 follow the mouse pointer . Something like this c.goto(circle1, x, y).
You can do it by modifying the coordinates of the two "circles" in the move_circle() event handler function. A simple calculation is done to make it so the centers of these two objects are positioned at the "tip" of the mouse pointer (see image below).
Note, I also modified your code to more closely follow the PEP 8 - Style Guide for Python Code coding guidelines.
import tkinter as tk
# Constants
CIRCLE1_X = 250
CIRCLE1_Y = 250
CIRCLE2_X = 250
CIRCLE2_Y = 250
SIZE = 10 # Height and width of the two "circle" Canvas objects.
EXTENT = SIZE // 2 # Their height and width as measured from center.
root = tk.Tk()
c = tk.Canvas(root, height=500, width=500, bg='blue')
c.pack()
circle1 = c.create_oval(CIRCLE1_X, CIRCLE1_Y,
CIRCLE1_X + SIZE, CIRCLE1_Y + SIZE,
outline='white')
circle2 = c.create_rectangle(CIRCLE2_X, CIRCLE2_Y,
CIRCLE2_X + SIZE, CIRCLE2_Y + SIZE)
pos1 = c.coords(circle1)
pos2 = c.coords(circle2)
c.move(circle1, 250-pos1[0], 250-pos1[2])
c.move(circle2, 250-pos1[0], 250-pos1[2])
begin_wall = c.create_rectangle(0, 200, 500, 210, outline='white')
def move_circles(event):
# Move two "circle" widgets so they're centered at event.x, event.y.
x0, y0 = event.x - EXTENT, event.y - EXTENT
x1, y1 = event.x + EXTENT, event.y + EXTENT
c.coords(circle1, x0, y0, x1, y1)
c.coords(circle2, x0, y0, x1, y1)
c.bind('<Motion>', move_circles)
root.mainloop()
Here's a screenshot of it running on my Windows computer:

How to use recursion with square?

I have a question how can you recursively draw a rectangle with the use of fractional coordinates? I have been trying to make the squares/rectangles get smaller by 10% each time until it either takes up the entire canvas(meaning it cannot go beyond the width and height of the board or it just get's to 0 meaning it won't draw anything or it's so small you won't see the final square). Or maybe is there another way that I can go about drawing squares in a recursive way that keeps decreasing by a certain percent?
I have this so far but I can't seem to think of who to get it to continue to recurse until it can no longer do it.
import tkinter
class draw_squares:
def __init__(self, squares: [(float, float, float, float)]):
self.squares = squares
self.root_window = tkinter.Tk()
self.canvas = tkinter.Canvas(master = self.root_window,
width = 500, height = 500,
background = 'blue')
self.canvas.grid(row=0, column = 0, padx=0, pady = 0,
sticky = tkinter.E + tkinter.W + tkinter.S + tkinter.N)
self.canvas.bind('<Configure>', self._resized)
self.root_window.rowconfigure(0, weight = 1)
self.root_window.columnconfigure(0, weight = 1)
def start(self):
self.root_window.mainloop()
def _resized(self, event: tkinter.Event):
self.draw_squares_recursively()
def draw_squares_recursively(self):
self.canvas.delete(tkinter.ALL)
for x, y, x2, y2 in self.squares:
if x != 0.5 and y != 0.5 and x2 != 0.5 and y2 != 0.5:
self._draw_square(self._draw_square(x-.1,y-.1,x2+.1,y2+.1))
def _draw_square(self, x: float, y:float, x2:float, y2:float):
width = self.canvas.winfo_width()
height = self.canvas.winfo_height()
self.canvas.create_rectangle(
x * width, y * height,
x2 * width, y2 *height, outline = 'grey')
if __name__ == '__main__':
squares = [(0.9, 0.9, 0.1, 0.1), (0.8,0.8,0.2,0.2), (0.7,0.7,0.3,0.3),
(0.6,0.6,0.4,0.4), (0.5,0.5,0.5,0.5)]
app = draw_squares(squares)
app.start()
Thank you very much in advance for the help!
First, you are using floating point numbers, so (not) equal is not a good idea http://www.lahey.com/float.htm draw_squares_recursively() can call itself until the limit is reached. You can work out the arithmetic to get the sizes and locations you want.
class DrawSquares:
def __init__(self, square):
## self.squares = squares
self.root_window = tkinter.Tk()
self.canvas = tkinter.Canvas(master = self.root_window,
width = 500, height = 500,
background = 'blue')
self.canvas.grid(row=0, column = 0, padx=0, pady = 0,
sticky = "nsew")
## self.canvas.bind('<Configure>', self._resized)
self.root_window.rowconfigure(0, weight = 1)
self.root_window.columnconfigure(0, weight = 1)
self.draw_squares_recursively(square)
self.root_window.mainloop()
def draw_squares_recursively(self, square):
##self.canvas.delete(tkinter.ALL)
for value in square:
if value < 0.05 or value > 100:
return
self.canvas.create_rectangle(square[0], square[1],
square[2], square[3],
outline = 'grey', width=2)
square[2] += 20
square[3] += 20
self.draw_squares_recursively(square)
if __name__ == '__main__':
squares = [0.9, 0.9, 10.1, 10.1]
app = DrawSquares(squares)

Categories