company_banner

Практика программирования игр на python: жизнь



    Недавно стало известно, что python признан самым популярным языком для обучения студентов в США. Я, будучи студентом Технопарка, решил не отставать от тренда, поподробнее изучить этот модный язык и заодно написать несколько постов. Для разминки я решил реализовать Conway's Game of Life. Это довольно-таки забавная «игра», в которой мы можем в некотором смысле моделировать развитие группы организмов в окружающей среде. Правила такие: делим пространство на клетки, которые могут быть либо живыми, либо пустыми. А затем на каждом шаге состояние клетки обновляем в зависимости от числа живых соседей. Например, слишком много — клетка умирает, а если нет — рождается. Можно от души экспериментировать с конфигурациями, получаются разные странные вещи, иногда корабли. Корабли (gliders) — отдельная тема, это такие группы клеток, которые изменяются и вместе с тем путешествуют в пространстве. Кроме кораблей могут образовываться и другие группы клеток с хитрыми свойствами, но о них — в Википедии.

    Итак, python у нас, будем полагать, установлен. Для отрисовки станем использовать стандартную библиотеку Tkinter. План действия такой: рисуем двумерный массив квадратиков, и при нажатии на кнопку вызываем функцию для обновления поля в соответствии с нашими правилами эволюции. Кроме того, сделаем возможным вмешательство в текущую картину, чтобы можно было нарисовать свою панораму, с кораблями и другими фигурами.

    Теперь немного кода. Он будет идти не по порядку. Так, чтобы было наглядно.
    from Tkinter import Tk, Canvas, Button, Frame, BOTH, NORMAL, HIDDEN
    # создаем само окно
    root = Tk()
    # это ширина и высота окна
    win_width = 350
    win_height = 370
    config_string = "{0}x{1}".format(win_width, win_height + 32)
    # методом geometry() задаем размеры, тут можно написать и
    # просто строчку вида '350x370', но мы сделаем гибко
    root.geometry(config_string)
    # это ширина самой клетки, правда с учетом просвета
    cell_size = 20
    # тут начинаются более интересные вещи, создается объект типа Canvas,
    # на котором будет происходить непосредственно рисование,
    # он делается дочерним по отношению к самому окну root
    canvas = Canvas(root, height=win_height)
    # пакуем его, аналог show() в других системах
    canvas.pack(fill=BOTH)
    # определяем размеры поля в клетках
    field_height = win_height / a
    field_width = win_width / a
    
    # создаем массив для клеток, он одномерный, но ничего
    cell_matrix = []   
    for i in xrange(field_height):
       for j in xrange(field_width):
          # здесь создаем экземпляры клеток и делаем их скрытыми
          square = canvas.create_rectangle(2 + cell_size*j, 2 + cell_size*i, cell_size + cell_size*j - 2, cell_size + cell_size*i - 2, fill="green")
          canvas.itemconfig(square,  state=HIDDEN, tags=('hid','0'))
    	  # пакуем в массив
          cell_matrix.append(square)
    # это фиктивный элемент, он как бы повсюду вне поля
    fict_square = canvas.create_rectangle(0,0,0,0, state=HIDDEN, tags=('hid','0'))
    
    cell_matrix.append(fict_square)
    
    # создаем фрейм для хранения кнопок и аналогичным образом, как с Canvas,
    # устанавливаем кнопки дочерними фрейму
    frame = Frame(root)
    btn1 = Button(frame, text='Eval', command = step) 
    btn2 = Button(frame, text='Clear', command = clear)
    # пакуем кнопки
    btn1.pack(side='left')
    btn2.pack(side='right')  
    # пакуем фрейм
    frame.pack(side='bottom')
    
    # здесь привязываем события клика и движения мыши над canvas к функции draw_a
    canvas.bind('', draw_a)
    
    # стандартный цикл, организующий cобытия и общую работу оконного приложения
    root.mainloop()
    

    Теперь посмотрим на функциональную часть:
    # здесь мы обновляем картину
    def refresh():
        for i in xrange(field_height):
            for j in xrange(field_width):
                k = 0
                # считаем число соседей    
                for i_shift in xrange(-1, 2):
                    for j_shift in xrange(-1, 2):
                        if (canvas.gettags(cell_matrix[addr(i + i_shift, j + j_shift)])[0] == 'vis' and (i_shift != 0 or j_shift != 0)):
                            k += 1			
                current_tag = canvas.gettags(cell_matrix[addr(i, j)])[0]
                # в зависимости от их числа устанавливаем состояние клетки
                if(k == 3):
                    canvas.itemconfig(cell_matrix[addr(i, j)], tags=(current_tag, 'to_vis'))
                # nota bene, в этом месте можно экспериментировать с самими "правилами" игры
                if(k = 4):
                    canvas.itemconfig(cell_matrix[addr(i, j)], tags=(current_tag, 'to_hid'))
                if(k == 2 and canvas.gettags(sm[addr(i, j)])[0] == 'vis'):            
                    canvas.itemconfig(cell_matrix[addr(i, j)], tags=(current_tag, 'to_vis'))		
    
    # перерисовываем поле по буферу из второго тега элемента
    def repaint():
        for i in xrange(field_height):
            for j in xrange(field_width):			            
                if (canvas.gettags(sm[addr(i, j)])[1] == 'to_hid'):
                    canvas.itemconfig(sm[addr(i, j)], state=HIDDEN, tags=('hid','0'))
                if (canvas.gettags(sm[addr(i, j)])[1] == 'to_vis'):
                    canvas.itemconfig(sm[addr(i, j)], state=NORMAL, tags=('vis','0'))
    
    # сам шаг: обновляем состояние и рисуем
    def step():	      
        refresh()
        repaint()
    

    И вспомогательные функции:
    # функция получает координаты мыши в момент нажатия или передвижения с зажатой кнопкой
    def draw_a(e):
        ii = (e.y - 3)/cell_size
        jj = (e.x - 3)/cell_size
        # оживляем клетку
        canvas.itemconfig(cell_matrix[addr(ii, jj)], state=NORMAL, tags='vis')
    
    # эта функция преобразует двумерную координату в простой адрес нашего одномерного массива
    def addr(ii,jj):
        if(ii < 0 or jj < 0 or ii >= field_height or jj >= field_width):
            # тут адресуется фиктивная клетка	
            return len(cell_matrix) - 1
        else:
            return ii*(win_width/a) + jj
    

    Ну вот примерно так, надеюсь, вам было интересно. Ссылка на github.
    Mail.Ru Group 795,15
    Строим Интернет
    Поделиться публикацией
    Комментарии 11
    • +10
      ну и код "# считаем число соседей", простихоспади. Не спорю, все так пишут для себя, но показывать такое публично, к тому же в статье.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +2
          Простите за сарказм, но циклы ещё в школе начали получаться :)

          Но в остальном согласен.
      • +3
        github.com/gralexey/cglife/blob/master/cglife.py#L4-L5
        Вместо cell_size у вас там какая-то a, из-за чего NameError при рисовании
      • НЛО прилетело и опубликовало эту надпись здесь
        • +3
          Красивая и лаконичная реализация логики «жизни» есть в статье Перестаньте писать классы.
          • 0
            Не запускается.

            Сначала ругается на if(k = 4):, Invaild Syntax
            Потом ругается на field_height = win_height / a, NameError: name 'a' is not defined
            Потом ругается на btn2 = Button(frame, text='Clear', command = clear), NameError: name 'clear' is not defined
            Потом ругается на canvas.bind('', draw_a), _tkinter.TclError: no events specified in binding
            Потом ругается на return ii*(win_width/a) + jj, NameError: name 'a' is not defined
            Потом ругается на if (canvas.gettags(sm[addr(i, j)])[1] == 'to_hid'):, NameError: global name 'sm' is not defined
            Потом ругается на if (canvas.gettags(cell_matrix[addr(i, j)])[1] == 'to_hid'):, IndexError: tuple index out of range(тут, может быть, я неправильно исправил).

            Есть ощущение, что вы не запускали код перед отправкой. К тому же, он выглядит сырым: не обрабатываются случаи, когда число соседей >4 или <=1.
            • +1
              А какая у вас версия python? На второй все работает, попробуйте еще последнюю версию программы загрузить.
              • 0
                Я запускал код из статьи на Python 2.7.7. Невооружённым взглядом видно, что он не запустится:
                1) if(k = 4) — неправильный синтаксис в любом питоне(возможно, хабрапарсер съел знак «больше»;
                2) переменная a из win_height / a не объявлена;
                3) в Button(frame, text='Clear', command = clear) функция clear не объявлена
                4) в canvas.bind('', draw_a) первым аргументом указывается тип события
                5) переменная sm не объявлена.

                Код из github'а запускается. Код из статьи — нет.

                Кстати, статьи на хабре тоже можно редактировать.

            • 0
              PEP8 попран…

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое