Search code examples
pythonpygamegrid

Is it possible to update a pygame drawing attribute when clicking in a cell of a grid?


I'm approaching PyGame but I stepped on a problem: I am trying to draw a grid made out of single rectangle drawings, I want each one of those rectangle to fill or empty when the left mouse button is released on said rectangle. The grid i represented by a matrix of tuples each one representing each rectangle formatted like this (column, row) to be in line with pygame pixel's coordinates, then I create another matrix of tuples to convert from mouse position tuples (colpixel, rowpixel) to rectangle tuples, I also created a dictionary that pairs rectangle tuples as keys with a value that can be 1 or 0, that value represents whether the rectangle is filled or not, that is the value that I change when the mouse button is released. Here is the code:

import pygame

pygame.init()

SCREEN_WIDTH = 40
SCREEN_HEIGHT = 20

DIM = 20

ROWS = int(SCREEN_HEIGHT / DIM)
COLUMNS = int(SCREEN_WIDTH / DIM)


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

pygame.display.set_caption('Game of Life')


# filling rect_matrix with tuple, each one representing a rectangle of the grid (columns, rows).
# filling rect_map by pairing the tuples from rect_matrix with a value that can be 0 or 1, default 1.
rect_matrix = []
rect_map = {}

for i in range(ROWS):
    temp = []
    for j in range(COLUMNS):
        temp.append(pygame.Rect((j * DIM, i * DIM), (DIM, DIM)))
        rect_map[(j, i)] = 1
    rect_matrix.append(temp)


# filling mouse_to_rectangle, sort of a decoder to convert from groups of tuples
# (representing mouse coordinates in pixels) to a single tuple (representing single rectangles)
mouse_to_rectangle = []

ii = 0
for i in range(SCREEN_WIDTH):
    jj = 0
    temp = []
    for j in range(SCREEN_HEIGHT):
        temp.append((ii, jj))
        if ((j + 1) % DIM == 0):
            jj += 1
    if ((i + 1) % DIM == 0):
        ii += 1
    mouse_to_rectangle.append(temp)


is_running = True

while is_running:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            is_running = False
            pygame.quit()
            exit()
        if event.type == pygame.MOUSEBUTTONUP:

            if rect_map[mouse_to_rectangle[pygame.mouse.get_pos()[0]][pygame.mouse.get_pos()[1]]] == 1:
                rect_map[mouse_to_rectangle[pygame.mouse.get_pos()[0]][pygame.mouse.get_pos()[1]]] = 0
            else:
                rect_map[mouse_to_rectangle[pygame.mouse.get_pos()[0]][pygame.mouse.get_pos()[1]]] = 1



    for i, ii in zip(rect_matrix, range(len(rect_matrix))):
        for j, jj in zip(i, range(len(i))):
            pygame.draw.rect(screen, (100, 100, 100), j, rect_map[(jj, ii)])

    pygame.display.update()

I managed to fill (relative value in rect_map goes to 1) a rectangle upon releasing the left button when pointing it but when I try to click again on a rectangle that is now filled it won't empty. I represented the "filled or not" property with ones or zeroes beacuse the rectangle is filled when the width parameter in pygame.draw.rect is set to 0, if a >0 value is used then the rectangle will have a border as thick as the specified value in pixel.


Solution

  • You missed to clear the display before drawing the grid. Thus all the rectangles which hav been draw stayed there for ever:

    screen.fill((0, 0, 0))
    

    Anyway you've over complicated the algorithm. The position in the grind can be computed by dividing the components of the mouse position by DIM (// (floor division) operator):

    mousepos = pygame.mouse.get_pos()
    gridpos = mousepos[0] // DIM, mousepos[1] // DIM
    

    Finally it the grid state can be changed with ease and mouse_to_rectangle is not need at all:

    if rect_map[gridpos] == 1:
        rect_map[gridpos] = 0
    else:
        rect_map[gridpos] = 1
    

    See the example:

    import pygame
    
    pygame.init()
    
    SCREEN_WIDTH = 200
    SCREEN_HEIGHT = 200
    
    DIM = 20
    
    ROWS = int(SCREEN_HEIGHT / DIM)
    COLUMNS = int(SCREEN_WIDTH / DIM)
    
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption('Game of Life')
    
    # filling rect_matrix with tuple, each one representing a rectangle of the grid (columns, rows).
    # filling rect_map by pairing the tuples from rect_matrix with a value that can be 0 or 1, default 1.
    rect_matrix = []
    rect_map = {}
    
    for i in range(ROWS):
        temp = []
        for j in range(COLUMNS):
            temp.append(pygame.Rect((j * DIM, i * DIM), (DIM, DIM)))
            rect_map[(j, i)] = 1
        rect_matrix.append(temp)
    
    is_running = True
    
    while is_running:
    
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                is_running = False
                pygame.quit()
                exit()
            if event.type == pygame.MOUSEBUTTONUP:
    
                mousepos = pygame.mouse.get_pos()
                gridpos = mousepos[0] // DIM, mousepos[1] // DIM
                if rect_map[gridpos] == 1:
                    rect_map[gridpos] = 0
                else:
                    rect_map[gridpos] = 1
    
        screen.fill((0, 0, 0))
    
        for i, ii in zip(rect_matrix, range(len(rect_matrix))):
            for j, jj in zip(i, range(len(i))):
                pygame.draw.rect(screen, (100, 100, 100), j, rect_map[(jj, ii)])
    
        pygame.display.update()