обновление холста замедляется (возможно, нужно освободить некоторые объекты) - PullRequest
0 голосов
/ 12 ноября 2018

Эта игра замедляется примерно после 10 итераций до сканирования. Я почти уверен, что это связано с мусорными объектами (вероятно, прямоугольниками, которые я создаю на каждой итерации), которые лежат там и занимают пространство, даже если они не нужны. Как я могу определить нежелательные объекты и удалить их?

# Game of life
from random import randint
import numpy as np
from copy import deepcopy
from enum import Enum
import tkinter as tk


class State(Enum):
    Dead    = 0
    Alive   = 1

    def __str__(self):
        return str(self.value)


class Cell:    
    def __init__(self, m, n, state):
        self.m = np.uint(m)
        self.n = np.uint(n)
        self.state = state

    def kill(self):
        self.state = State.Dead

    def birth(self):
        self.state = State.Alive

    def __str__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)

    def __repr__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)


class Game:
    def __init__(self, m, n, alive_cells = None):
        self.m = m
        self.n = n
        self.grid = np.ndarray((m,n), dtype = np.uint8)

        if alive_cells:
            self.cells = [Cell(i // n,i % n, State.Alive if (i // n,i % n) in alive_cells else State.Dead) for i in range(m*n)]
        else:
            self.cells = [Cell(i / n,i % n,randint(0,1)) for i in range(m*n)]

        # GUI #
        self.top = tk.Tk()
        self.cell_size = 10000 // 400 #(self.m * self.n)
        self.canvas = tk.Canvas(self.top, bg="gray", height=self.m *self. cell_size, width=self.n * self.cell_size)  

    def populate_grid(self):
        for cell in self.cells:
            self.grid[cell.m,cell.n] = cell.state.value

    def show(self, show_GUI = True, print_2_console = False):
        self.populate_grid()

        if print_2_console:
            print('#'*self.m*3)
            print(self.grid)
        if show_GUI:
            self.draw_canvas()

    def iterate(self):
        '''
        Rules:
        (1) If cell has less than 2 neighbours, it dies
        (2) If cell has more than 3 neighbours, it dies
        (3) If cell has 2-3 neighbours, it survives
        (4) If cell has 3 neighbours, it rebirths
        '''
        new_cells = []
        for cell in self.cells:
            alive_neighbours = 0
            for i in range(cell.m - 1, cell.m + 2):
                for j in range(cell.n - 1, cell.n + 2):
                    if i == cell.m and j == cell.n:
                        continue
                    else:
                        try:
                            alive_neighbours += self.grid[i,j]
                        except IndexError:
                            pass


            tmp = deepcopy(cell)

            if alive_neighbours < 2 or alive_neighbours > 3:
                tmp.kill()
            elif alive_neighbours == 3:
                tmp.birth()
            else: # == 2
                pass 

            new_cells.append(tmp)

        self.cells = new_cells
        self.show()

    def draw_canvas(self):    
        for cell in self.cells:
            if cell.state == State.Alive:
                color = 'blue'
            else:
                color = 'red'

            self.canvas.create_rectangle(cell.n*self.cell_size, cell.m*self.cell_size, (1+cell.n)*self.cell_size, (1+cell.m)*self.cell_size, fill=color)

        self.canvas.pack()
        self.update_canvas()
        self.top.mainloop()

    def update_canvas(self): 
        for cell in self.cells:
            if cell.state == State.Alive:
                color = 'blue'
            else:
                color = 'red'

            self.canvas.create_rectangle(cell.n*self.cell_size, cell.m*self.cell_size, (1+cell.n)*self.cell_size, (1+cell.m)*self.cell_size, fill=color)

        # call again after 100 ms
        self.top.after(100, self.iterate)



if __name__ == "__main__":
    glider = (20, 20, ((1,3), (2,3), (2,1), (3,2), (3,3)))
    small_exploder = (30, 30, ((10,10), (11,9), (11,10), (11,11), (12,9), (12,11), (13,10)))


    M, N, STARTING_LIVE_CELLS, ITERATIONS = *small_exploder, 0

    g = Game(M, N, STARTING_LIVE_CELLS)
    g.show()

Ответы [ 3 ]

0 голосов
/ 13 ноября 2018

Я наконец-то понял все, и ваш скрипт заработал без сбоев. Проблема с рекурсией, с которой я столкнулся после исправления проблемы с мусором, была вызвана тем, что mainloop() вызывался снова и снова. Он должен вызываться только один раз в сценарии tkinter.

Эта версия позволяет избежать постоянного создания все большего числа прямоугольников tkinter, «помечая» их все одним и тем же значением тега , что позволяет удалить их все как группу одним вызовом метода Canvas.delete(). .

Было бы эффективнее просто изменять существующие прямоугольники на каждой итерации, но эта версия просто удаляет и воссоздает их все каждую итерацию. Для их обновления потребуется какой-то способ ссылаться на них по отдельности после их создания. Если я чувствую мотивацию и вам интересно, я могу позже обновить этот ответ на этот вопрос.

Я сделал множество изменений, но ваш основной код все еще там.

# Game of life
from random import randint
import numpy as np
from copy import deepcopy
from enum import Enum
import tkinter as tk


class State(Enum):
    Dead = 0
    Alive = 1

    def __str__(self):
        return str(self.value)

    @property
    def color(self):
        return 'blue' if self.value else 'red'


class Cell:
    def __init__(self, m, n, state):
        self.m = np.uint(m)
        self.n = np.uint(n)
        self.state = state

    def kill(self):
        self.state = State.Dead

    def birth(self):
        self.state = State.Alive

    def __str__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)

    def __repr__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)


class Game:
    CELL_TAG = 'cells'

    def __init__(self, m, n, alive_cells = None):
        self.m = m
        self.n = n
        self.grid = np.ndarray((m,n), dtype = np.uint8)

        if alive_cells:
            self.cells = [Cell(i // n, i % n,
                State.Alive if (i // n, i % n) in alive_cells else State.Dead)
                    for i in range(m*n)]
        else:
            self.cells = [Cell(i / n, i % n, randint(0,1)) for i in range(m*n)]

        # GUI #
        self.top = tk.Tk()
        self.cell_size = 10000 // 400  # (self.m * self.n)
        self.canvas = tk.Canvas(self.top, bg="gray", height=self.m * self.cell_size,
                                                      width=self.n * self.cell_size)
        self.canvas.pack()

    def populate_grid(self):
        for cell in self.cells:
            self.grid[cell.m, cell.n] = cell.state.value

    def show(self, show_GUI = True, print_2_console = False):
        self.populate_grid()

        if print_2_console:
            print('#'*self.m*3)
            print(self.grid)
        if show_GUI:
            self.draw_canvas()
            self.top.mainloop()

    def iterate(self):
        '''
        Rules:
        (1) If cell has less than 2 neighbours, it dies
        (2) If cell has more than 3 neighbours, it dies
        (3) If cell has 2-3 neighbours, it survives
        (4) If cell has 3 neighbours, it rebirths
        '''
        new_cells = []
        for cell in self.cells:
            alive_neighbours = 0
            for i in range(cell.m - 1, cell.m + 2):
                for j in range(cell.n - 1, cell.n + 2):
                    if i == cell.m and j == cell.n:
                        continue
                    else:
                        try:
                            alive_neighbours += self.grid[i,j]
                        except IndexError:
                            pass

            tmp = deepcopy(cell)

            if alive_neighbours < 2 or alive_neighbours > 3:
                tmp.kill()
            elif alive_neighbours == 3:
                tmp.birth()
            else: # == 2
                pass

            new_cells.append(tmp)

        self.cells = new_cells
        self.populate_grid()
        self.draw_canvas()

    def draw_canvas(self):
        self.canvas.delete(self.CELL_TAG)  # Gets rid of any existing cell rects.

        for cell in self.cells:
            self.canvas.create_rectangle(
                cell.n*self.cell_size, cell.m*self.cell_size,
                (1+cell.n)*self.cell_size, (1+cell.m)*self.cell_size,
                fill=cell.state.color, tag=self.CELL_TAG)

        self.top.after(100, self.iterate)


if __name__ == "__main__":
    glider = (20, 20, ((1,3), (2,3), (2,1), (3,2), (3,3)))
    small_exploder = (30, 30, ((10,10), (11,9), (11,10), (11,11), (12,9), (12,11), (13,10)))

    M, N, STARTING_LIVE_CELLS, ITERATIONS = *small_exploder, 0

    g = Game(M, N, STARTING_LIVE_CELLS)
    g.show()
0 голосов
/ 17 ноября 2018

Несмотря на то, что вы не проявили особого интереса к просмотру этой версии, я все равно опубликую ее, потому что думаю, что она может быть интересна другим, если не вам, - и потому что я положил немало время игры с , работающей над этим, чтобы улучшить и оптимизировать его.

Он отслеживает прямоугольники Canvas, созданные путем добавления атрибута id к экземплярам класса Cell, которые сохраняют идентификатор объекта Canvas, возвращенный из метода create_rectangle(), вместо отдельного canvas_rect_list. Это позволяет избежать повторного создания их при каждой итерации.

Кроме того, метод iterate() больше не создает deepcopy с каждого из существующих Cell в процессе создания их нового списка. Вместо этого он просто обновляет существующие экземпляры. Это работает потому, что реализованы правила, и они ссылаются только на текущие значения в отдельном массиве grid, который на данный момент еще не обновлен.

Говоря об этом, я также сделал использование библиотеки numpy необязательным и использовал словарь на месте. Все строки, затронутые этим изменением, начинаются с #xnp. Это делает его работоспособным без установки каких-либо дополнительных модулей, но код для использования numpy все еще там, если вы действительно хотите использовать его по какой-то причине. (Лично я не думаю, что есть много преимуществ в его использовании).

Результатом всего этого является то, что работает очень быстро, и код теперь очень модульный. Я экспериментально уменьшил задержку между итерациями до 16 мсек, и она действительно летела ...

# Conway's Game of life
from random import randint
from enum import IntEnum
#xnp  import numpy as np
import tkinter as tk

DELAY = 100  # Msecs between iterations.

class State(IntEnum): Dead = False; Alive = True

class Cell:
    __slots__ = "m", "n", "state", "id"

    def __init__(self, m, n, state):
#xnp     self.m, self.n, self.state = np.uint(m), np.uint(n), state
        self.m, self.n, self.state = m, n, state
        self.id = None

    def get_posn(self):
        return self.m, self.n  # (Must) return a tuple.

    def kill(self):
        self.state = State.Dead

    def birth(self):
        self.state = State.Alive

    def __str__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)

    def __repr__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)


class CanvasFrame(tk.Frame):
    COLORMAP = {State.Dead: 'red', State.Alive: 'blue'}

    def __init__(self, parent, m, n, cell_size, cells):
        tk.Frame.__init__(self, parent)

        self.canvas = tk.Canvas(self, bg="gray", height=m*cell_size, width=n*cell_size)
        self.canvas.pack()

        COLORMAP = self.COLORMAP
        create_rectangle = self.canvas.create_rectangle
        for cell in cells:
            cell.id = create_rectangle(cell.n * cell_size, cell.m * cell_size,
                                       (cell.n+1) * cell_size, (cell.m+1) * cell_size,
                                       fill=COLORMAP[cell.state])

    def update_canvas(self, cells):
        COLORMAP = self.COLORMAP
        itemconfig = self.canvas.itemconfig
        for cell in cells:
            itemconfig(cell.id, fill=COLORMAP[cell.state])


class Game:

    def __init__(self, m, n, seed=None):
        self.m, self.n = m, n
        self.cell_size = 10000 // 400  # (self.m * self.n)
#xnp     self.grid = np.ndarray((m, n), dtype=np.uint8)
        self.grid = {}  # Use a dictionary instead of ndarray.

        states = tuple(s for s in State)  # Local var for faster access.
        statefunc = ((lambda q, r: states[(q, r) in seed]) if seed else
                     (lambda q, r: states[randint(0, 1)]))
        self.cells = [Cell(q, r, statefunc(q, r))
                        for q, r in (divmod(i, n) for i in range(m*n))]
        self.populate_grid()

    def populate_grid(self):
#xnp     for cell in self.cells:
#xnp         self.grid[cell.m, cell.n] = cell.state.value
        self.grid = {cell.get_posn(): cell.state.value for cell in self.cells}

    def show(self):
        self.root = tk.Tk()
        self.container = tk.Frame(self.root)
        self.container.pack(side="top", fill="both", expand=True)
        self.container.grid_rowconfigure(0, weight=1)
        self.container.grid_columnconfigure(0, weight=1)

        self.canvasframe = CanvasFrame(self.container, self.m, self.n, self.cell_size, self.cells)
        self.canvasframe.grid(row=0, column=0, sticky="nsew")  # Each in same location.

        self.container.after(500, self.tick)
        self.root.mainloop()

    def tick(self):
        """ Transition to next generation. """
        self.iterate()  # Update cells and grid.
        self.canvasframe.update_canvas(self.cells)
        self.container.after(DELAY, self.tick)

    def iterate(self):
        """
        Rules:
        (1) If cell has less than 2 neighbours, it dies
        (2) If cell has more than 3 neighbours, it dies
        (3) If cell has 2-3 neighbours, it survives
        (4) If cell has 3 neighbours, it rebirths
        """
        deltas = ((-1, -1), (-1, 0), (-1, 1),
                  ( 0, -1),          ( 0, 1),
                  ( 1, -1), ( 1, 0), ( 1, 1))

        for cell in self.cells:
            alive_neighbours = 0
            m, n = cell.m, cell.n
            for i, j in ((m+dm, n+dn) for dm, dn in deltas):
                try:
                    alive_neighbours += self.grid[i, j]
                # Exception depends on whether np is being used or not.
                except (KeyError, IndexError):
                    pass

            if alive_neighbours < 2 or alive_neighbours > 3:  # Death?
                cell.state = State.Dead
            elif alive_neighbours == 3:  # Birth?
                cell.state = State.Alive

        self.populate_grid()  # Update.


if __name__ == "__main__":
    glider = (20, 20, ((1,3), (2,3), (2,1), (3,2), (3,3)))
    small_exploder = (30, 30, ((10,10), (11,9), (11,10), (11,11), (12,9), (12,11), (13,10)))
    random_pattern = (30, 30, None)
# @Mike - SMT version that runs longer.
    small_exploder = (32, 30, ((12,10), (11,9), (11,10), (11,11), (12,9), (12,11), (14,10)))

#    g = Game(*small_exploder)
    g = Game(*random_pattern)
    g.show()

Вот оно работает со случайной начальной точки:

Screenshot of it running

0 голосов
/ 12 ноября 2018

Я изменил ваш код, чтобы использовать список для хранения каждой ячейки, а затем обновил цвет. Это можно сделать с помощью списка атрибутов класса и отслеживая индекс каждой ячейки в self.cells.

Похоже, это устраняет проблемы с замедлением. При этом ваши обновления остановятся на 17 итерациях с моей стороны. Похоже, ваша игра в правила жизни заставляет ее остановиться на 17-м раунде, и ни одна из ячеек, соответствующих правилам, может измениться в этот момент. Код продолжает цикл только без изменений, внесенных после 17 раунда.

Я добавил несколько комментариев, в которые были внесены все основные изменения в ваш код:

# Game of life
from random import randint
import numpy as np
from copy import deepcopy
from enum import Enum
import tkinter as tk


class State(Enum):
    Dead    = 0
    Alive   = 1

    def __str__(self):
        return str(self.value)


class Cell:    
    def __init__(self, m, n, state):
        self.m = np.uint(m)
        self.n = np.uint(n)
        self.state = state

    def kill(self):
        self.state = State.Dead

    def birth(self):
        self.state = State.Alive

    def __str__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)

    def __repr__(self):
        return '({},{}) {}'.format(self.m, self.n, self.state)

# made the class inherit from Tk to make it easier to manage.
class Game(tk.Tk):
    def __init__(self, m, n, alive_cells=None):
        super().__init__()
        self.m = m
        self.n = n
        self.grid = np.ndarray((m, n), dtype = np.uint8)
        self.first = False # This variable is used to check if we need to draw or update rectangles.
        self.canvas_rect_list = [] # This list is used to track the list.
        if alive_cells:
            self.cells = [Cell(i // n,i % n, State.Alive if (i // n, i % n) in alive_cells else State.Dead) for i in range(m * n)]
        else:
            self.cells = [Cell(i / n, i % n, randint(0, 1)) for i in range(m * n)]
        self.cell_size = 10000 // 400 #(self.m * self.n)
        self.canvas = tk.Canvas(self, bg="gray", height=self.m * self.cell_size, width=self.n * self.cell_size)
        self.show()

    def populate_grid(self):
        for cell in self.cells:
            self.grid[cell.m, cell.n] = cell.state.value

    def show(self, show_GUI=True, print_2_console=False):
        self.populate_grid()
        if print_2_console:
            print('#' * self.m * 3)
            print(self.grid)

        # Added this to check if we need to draw or update rectangles.
        if self.first == False:
            self.draw_canvas()
        else:
            self.update_canvas()

    def iterate(self):
        new_cells = []
        for cell in self.cells:
            alive_neighbours = 0
            for i in range(cell.m - 1, cell.m + 2):
                for j in range(cell.n - 1, cell.n + 2):
                    if i == cell.m and j == cell.n:
                        continue
                    else:
                        try:
                            alive_neighbours += self.grid[i, j]
                        except IndexError:
                            pass

            tmp = deepcopy(cell)
            if alive_neighbours < 2 or alive_neighbours > 3:
                tmp.kill()
            elif alive_neighbours == 3:
                tmp.birth()
            else: # == 2
                pass 

            new_cells.append(tmp)
        self.cells = new_cells
        self.show()

    def draw_canvas(self):    
        for cell in self.cells:
            if cell.state == State.Alive:
                color = 'blue'
            else:
                color = 'red'
            # Add each rectangle to a list.
            self.canvas_rect_list.append(self.canvas.create_rectangle(cell.n * self.cell_size, cell.m * self.cell_size, (1+cell.n) * self.cell_size, (1+cell.m) * self.cell_size, fill=color))

        # sets first to True after first use of draw so we do not create new rectangles later.
        self.first = True
        self.canvas.pack()
        self.update_canvas()

    def update_canvas(self): 
        for ndex, cell in enumerate(self.cells):
            if cell.state == State.Alive:
                color = 'blue'
            else:
                color = 'red'
            # Configure each item in list baste of cell state and index.
            self.canvas.itemconfig(self.canvas_rect_list[ndex], fill=color)
        self.after(500, self.iterate)


if __name__ == "__main__":
    # Change your glider and small_exploder variables to the
    # commented out ones below for an interesting pattern
    # That ends with a rotating 3 block line.
    # glider = (20, 17, ((1,3), (2,3), (2,1), (3,2), (3,3)))
    # small_exploder = (32, 30, ((12,10), (11,9), (11,10), (11,11), (12,9), (12,11), (14,10)))
    glider = (20, 17, ((1,3), (2,3), (2,1), (3,2), (3,3)))
    small_exploder = (32, 30, ((12,10), (11,9), (11,10), (11,11), (12,9), (12,11), (14,10)))
    M, N, STARTING_LIVE_CELLS, ITERATIONS = * small_exploder, 0
    g = Game(M, N, STARTING_LIVE_CELLS)
    g.mainloop()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...