Search code examples
pythonmatrixpygametic-tac-toe

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


The draw Board functions has an if statement to see which colour circle it should draw. But when the mouse button is clicked it draws the circle in the right x coordinate but in the opposite y coordinate. Also, the end is a little bit weird I want to display the winner and then wait using time.wait but it waits and then prints the statement for a second. If you could run it, you would understand. I am also looking for general improvements that I could make.

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), HEIGHT - 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), HEIGHT - 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 - 1):
            for r in range(ROW_COUNT - 1):
                if board[r - 1][c] == piece and board[r][c] == piece and board[r + 1][c] == piece:
                    return True

        # Vertical
        for c in range(0, COLUMN_COUNT - 1):
            for r in range(0, ROW_COUNT - 1):
                if board[r][c - 1] == piece and board[r][c] == piece and board[r][c + 1] == piece:
                    return True
        # Positively sloped diagonals
        for c in range(0, COLUMN_COUNT - 1):
            for r in range(0, ROW_COUNT - 1):
                if board[r][c] == piece and board[r + 1][c + 1] == piece and board[r + 2][c + 2] == piece:
                    return True

        # Negatively sloped diagonals
        for c in range(COLUMN_COUNT - 1):
            for r in range(ROW_COUNT - 1):
                if board[r + 1][c + 1] == piece and board[r][c] == piece and board[r - 1][c - 1] == 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.time.wait(3000)
                run = False

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

Solution

  • 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)