Игра «Жизнь» Джона Конвея на CoffeeScript и canvas

Создание небольшой игры с помощью CoffeeScript и HTML5-элемента canvas

Игра «Жизнь» Джона Конвея (John Conway) — это игра, не требующая участия других игроков и работающая без постороннего вмешательства исходя только из начальной конфигурации. В настоящей статье вы пройдете через процесс создания собственной версии этой игры. Вы узнаете, как использовать возможности CoffeeScript и элемент HTML5 canvas для создания собственной игры. К статье прилагаются примеры программного кода.

Давид Штраус, программист, NerdKitchen

David Strauß photoДавид Штраус (David Strauß) – творчески мыслящий программист, автор полезных программ и руководитель компании NerdKitchen. Магию созидания он открыл для себя в очень юном возрасте благодаря пристрастию своего отца к работе с деревом. В основном Давид использует CoffeeScript и Ruby в среде Rails. Один из его первых проектов, Marble Run, победил в конкурсе Game On 2010, проводимом компанией Mozilla, обойдя разработки таких команд, как Fantasy Interactive и Google Chrome Team.



27.05.2013

Введение

Небольшие игры отлично подходят для изучения новых технологий. Эта статья проведет вас через процесс создания собственной версии игры «Жизнь» Джона Конвея с помощью CoffeeScript и элемента HTML5 canvas. Хотя игра «Жизнь» в сущности не является игрой, она представляет собой замечательную удобную для решения небольшую задачу.

Часто используемые сокращения

  • CSS: каскадные таблицы стилей
  • DOM: объектная модель документа
  • HTML: язык гипертекстовой разметки

Что касается самой технологии, то вам понадобятся:

  • подходящий Web-браузер, поддерживающий работу с элементом HTML5 canvas;
  • CoffeeScript — мини-язык программирования, компилирующий исходный текст в JavaScript. Рекомендуется потратить 10 минут на изучение возможностей CoffeeScript на его сайте или прочесть бесплатную онлайновую книгу «Маленькая книга о CoffeeScript» (см. раздел Ресурсы).
  • компилятор CoffeeScript, позволяющий создавать программы CoffeeScript. Рекомендуется установить Node.js и затем использовать Node Package Manager (NPM) для установки компилятора (см. раздел Ресурсы). Пример, приведенный в этой статье, использует компилятор CoffeeScript версии 1.3.3;
  • ваш любимый текстовый редактор.

Исходный код с примерами для этой статьи можно скачать здесь.


Игра «Жизнь» Джона Конвея

В сущности, игра «Жизнь» представляет собой модель, работающую на плоской сетке, состоящей из квадратных клеток. Каждая клетка может находиться в одном из двух состояний: живом или мертвом. Игра состоит из последовательности периодических обновлений, называемых также тактами. С каждым тактом текущее поколение клеток эволюционирует в следующее поколение. В каждом такте к каждой клетке применяются следующие правила:

  • живая клетка умирает от одиночества, если у нее меньше двух живых соседей;
  • живая клетка с двумя или тремя живыми соседями продолжает жить;
  • живая клетка с числом живых соседей больше трех умирает из-за перенаселенности;
  • в мертвой клетке с тремя живыми соседями зарождается жизнь в результате размножения.

Первое поколение клеток создается случайным образом. Затем модель начинает работать, пока все клетки не умрут или пока конфигурация не начнет повторяться. Подробная информация о разных конфигурациях и история создания этой игры приведены в разделе Ресурсы.

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

Рисунок 1. Пример реализации игры «Жизнь» Джона Конвея
The finished Conway's Game of Life implementation

Реализация

Приведенная в настоящей статье реализация игры «Жизнь» Джона Конвея состоит из двух частей: первая часть состоит изразметки HTML5 и CSS и является фундаментом для второй части; вторая часть собственно и является реализацией игры на CoffeeScript.


Разметка HTML5 и CSS

Первым шагом является создание директории с именем game-of-life (игра «Жизнь»), где вы будете сохранять все файлы этого примера. Разметка и CSS требуют определенного местоположения, поэтому создайте в новой директории game-of-life новый файл index.html. В листинге 1 показаны разметка HTML5 и CSS, необходимые для реализации игры «Жизнь».

Листинг 1. Разметка и CSS в файле index.html
<html>
<head>
  <title>Game of Life</title>

  <script type="text/javascript" src="javascripts/game_of_life.js">
  </script>

  <style type="text/css">
    body {
      background-color: rgb(38, 38, 38);
    }

    canvas {
      border: 1px solid rgba(242, 198, 65, 0.1);
      margin: 50px auto 0 auto;
      display: block;
    }
  </style>
</head>
<body>
  <script type="text/javascript">
    new GameOfLife();
  </script>
</body>
</html>

Код в листинге 1 создает заголовок игры и подключает файл JavaScript с именем game_of_life.js из директории javascripts. Не беспокойтесь, программировать мы будем на CoffeeScript. Включите эту строку в файл index.html (пусть вас не смущает то, что файл game_of_life.js еще не существует).

Теперь давайте немного украсим игру, чтобы она выглядела симпатично. Элемент body приобретает темный фон, элемент canvas (холст) обзаводится рамкой. С помощью атрибутов CSS margin и display мы добиваемся удобного расположения холста в центре экрана.

Для запуска игры «Жизнь» код добавляет небольшой блок сценария в тело разметки и создает новый экземпляр GameOfLife.


Реализация игры «Жизнь» с помощью CoffeeScript

Вероятно, вас удивило, что в листинге 1 подключается файл JavaScript game_of_life.js, а не файл CoffeeScript. Многие браузеры все еще не понимают CoffeeScript, поэтому нужно скомпилировать код CoffeeScript в JavaScript, чтобы браузер мог его интерпретировать. Именно по этой причине вам понадобятся еще две директории внутри директории game-of-life. В первой (с именем coffeescripts) будет храниться код CoffeeScript, а во второй (javascripts) — скомпилированный код JavaScript.

Наш пример реализации содержит всего один класс, поэтому вам придется создать всего один соответствующий файл. Файл game_of_life.coffee, который будет содержать весь код CoffeeScript, расположен в директории coffeescripts.


Компиляция CoffeeScript

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

coffee --output javascripts/ --watch --compile coffeescripts/

Чтобы подать эту команду, откройте свой любимый инструмент командной строки и перейдите в директорию game-of-life. Введите команду coffee, чтобы скомпилировать код CoffeeScript в JavaScript. На первом месте в нашем примере указана выходная папка с флагом --output. Затем используются флаги --watch и --compile, указывающие на директорию coffeescripts.

Что все это значит? Каждый раз, когда файл в директории coffeescripts изменяется, команда coffee берет его, компилирует и сохраняет скомпилированный файл JavaScript в директорию javascripts. Теперь вы знаете, почему мы не создаем файл game_of_life.js, который подключается в листинге 1. Когда вы заносите код CoffeeScript в файл game_of_life.coffee, он компилируется, и результирующий код JavaScript автоматически сохраняется в файле game_of_life.js.


Инициализация игры

Теперь, когда мы разобрались с компилятором, можно приступить к программированию нашей версии игры «Жизнь». Откройте файл game_of_life.coffee в текстовом редакторе. Как показано в листинге 2, здесь присутствует единственный класс GameOfLife с несколькими атрибутами.

Листинг 2. Класс GameOfLife и его атрибуты
class GameOfLife
  currentCellGeneration: null
  cellSize: 7
  numberOfRows: 50
  numberOfColumns: 50
  seedProbability: 0.5
  tickLength: 100
  canvas: null
  drawingContext: null

Говорящие имена переменных и методов позволяют легко разобраться в этом исходном коде. Приведем некоторые пояснения к листингу 2:

  • поскольку игра «Жизнь» состоит из двумерного набора клеток, наш пример должен иметь подобную же структуру. Атрибут currentCellGeneration будет содержать все клетки двумерного массива;
  • cellSize указывает ширину и высоту отдельной клетки – в нашем случае это семь пикселов;
  • атрибуты numberOfRows и numberOfColumns определяют размер поля;
  • игре «Жизнь» нужна начальная («затравочная») конфигурация клеток. Атрибут seedProbability используется при создании затравочной конфигурации для определения того, мертва клетка или жива;
  • атрибут tickLength определяет интервал обновления игры. В нашем примере игра обновляется каждые сто миллисекунд;
  • атрибут canvas будет содержать элемент canvas (холст), который вам предстоит создать;
  • чтобы нарисовать рисунок на холсте, вам понадобится графический контекст, который хранится в атрибуте drawingContext.

За настройку игры отвечает конструктор класса GameOfLife. Как показано в листинге 3, сначала нужно создать холст, а уже потом растянуть его до нужных размеров. Затем можно использовать вновь созданный холст для создания графического контекста. После этого вы будете готовы к созданию начальной структуры и запуску цикла игры путем запуска первого такта. Но сначала давайте создадим холст.

Листинг 3. Конструктор класса GameOfLife
  constructor: ->
    @createCanvas()
    @resizeCanvas()
    @createDrawingContext()

    @seed()

    @tick()

Поскольку современные браузеры предлагают замечательный API для управления объектной моделью документа (DOM) и поскольку в нашем примере нет ничего хитроумного, вы можете использовать внешние системы, такие как jQuery. Используйте метод document.createElement для создания нового элемента canvas, который затем сохраняется в одноименном атрибуте. Добавьте вновь созданный элемент к телу страницы. Все это происходит в методе createCanvas, как показано в листинге 4.

Листинг 4. Подготовка элемента canvas
  createCanvas: ->
    @canvas = document.createElement 'canvas'
    document.body.appendChild @canvas

  resizeCanvas: ->
    @canvas.height = @cellSize * @numberOfRows
    @canvas.width = @cellSize * @numberOfColumns

  createDrawingContext: ->
    @drawingContext = @canvas.getContext '2d'

Метод resizeCanvas использует атрибуты cellSize, numberOfRows и numberOfColumns для расчета ширины и высоты элемента canvas. Третий метод в листинге 4, createDrawingContext, получает 2-й контекст из canvas и сохраняет его для дальнейшего применения.

Кроме этих трех методов, конструктор в листинге 4 использует два дополнительных метода: seed и tick. Они содержат солидную часть кода и будут обсуждаться в следующих разделах.


Создание начальной конфигурации

Игре «Жизнь» нужна начальная конфигурация. Исходя из этой конфигурации, клетки на поле начинают эволюционировать, с каждым тактом переходя в новое поколение. Для создания начальной конфигурации (затравки) нужно принять для каждой клетки случайное решение – будет ли она жива или мертва. Как показано в листинге 5, для этого используется метод seed. Два вложенных цикла позволяют посетить каждую клетку поля.

Внешний цикл перебирает все строки, что выполняется с помощью функции CoffeeScript, называемой ranges. Три точки (.) в выражении for row in [0...@numberOfRows] указывают на то, что диапазон не включает крайнее значение. Так, если numberOfRows имеет значение 3, переменная цикла (в данном случае row) будет принимать значения в диапазоне от 0 до 2. Это позволяет создать двумерный массив currentCellGeneration.

Внутренний цикл перебирает все столбцы и создает для каждой клетки новое значение seedCell. Он вызывает метод createSeedCell с текущим значением строки и столбца. После создания всех клеток начальной конфигурации он сохраняет их в соответствующем месте в currentCellGeneration.

Листинг 5. Начальная конфигурация
  seed: ->
    @currentCellGeneration = []

    for row in [0...@numberOfRows]
      @currentCellGeneration[row] = []

      for column in [0...@numberOfColumns]
        seedCell = @createSeedCell row, column
        @currentCellGeneration[row][column] = seedCell

  createSeedCell: (row, column) ->
    isAlive: Math.random() < @seedProbability
    row: row
    column: column

Создание новой клетки выполняется очень просто. Клетка является простым объектом и состоит из трех атрибутов. Метод createSeedCell в листинге 5 просто передает аргументы row (строка) и column (столбец) объекту cell (клетка). Атрибут isAlive определяет, жива клетка или мертва. С помощью метода Math.random и атрибута seedProbability вы случайным образом создаете живые и мертвые клетки. Возможно, вы обратили внимание на отсутствие ключевого слова return. Это связано с тем, что методы CoffeeScript автоматически возвращают свое конечное значение.


Игровой цикл

Теперь, когда вы создали начальную структуру, пришло время запустить игру. Нужно нарисовать на холсте текущее поколение клеток и преобразовать его в следующее поколение. Как показано в листинге 6, это преобразование начинается с вызова метода tick. Метод tick в листинге 6, делает три вещи:

  • рисует текущее поколение клеток с помощью метода drawGrid.
  • преобразует текущее поколение клеток в следующее;
  • выдерживает паузу (таймаут), определяющую скорость работы игры.

Метод setTimeout требует использования двух аргументов. Первый аргумент представляет собой метод, который нужно вызвать — в данном случае это сам метод tick. Второй аргумент определяет время в миллисекундах, которое должно пройти перед вызовом метода. С помощью атрибута tickLength можно регулировать скорость игры.

Вероятно, вы заметили разницу между методом tick и всеми другими методами. Метод tick использует функцию CoffeeScript, обозначенную жирной стрелкой (=>). Жирная стрелка привязывает метод к текущему контексту, который всегда будет корректным (без этого таймаут работать не будет).

Листинг 6. Метод tick игрового цикла
  tick: =>
    @drawGrid()
    @evolveCellGeneration()

    setTimeout @tick, @tickLength

Рисование поля выполняется просто. Метод drawGrid в листинге 7 использует два вложенных цикла для посещения каждой клетки поля и передает клетку в метод drawCell. Метод drawCell рассчитывает координаты x и y с помощью cellSize, а также атрибуты строки и столбца клетки. В зависимости от атрибута isAlive вы устанавливаете стиль заполнения клетки. Перед использованием методов strokeRect и fillRect для рисования клетки установите атрибуты холста strokeStyle и fillStyle.

Листинг 7. Рисование поля
  drawGrid: ->
    for row in [0...@numberOfRows]
      for column in [0...@numberOfColumns]
        @drawCell @currentCellGeneration[row][column]

  drawCell: (cell) ->
    x = cell.column * @cellSize
    y = cell.row * @cellSize

    if cell.isAlive
      fillStyle = 'rgb(242, 198, 65)'
    else
      fillStyle = 'rgb(38, 38, 38)'

    @drawingContext.strokeStyle = 'rgba(242, 198, 65, 0.1)'
    @drawingContext.strokeRect x, y, @cellSize, @cellSize

    @drawingContext.fillStyle = fillStyle
    @drawingContext.fillRect x, y, @cellSize, @cellSize

Для преобразования текущего поколения клеток используются три метода. Метод evolveCellGeneration показан в листинге 8. Подобно методу seed, он использует два вложенных цикла для создания двумерного массива с именем newCellGeneration, в котором будет сохраняться преобразованное поколение клеток. Внутренний цикл передает клетку методу evolveCell, который возвращает преобразованную клетку. Затем преобразованная клетка сохраняется в нужной позиции в массиве newCellGeneration. После преобразования всех клеток текущего поколения можно обновить атрибут currentCellGeneration.

Листинг 8. Преобразование текущего поколения клеток
evolveCellGeneration: ->
    newCellGeneration = []

    for row in [0...@numberOfRows]
      newCellGeneration[row] = []

      for column in [0...@numberOfColumns]
        evolvedCell = @evolveCell @currentCellGeneration[row][column]
        newCellGeneration[row][column] = evolvedCell

    @currentCellGeneration = newCellGeneration

Метод evolveCell в листинге 9 начинается с создания переменной evolvedCell с теми же атрибутами, что и у переданной клетки. Чтобы решить, умрет клетка, родится или останется живой, нужно знать, сколько у нее живых соседей. Чтобы узнать число живых соседей, вызовите метод countAliveNeighbors для данной клетки. Этот метод подсчитывает и возвращает число живых соседей.

Узнав число живых соседей, вы сможете в соответствии с правилами игры обновить атрибут isAlive преобразуемой клетки. После обновления этого атрибута просто верните объект evolvedCell.

Листинг 9. Преобразование одной клетки
  evolveCell: (cell) ->
    evolvedCell =
      row: cell.row
      column: cell.column
      isAlive: cell.isAlive

    numberOfAliveNeighbors = @countAliveNeighbors cell

    if cell.isAlive or numberOfAliveNeighbors is 3
      evolvedCell.isAlive = 1 < numberOfAliveNeighbors < 4

    evolvedCell

Метод countAliveNeighbors в листинге 10 получает в качестве аргумента одну клетку и возвращает число живых соседей. Обычно клетка имеет восемь соседей, но если она расположена у края поля, число соседей будет меньше. Подсчет живых соседей не так уж и прост.

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

Совсем иначе дело обстоит при расположении клетки в строке 0 и столбце 0. Ее соседи находятся в диапазоне строк от 0 до 1 и в диапазоне столбцов от 0 до 1. Нижняя граница строки определяется номером строки минус один, но она не может быть меньше нуля. Этот алгоритм можно реализовать с помощью метода Math.max, как показано в листинге 10. Расчет нижней границы столбца выполняется аналогично.

Верхняя граница рассчитывается методом Math.min. Убедитесь, что число, равное номеру строки клетки плюс один, не превышает номера последней строки. Получив верхнюю и нижнюю границу для строк и столбцов, можно перебрать все клетки в этой области с помощью двух вложенных циклов. В данном случае мы используем оператор неявного диапазона CoffeeScript, чтобы гарантировать учет значений upperRowBound и upperColumnBound.

Саму клетку подсчитывать не нужно, поэтому следует вставить во внутренний цикл выражение continue, которое вызывается, когда значения строки и столбца совпадают с атрибутами клетки. После этого увеличьте счетчик numberOfAliveNeighbors на единицу, если текущая проверяемая клетка жива. В конце цикла нужно просто вернуть значение этого счетчика.

Листинг 10. Подсчет числа живых соседей клетки
  countAliveNeighbors: (cell) ->
    lowerRowBound = Math.max cell.row - 1, 0
    upperRowBound = Math.min cell.row + 1, @numberOfRows - 1
    lowerColumnBound = Math.max cell.column - 1, 0
    upperColumnBound = Math.min cell.column + 1, @numberOfColumns - 1
    numberOfAliveNeighbors = 0

    for row in [lowerRowBound..upperRowBound]
      for column in [lowerColumnBound..upperColumnBound]
        continue if row is cell.row and column is cell.column

        if @currentCellGeneration[row][column].isAlive
          numberOfAliveNeighbors++

    numberOfAliveNeighbors

Поскольку CoffeeScript упаковывает каждый файл в собственную оболочку, нужно экспортировать класс GameOfLife, чтобы его можно было использовать за пределами собственного файла. Добавьте атрибут GameOfLife к объекту window следующим образом: window.GameOfLife = GameOfLife.

Вот и все! Вы завершили пример реализации игры «Жизнь» Джона Конвея. Открыв файл index.html в браузере, вы можете увидеть собственную версию этой игры, как показано на рисунке 1. Если что-то пойдет не так, вы можете сравнить свою версию с полным исходным кодом самого автора (см. раздел Ресурсы).


Заключение

Хотя игра «Жизнь» Джона Конвея (John Conway) совсем мала и имеет очень простые правила, ее программирование сопряжено с некоторыми каверзными проблемами, которые приходится преодолевать. Игра является замечательным примером для изучения нового языка программирования или для повышения уровня мастерства.

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


Загрузка

ОписаниеИмяРазмер
Пример исходного кода для статьиgame-of-life.zip4 КБ

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура
ArticleID=931460
ArticleTitle=Игра «Жизнь» Джона Конвея на CoffeeScript и canvas
publish-date=05272013