Pygame Tic Tak Toe Logic? How Would I Do It

Pygame Tic Tak Toe Logic? How Would I Do It

Class names should normally use the CapWords convention. The name of the class should be Button rather than button. Instance Variables should be lowercase. Hence the name of an object of the typ Button should be button.

Furthermore use a pygame.Rect object and collidepoint. See How do I detect if the mouse is hovering over a button? PyGame button class is not displaying the text or changing colour on hover.

For Instance:

class Button():
def __init__(self, color, x, y, width, height, text=''):
self.color = color
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.over = False

def draw(self, window, outline=None):
pygame.draw.rect(window, self.color, self.rect)
if outline:
pygame.draw.rect(window, outline, self.rect.inflate(-4, -4), 3)

def isOver(self, pos):
return self.rect.collidepoint(pos)

Create a 3x3 grid of Buttons and an 3x3 grid for the Particle objects. The initial value of the particle grid is None:

button_grid = [[Button((0, 128, 0), 70+i*120, 90+j*120, 100, 100, '') for i in range(3)] for j in range(3)]
particle_grid = [[None for i in range(3)] for j in range(3)]

Draw the grids in redraw

def redraw():
window.fill((32, 32, 32))

for i in range(len(button_grid)):
for j in range(len(button_grid[0])):
b = button_grid[i][j]
p = particle_grid[i][j]

is_over = p == None and b.isOver(pygame.mouse.get_pos())
b.draw(window, (255, 255, 255) if is_over else None)
if p:
p.draw(window)

The particle image ahs to be an argument of the constructor of the class Particle. Add a method to the class to set the position:

class Particle:
def __init__(self, image):
self.pos = (0, 0)
self.image = image
def set_pos(self, pos):
self.pos = pos
def draw(self, window):
window.blit(self.image, self.image.get_rect(center = self.pos))

Create a method that creates a new particle dependent on the current turn:

def new_particle(turn):
image = MANUAL_CURSOR if turn % 2 == 0 else MANUAL_CURSOR2
return Particle(image)

Add a variable for the current turn and create an initial Particle object:

turn = 0
particle = new_particle(turn)

Set the position of the Particle object using the current mouse position and draw the object in the application loop:

runninggame = True
while runninggame:
# [...]

particle.set_pos(pygame.mouse.get_pos())

redraw()
particle.draw(window)
pygame.display.update()

When you click a field, check that the corresponding field in the particle grid is empty. Copy the center of the field to the position of the particle. Assign the particle to the grid. Increment the turn and create a new particle:

while runninggame:
clock.tick(fps)
for event in pygame.event.get():
if event.type == pygame.QUIT:
runninggame = False

if event.type == pygame.MOUSEBUTTONDOWN:
for i in range(len(button_grid)):
for j in range(len(button_grid[0])):
b = button_grid[i][j]
if b.isOver(event.pos) and particle_grid[i][j] == None:
particle.set_pos(b.rect.center)
particle_grid[i][j] = particle
turn += 1
particle = new_particle(turn)

Add a score for both players:

score1 = 0
score2 = 0

Add a function that evaluates whether there are 3 identical images in a row:

def has_won(pg):
pg = particle_grid
for i in range(3):
if pg[i][0] and pg[i][1] and pg[i][2]:
if pg[i][0].image == pg[i][1].image == pg[i][2].image:
return pg[i][0].image
for j in range(3):
if pg[0][j] and pg[1][j] and pg[2][j]:
if pg[0][j].image == pg[1][j].image == pg[2][j].image:
return pg[0][j].image
if pg[0][0] and pg[1][1] and pg[2][2]:
if pg[0][0].image == pg[1][1].image == pg[2][2].image:
return pg[0][0].image
if pg[0][2] and pg[1][1] and pg[2][0]:
if pg[0][2].image == pg[1][1].image == pg[2][0].image:
return pg[0][2].image
return None

Test to see if a player has won after clicking a button. When a player wins, increase the appropriate score and reset the particle grid:

while runninggame:
clock.tick(fps)
for event in pygame.event.get():
# [...]

if event.type == pygame.MOUSEBUTTONDOWN:
[...]

won = has_won(particle_grid)
if won:
if won == MANUAL_CURSOR:
score1 += 1
else:
score2 += 1
print(score1, score2)
particle_grid = [[None for i in range(3)] for j in range(3)]

Add a function that evaluates whether the grid is full:

def grid_is_full(pg):
return all(cell for row in pg for cell in row)

Clear the grid when it's full:

while runninggame:
clock.tick(fps)
for event in pygame.event.get():
# [...]

if event.type == pygame.MOUSEBUTTONDOWN:
[...]

if grid_is_full(particle_grid):
particle_grid = [[None for i in range(3)] for j in range(3)]

Minimal example:

Sample Image

import pygame,random
pygame.init()

class Button():
def __init__(self, color, x, y, width, height, text=''):
self.color = color
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.over = False

def draw(self, window, outline=None):
pygame.draw.rect(window, self.color, self.rect)
if outline:
pygame.draw.rect(window, outline, self.rect.inflate(-4, -4), 3)

def isOver(self, pos):
return self.rect.collidepoint(pos)

class Particle:
def __init__(self, image):
self.pos = (0, 0)
self.image = image
def set_pos(self, pos):
self.pos = pos
def draw(self, window):
window.blit(self.image, self.image.get_rect(center = self.pos))

def new_particle(turn):
image = MANUAL_CURSOR if turn % 2 == 0 else MANUAL_CURSOR2
return Particle(image)

def has_won(pg):
pg = particle_grid
for i in range(3):
if pg[i][0] and pg[i][1] and pg[i][2]:
if pg[i][0].image == pg[i][1].image == pg[i][2].image:
return pg[i][0].image
for j in range(3):
if pg[0][j] and pg[1][j] and pg[2][j]:
if pg[0][j].image == pg[1][j].image == pg[2][j].image:
return pg[0][j].image
if pg[0][0] and pg[1][1] and pg[2][2]:
if pg[0][0].image == pg[1][1].image == pg[2][2].image:
return pg[0][0].image
if pg[0][2] and pg[1][1] and pg[2][0]:
if pg[0][2].image == pg[1][1].image == pg[2][0].image:
return pg[0][2].image
return None

def grid_is_full(pg):
return all(cell for row in pg for cell in row)

def redraw():
window.fill((32, 32, 32))

for i in range(len(button_grid)):
for j in range(len(button_grid[0])):
b = button_grid[i][j]
p = particle_grid[i][j]

is_over = p == None and b.isOver(pygame.mouse.get_pos())
b.draw(window, (255, 255, 255) if is_over else None)
if p:
p.draw(window)

window = pygame.display.set_mode((500,540))
pygame.display.set_caption("Tic Tac TOE")
fps = 40
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 50)

#MANUAL_CURSOR = pygame.image.load('nw.png').convert_alpha()
#MANUAL_CURSOR2 = pygame.image.load('nOW.png').convert_alpha()
size = (60, 60)
MANUAL_CURSOR = pygame.Surface(size)
MANUAL_CURSOR.fill((127, 127, 127))
pygame.draw.line(MANUAL_CURSOR, (0, 127, 255), (5, 5), (size[0]-5, size[1]-5), 3)
pygame.draw.line(MANUAL_CURSOR, (0, 127, 255), (size[0]-5, 5), (5, size[1]-5), 3)
MANUAL_CURSOR2 = pygame.Surface(size)
MANUAL_CURSOR2.fill((127, 127, 127))
pygame.draw.circle(MANUAL_CURSOR2, (127, 0, 255), (size[0]//2, size[1]//2), size[0]//2-5, 3)

score1 = 0
score2 = 0

button_grid = [[Button((0, 128, 0), 70+i*120, 90+j*120, 100, 100, '') for i in range(3)] for j in range(3)]
particle_grid = [[None for i in range(3)] for j in range(3)]
turn = 0
particle = new_particle(turn)

runninggame = True
while runninggame:
clock.tick(fps)
for event in pygame.event.get():
if event.type == pygame.QUIT:
runninggame = False

if event.type == pygame.MOUSEBUTTONDOWN:
for i in range(len(button_grid)):
for j in range(len(button_grid[0])):
b = button_grid[i][j]
if b.isOver(event.pos) and particle_grid[i][j] == None:
particle.set_pos(b.rect.center)
particle_grid[i][j] = particle
turn += 1
particle = new_particle(turn)

won = has_won(particle_grid)
if won:
if won == MANUAL_CURSOR:
score1 += 1
else:
score2 += 1
print(score1, score2)
particle_grid = [[None for i in range(3)] for j in range(3)]

if grid_is_full(particle_grid):
particle_grid = [[None for i in range(3)] for j in range(3)]

particle.set_pos(pygame.mouse.get_pos())
scoreText1 = font.render("Player 1 Score: " + str(score1), True, (128, 0, 0))
scoreText2 = font.render("Player 2 Score: " + str(score2), True, (128, 0, 0))

redraw()
window.blit(scoreText1, (70, 30))
window.blit(scoreText2, (70, 460))
particle.draw(window)
pygame.display.update()

pygame.quit()
exit()

How to determine tic tac toe winner using this code?

Your code lacks an essential part: a representation of the game in memory. Currently your code has no knowledge of the moves that have already been played, and it cannot verify whether a user attempts to play in a box that already has a symbol from a previous move.

I will not adjust your existing code, but instead show you how you should start:

  • Create a class that focuses only on maintaining the game's state: it should provide the functions to play a move, to verify that a move is valid, to check whether the game has ended, whether it is a draw or has a winner, ... None of this needs to know about the visualisation of the game, but it will be a class that you can use whether you have a graphical user interface for playing the game, or just a terminal input/output, or a keyboard driven interface, ...etc, ...etc.

  • An instance of that class should have attributes that remember what has been played and what the current game looks like. But this should be as simple as possible. For instance, that representation could be a standard list of length 9, where each element of that list indicates with a number or a string what the state is of that cell in the game. "X" could be for when the first player has played there, "O" for when the second player has played there, and then anything else to indicate the cell is still free. The latter could be a space, or -- why not -- the numerical identifier of that cell (0, 1, 2, ... 8).

  • The class could also provide a very, very basic string representation, so at least you can debug with print statements.

  • The next step is to create the flow of a game -- the driver code: it will have the loop where in each iteration a move is played, and an inner loop where you keep getting the user's next move until that input is valid. The outer loop should exit when the game is over. This code should be simple and rely on the class for any game-logic.

So here is an implementation of that class and a very simple version of the driver code, which uses simple input() and print() calls for the user interface. It will be your job to replace that input/output with the graphical user interface you are using (where input is coming partly from the mouse, and output consists of drawing lines).

class TicTacToe:
def __init__(self):
self.board = list("012345678")
self.moves = []

def __repr__(self):
s = " ".join(self.board)
return "\n".join([s[:5], s[6:11], s[12:]])

def is_free(self, spot):
return 0 <= spot < 9 and self.board[spot] not in "XO"

def turn(self):
return "XO"[len(self.moves) % 2]

def move(self, spot):
if not self.is_free(spot):
raise ValueError("This is not a valid move")
symbol = self.turn()
self.board[spot] = symbol
self.moves.append(symbol)

def winner(self):
# Check each line of 3 for a win
for a, b, c in ((0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)):
if self.board[a] == self.board[b] == self.board[c] and self.board[a] in "XO":
return self.board[a]
return ""

def is_draw(self):
return not self.winner() and len(self.moves) == 9

def over(self):
return self.winner() or self.is_draw()

# Play one game
game = TicTacToe()
while not game.over():
print(game) # Here you would draw the game
while True: # Repeat until valid move is entered
# Here you would get the mouse click, ...etc
spot = int(input(f"Choose the spot for {game.turn()}: "))
if game.is_free(spot):
break
print("That is not a valid spot...")
game.move(spot)

print(game)
if game.is_draw():
print("It's a draw!")
else:
print(f"{game.winner()} won the game!")

Im making a simple TicTacToe in pyjama but I am running into some errors

In the PyGame coordinate system the top left is (0, 0). You don't have to flip the y-axis or subtract the y-coordinate from the height. Remove the term HEIGHT - in the method draw_board of the class Board:

class Board:
# [...]

def draw_board(self):
# [...]

for c in range(COLUMN_COUNT):
for r in range(ROW_COUNT):
if self.board_body[r][c] == 1:
pygame.draw.circle(screen, RED, (int(c * SQUARE_SIZE + SQUARE_SIZE / 2), int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
elif self.board_body[r][c] == 2:
pygame.draw.circle(screen, GREEN, (int(c * SQUARE_SIZE + SQUARE_SIZE / 2), int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)

# [...]

If you just wait for some time, you can use pygame.time.wait or pygame.time.delay. However, if you want to display a message and then wait some time, you need to update the display beforehand. The display is updated only if either pygame.display.update() or pygame.display.flip()
is called. Further you've to handles the events by pygame.event.pump(), before the update of the display becomes visible in the window:

(See How to wait some time in pygame?)

screen.fill(BLACK)
screen.blit(label, (20, 50))
pygame.display.flip()
pygame.event.pump()
pygame.time.wait(3000)

Also, there are some mistakes when looking for a winner. Yo don't need nestes loops at all. There are just 2 diagonals. So you don't need any loops for the diagonals:

class Board:
# [...]

def check_winning_move(self, board, piece):
# Horizontal
for c in range(COLUMN_COUNT):
if board[0][c] == piece and board[1][c] == piece and board[2][c] == piece:
return True

# Vertical
for r in range(ROW_COUNT):
if board[r][0] == piece and board[r][1] == piece and board[r][2] == piece:
return True

# Positively sloped diagonals
if board[0][0] == piece and board[1][1] == piece and board[2][2] == piece:
return True

# Negatively sloped diagonals
if board[0][2] == piece and board[1][1] == piece and board[2][0] == piece:
return True

See also Pygame Tic Tak Toe Logic? How Would I Do It.


Complete code:

import pygame, sys
import numpy as np
import math

pygame.init()
clock = pygame.time.Clock()

# Colours
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

ROW_COUNT = 3
COLUMN_COUNT = 3
SQUARE_SIZE = 100
RADIUS = int(SQUARE_SIZE/2 - 5)

WIDTH = COLUMN_COUNT * SQUARE_SIZE
HEIGHT = ROW_COUNT * SQUARE_SIZE

myfont = pygame.font.SysFont("monospace", 20)

PLAYER_COUNT = 2
screen = pygame.display.set_mode((WIDTH, HEIGHT))
screen.fill(WHITE)

class Player:
def __init__(self, number):
self.number = number

players = [Player(i) for i in range(1, PLAYER_COUNT + 1)]

class Board:
def __init__(self):
self.board_body = np.zeros((ROW_COUNT, COLUMN_COUNT), dtype=np.uint8)
self.remaining_spaces = 9
self.print_board()
self.draw_board()

def print_board(self):
print(np.flip(self.board_body, 0))

def draw_board(self):
one_x, one_y, one_end_x, one_end_y, two_x, two_y, two_end_x, two_end_y = 100, 0, 100, 300, 0, 100, 300, 100
for r in range(ROW_COUNT):
for c in range(COLUMN_COUNT):
pygame.draw.line(screen, BLACK, (one_x, one_y), (one_end_x, one_end_y), 3)
one_x += 100
one_end_x += 100
pygame.draw.line(screen, BLACK, (two_x, two_y), (two_end_x, two_end_y), 3)
two_y += 100
two_end_y += 100

for c in range(COLUMN_COUNT):
for r in range(ROW_COUNT):
if self.board_body[r][c] == 1:
pygame.draw.circle(screen, RED, (int(c * SQUARE_SIZE + SQUARE_SIZE / 2), int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
elif self.board_body[r][c] == 2:
pygame.draw.circle(screen, GREEN, (int(c * SQUARE_SIZE + SQUARE_SIZE / 2), int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
pygame.display.update()

def check_empty_space_and_place(self, row, column, number):
if self.board_body[row][column] == 0:
self.board_body[row][column] = number
self.remaining_spaces -= 1

def check_winning_move(self, board, piece):
# Horizontal
for c in range(COLUMN_COUNT):
if board[0][c] == piece and board[1][c] == piece and board[2][c] == piece:
return True

# Vertical
for r in range(ROW_COUNT):
if board[r][0] == piece and board[r][1] == piece and board[r][2] == piece:
return True

# Positively sloped diagonals
if board[0][0] == piece and board[1][1] == piece and board[2][2] == piece:
return True

# Negatively sloped diagonals
if board[0][2] == piece and board[1][1] == piece and board[2][0] == piece:
return True

def next_player():
while True:
for player in players:
yield player

player_generator = next_player()

board = Board()

run = True

while run:
if board.remaining_spaces == 0:
run = False
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

if event.type == pygame.MOUSEBUTTONDOWN:
pos_of_mouse = pygame.mouse.get_pos()
posx = pos_of_mouse[0]
posy = pos_of_mouse[1]
column = int(posx // SQUARE_SIZE)
row = int(posy // SQUARE_SIZE)
p = player_generator.__next__()
board.check_empty_space_and_place(row, column, p.number)
board.draw_board()
if board.check_winning_move(board.board_body, p.number):
label = myfont.render("Player {} wins!!!!".format(p.number), False, WHITE)
screen.fill(BLACK)
screen.blit(label, (20, 50))
pygame.display.flip()
pygame.event.pump()
pygame.time.wait(3000)

run = False

board.print_board()
pygame.display.update()
clock.tick(60)

Game AI works powerfully on one side and becomes dumb on the other in Tic-Tac-Toe

best_score = -float('inf')  # Least possible score

you need to vary this according to the player for which you calculate the move. I think because of this the negative player is choosing random/first plausible move.

I have implemented minimax and related heuristics like 2 times, and always found that using the "negamax" approach worked best, since you don't need to worry about when to apply max and when min based on the player.



Related Topics



Leave a reply



Submit