Пользователь
0,4
рейтинг
11 января 2011 в 21:49

Разработка → Javascript и canvas в игре «Жизнь» Джона Конвея

Напишем эту алгоритмическую игру [1] так, чтобы извлечь из неё максимальную образовательную пользу в области алгоритмов, языка Javascript, хорошего стиля программ, умения оптимизировать код. Центральным местом обсуждения будет не игра, а код, способы реализации, оптимизация.

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

Недавно проведённый на Хабре опрос [3] показал реальную картину — 20% программистов написали когда-либо её работающую реализацию, а порядка 10% о ней не слышали. Что ж, тем интереснее будет оставшимся 80% узнать, что можно извлечь из реализации игры.

Что это за игра?


Полноценный ответ даст тег «игра жизнь» на хабре [1] или статья из Википедии [2]. Другой перевод названия игры на русский — «Эволюция» (встречалось в журнале «Наука и жизнь» 70-х годов). Другой перевод фамилии автора на русский — Конуэй (John Conway, game «Life»). Вот что говорит сам Конвей об игре (youtube, 4 мин.) [4], там же — наглядный рассказ о правилах игры (англ.).

Возможно, причина непоголовной популярности в узких кругах в том, что, как написал в комментарии Danov, "… варкрафты и прочие симуляторы свели на нет восторг от движущихся квадратиков". (Но настоящих математиков должно интересовать не это?)

Если поискать результаты на youtube.com по словам «life conway», то найдём массу примеров демонстрации игровых полей как «Жизни» [5], так и игр по похожим правилам, затем по связям — демо эволюционных алгоритмов, поиска кратчайшего пути обхода и другие интересные наглядные демо. Стало быть, над ними где-то идёт упорная «плодотворная» работа.

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

Множество реализаций на Javascript можно найти поиском по google — «game life javascript». Но, в силу врождённой природной лени образованных людей, не будем вдаваться в их детали. Уже потом, после реализации, попробуем когда-нибудь сравнить эффективность того или другого решения. (Я это попробовал сделать, но дальше 2-го решения смотреть не захотелось по качеству результата.)

Идея реализации


Сделаем её на торе (точнее, спирали), организованном в виде линейного массива, чтобы не заботиться о граничных эффектах. Параметры высоты и ширины — переменные, поэтому предельная скорость смены поколений может регулироваться размерами рабочего поля. Начальная точка массива смещается с каждым поколением, чтобы результаты записывать в тот же массив. На скорость будет влиять метод визуализации: текст, таблица с фоновыми цветами, canvas, рисунки.

Почему именно так? Индекс одномерного массива вычисляется наиболее быстро, поэтому, если задача приводима к одномерному массиву, этим полезно воспользоваться. Помнится, так я её делал на GW-Basic на текстовом поле 80 на 50, чтобы предельно ускорить, и удовлетворил любопытство наблюдения за эволюцией наиболее долгоиграющей 5-точечной фигуры, напоминающей знак квадратного корня. Правда, в тор 80 на 50 она не поместилась.

На JS удобно её сделать потому, что скорости и возможности современных браузеров дают изобразить всё необходимое на одной странице, следовательно, отладка будет быстрой и удобной, а рабочей среды не нужно — всё есть в браузерах. Полезен будет универсальный текстовый редактор с подсветкой синтаксиса (Notepad++, E Editor, UltraEdit, ...).

Неделя ООП на Хабре


Далее, вспомним, что мы, пишущие просветительские статьи, в ответе за всех кого приучим писать плохо в JS, в традициях 2000-2005-х годов, когда никто не задумывался о глобальных переменных. Я до вчерашнего дня тоже не задумывался и писал как придётся, но тут вдруг осенило… Давайте даже такую простую задачу оформим в лучших традициях ООП, инкапсулируя детали реализации в объект. Не зря ведь на Хабре прошла неделя ООП:

habrahabr.ru/blogs/cpp/111120 — «10 лет практики. Часть 1: построение программы».
habrahabr.ru/blogs/development/111125 — «Мысли об ООП».
habrahabr.ru/blogs/development/111162 — «Зачем ООП».
МакКоннел — «Совершенный код».
habrahabr.ru/blogs/javascript/111393 — «Обёртки для создания классов: зло или добро?».

Правда, классов и наследования нам будет не нужно, только инкапсуляция реализации в конструкторе.

Ниже будет скрипт с теми самыми перегруженными методами, которые, если бы применялись в наследовании, дали бы тот самый не рекомендуемый в последней статье эффект размножения функций и траты памяти. Но писать неоправданно на каждой функции слово «prototype» тоже не хочется и дублировать решения Mootools в нативном коде — тоже.

Вообще, хороший обзор по способам организации ООП в JS лежит известно где [6], поэтому следующим шагом для самостоятельного написания хорошего кода должно стать изучение этой статьи. Но пока я не вижу настоятельной необходимости перехода к различным стилям кодирования.

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

Скрипт


Чтобы не засорять статью долгими кодами с подсветкой (спойлеров на хабре нет), опубликуем их на другом ресурсе.
   КОД НАХОДИТСЯ ЗДЕСЬ
Написаны функции для минимального комфорта и «подогрева дальнейшего энтузиазма»: рабочее (игровое) поле нужного размера, несколько стартовых полей, варианты оптимизаций. Теперь остаётся простор для развития, для сравнения методов оптимизации и проверка того, не перестарались ли в усердии, в затратах времени вычислений, да и во времени написания кода.

Как и где пользоваться скриптом


Достаточно взять приведённый код через copy-paste в страницу HTML, чтобы увидеть его работу. Или посмотреть страницу spmbt.github.io/spmbt/lifeConway.htm, [дубль].

Если размер поля слишком большой для экрана монитора (у меня требуется не менее 1400 на 1050), уменьшаем числа в полях «Ширина, „Высота“, затем нажимаем на одну из кнопок „Init...“. Простейший интерфейс на кнопках позволяет пронаблюдать описанные в статье действия со скриптом и время исполнения от нажатия кнопки до остановки выполнения.

Визуализация


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

Действительно, замеры скоростей первого варианта на Опере 10.51 показало, что время расчёта нового поколения было 30%, а время отрисовки — 70%. Неплохое соотношение для визуализации. И за 10 секунд отрисовывалось 1000 полей размером 180х150. Если всё запустить в рабочем цикле с просмотром каждого поколения, будет медленнее (reflow) — за 75 секунд (сильно зависит от процессора, браузера и видеокарты).

Сравнение скоростей работы скриптов в браузерах


Результаты проверки на случайном заполнении поля 180х150, 100 поколений. (E2180, 2.0 ГГц, видео Radeon X1650, WindowsXP)
(Для корректности всё поле должно быть на экране, окно — в фокусе, других действий на компьютере — не производиться.)
IE8 34c.
Opera 10.51 7.5c.
FF 3.6.10 125c.
Chrome 8 4.3c.
Safari 5.02 25c.
Да, в FF с этим скриптом — полный провал на фоне остальных. При очень больших полях без движений мыши над окном даже пропускает кадры — экономит отображение, обнаруживая полную загруженность скриптом. Как показывают другие замеры через doStep2(), тормозит расчёт в массивах: 1.3 секунды на поколение. В то же время, 23 секунды на 1000 отрисовок поля.

Оптимизация для Firefox


Анализ показывает, что 80% времени расчёта массива 180х150 отъедает перемещение вычисленных значений (w+1 штук, немного) в начало. И интересный факт, что первый расчёт поколения — на 100 мс быстрее следующих (за это „отвечает“ a.splice(0, w1+w1);). Значит, источник торможения — не столько сам массив, сколько часть работы с ним.

Конечно, когда код был написан первый раз, он создавался сознательно кратко, в ущерб оптимизациям, но в пользу выразительности. Переписываем перемещение значений другим способом. И первый цикл, и последующие стали длительностью не 120-200 мс, а 90-100. (Будем знать, что .splice() реализован в FF неудачно.) Новый код:
	var b=[];
	b = a.slice(iStart +w1+1, iStart +w1+w1+1).concat(a.slice(w1+w1, iStart+w1+1));
	a=b;
Тестирование всех браузеров на новом коде.
IE8 14.5с.
Opera 10.51 7.0c.
FF 3.6.10 12.5c.
Chrome 8 4.1c.
Safari 5.02 5.5c.
Оказалось, что оптимизация FF помогла заметно ускориться другим браузерам (кроме Opera-Chrome, которые и так быстры).

Что полезного получили от абстрактной игры? Направления дальнейших занятий


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

Для будущего развития подготовлена подходящая база выбором обдуманного дизайна кода программы. В зависимости от того, в каком направлении будет актуальна дальнейшая специализация, можем выбрать пути дальнейшего совершенствования программы.
1) интерфейс работы с мышью: кликами и протяжками мыши вставлять точки, очищать области, добавлять и вращать библиотечные элементы, расширять или обрезать рабочее поле, сдвигать всё игровое поле;
2) хранить библитечные элементы;
3) отработать профилирование работы частей программы;
4) визуально отображать статистику поколений в игре: изменение цвета точек в зависимости от возраста, показывать плотность и активность изменений;
5) оптимизация скорости расчёта за счёт обнаружения и исключения пустых полей (вести статистику пустого окружения на базе тех же okr, чтобы на следующий ход знать, вычислять ли здесь новое значение);
6) бросить всё и заниматься делом: в реальных программах тоже можно отработать для себя совершенные методики — за это ещё платят.

Иные формы „Жизни“


Среди правил поведения клеточных автоматов возможны вариации с интересными результатами. Упоминали клон правил с гексагональной решёткой. Можно представить и попытаться визуализировать трёхмерные решётки, наблюдать время жизни ячеек, изменить правила на простой решётке. Например, достаточно давно математики предложили клон игры с учётом возраста клеток — молодые более выносливы, чем старые. tangro сообщил, что писал многопользовательскую игру: „два игрока строят на своих половинах поля фигуры с целью уничтожить фигуры противника“. Есть варианты алгоритмов клеточных автоматов на графах.

Пример изменения правил


Попробуем расширить правила в рамках традиционных клеток (метод .step()). Если свести правила не к логике, а в таблицу, то получится простая и универсальная таблица — зависимость следующео значения клетки от суммы значений окружающих клеток.
Число точек окружения Следующее значение поля,
если было 0
Следующее значение поля,
если было 1
0 0 0
1 0 0
2 0 1
3 1 1
4 0 0
5 0 0
6 0 0
7 0 0
8 0 0
Вот и все правила. Реализация основного цикла через таблицу показала, что „просто логика“ работает на 10% быстрее. Поэтому классическую игру лучше писать через простую логику (здесь, для JS), а табличный цикл используем для изысканий того, какие могли бы быть правила. В реализации этот режим включают радиокнопкой „Кл. на матр.“ (»Классика на матрице"). В смеси с отображением на экране замедления расчётов совершенно не заметно (около 1%, по оценке). Его можно использовать для экспериментов по модификации правил игры.

//правило поколения (классика)
var prav = [0,0,0,1,0,0,0,0,0,0, 	0,0,1,1,0,0,0,0,0,0];
 //основной цикл (180*150, 1000 поколений за 4.5 с.(без графики))
for(i = iStart + w1; i >= w1; i--){
	okr = a[i-w1] + a[i-w] + a[i-w1m]
		+ a[i-1] + a[i+1]
		+ a[i+w1m] + a[i+w] + a[i+w1];
	a[i+w1] = prav[10*a[i] + okr];
}

Несложно заметить, что небольшие изменения в правилах приводят или к пустым полям, или к хаосу (а может быть, это не хаос, а настоящая жизнь? :) ), а самое занимательное поведение происходит именно по конвеевским правилам.

Модификация И.Сидорова (1975 г.)


Пройдём по пути модификации правил, (старая статья в «Науке и жизни» 1975 г., [10]), которые учитывают возраст точек. Молодые более жизнестойки, но и более агрессивны к старым. Автор модификации — и есть автор статьи, инженер-физик И. Сидоров.

Вот описание на словах из livejournal: "… другие правила — клетки двух типов, молодая и старая. После рождения клетка 1 ход — молодая, потом превращается в старую. Молодые клетки не умирают. Старые клетки без молодых соседей выживают по Конуэевским правилам. Старые клетки с молодыми соседями должны иметь 2 соседа (либо обе-молодые, либо — молодая и старая), иначе она умирает. Рождаются клетки по таким же правилам, как у Конуэя."

Чтобы описать правила выживания старых клеток и уложиться в ту же решающую матрицу, понадобится назвать молодую клетку числом 5, а старую — числом 4. Получаем такую решающую матрицу.
Сумма значений точек окружения Следующее значение поля,
если было 0
Следующее значение поля,
если a[i] было =5 («молодая» точка)
Следующее значение поля,
если a[i] было =4 («старая» точка)
с 0 по 7 0 4 0
с 8 по 11 0 4 4
12 5 4 4
с 13 по 15 5 4 0
более 16 0 4 0
Для реализации напишем код
if(rule ==11){ //правила с учётом молодости точек
	var prav = [0,0,0,0,0, 0,0,0,0,0,   0,0,5,5,5, 5,0,0,0,0, 	0,0,0,0,0, 0,0,0,0,0,	0,0,0,0,0, 0,0,0,0,0,0
	           ,0,0,0,0,0, 0,0,0,0,0,   0,0,0,0,0, 0,0,0,0,0, 	0,0,0,0,0, 0,0,0,0,0,	0,0,0,0,0, 0,0,0,0,0,0
	           ,0,0,0,0,0, 0,0,0,0,0,   0,0,0,0,0, 0,0,0,0,0, 	0,0,0,0,0, 0,0,0,0,0,	0,0,0,0,0, 0,0,0,0,0,0
	           ,0,0,0,0,0, 0,0,0,0,0,   0,0,0,0,0, 0,0,0,0,0, 	0,0,0,0,0, 0,0,0,0,0,	0,0,0,0,0, 0,0,0,0,0,0
	           ,0,0,0,0,0, 0,0,0,4,4,   4,4,4,0,0, 0,0,0,0,0, 	0,0,0,0,0, 0,0,0,0,0,	0,0,0,0,0, 0,0,0,0,0,0
	           ,4,4,4,4,4, 4,4,4,4,4,   4,4,4,4,4, 4,4,4,4,4, 	4,4,4,4,4, 4,4,4,4,4,	4,4,4,4,4, 4,4,4,4,4,4];
	for(i = iStart + w1 + w1; i >=0; i--){if(a[i] ==1) a[i] =4;}
	for(i = iStart + w1; i >= w1; i--){ //основной цикл
		okr = a[i-w1] + a[i-w] + a[i-w1m]
			+ a[i-1] + a[i+1]
			+ a[i+w1m] + a[i+w] + a[i+w1];
		a[i+w1] = prav[41*a[i] + okr];
	}
}
И дополним метод this.show() строчкой для цифр «4» и «5»:
var s = a.join('').replace(/0/g,'\xb7').replace(/[14]/g,'o').replace(/5/g,'s');
Посмотрим, как с этими правилами работает поле со случайным заполнением. Перейти к просмотру игры по этим правилам в законченной программе можно, выбрав радиокнопку «Sidorov's».

Правила проверены и перепроверены, прочитаны даже ещё раз на скане журнала. Ошибок в реализации нет, статические конвеевские конструкции, действительно, живут, «кванты» летают (проверяется, немного изменив initLib(), подставив «квант для клона»). Но, к сожалению, случайно заполненное превращается в хаос. Из чего можно сделать вывод, что правила несовершенны. Наверное, поэтому о них дальнейших упоминаний не было. Крупные хаотические структуры легко неопределённо долго живут без прихода к циклическим вариантам. Очевидно, причина — в избыточной устойчивости молодых клеток. Им нужно определить некоторые правила умирания при окружении более N соседей, тогда, может, что-то получится.

Таким образом, мы создали инструмент для модификации и исследования иных правил игры. Хорошо было бы разобраться, как сделали симметричную (относительно 0 и 1) вариацию игры [7], но придётся это сделать не сейчас, потому что близится 11 января.

В мире существует много программ, поддерживающих игру и её модификации. Существуют даже форматы сохранения данных для игры. Для знакомства достаточно посмотреть на один из сборников ссылок по теме [9]. Поэтому, на самом деле, процесс интеграции только что описанной программы с огромным множеством наработок у нас ещё даже не начинался, а часы, уже потраченные другими исследователями на изыскания, измеряются тысячами.

UPD (12.01.2011): Вариация правил Day & Night [7]


В программу добавлена вариация правил "Day & Night", описанная в Википедии и интересная тем, что при смене значений полей с 0 на 1 и наоборот — ничего не изменится, просто мир станет «инверсным», но живущим по таким же законам. С подготовленной функциональностью сделать дополнение оказалось очень просто (к чему и стремились).
if(rule==2){
	var prav = [0,0,0,1,0, 0,1,1,1,1, 	0,0,0,1,1, 0,1,1,1,1]; //правило поколения (DayNight)
	for(i = iStart + w1; i >= w1; i--){ //основной цикл
		okr = a[i-w1] + a[i-w] + a[i-w1m]
			+ a[i-1] + a[i+1]
			+ a[i+w1m] + a[i+w] + a[i+w1];
		a[i+w1] = prav[10*a[i] + okr];
	}
}
В этом мире существуют свои осцилляторы. Поле стремится перейти к статическим и осциллирующим объектам, лёгкого возникновения движущихся объектов не наблюдается, но они есть. Все правила и небольшое введение можно почитать на английском в приложенном там архиве, но они видны и приведённом выше коде. Готовую программу можно запускать там же, где остальные, на одной из страниц демонстрации: [*], дубль; на codepaste.ru дополнения нет (почему-то не могу авторизоваться, чтобы сменить версию кода). Нужно выбрать радиокнопку «DayNight», а дальше — как обычно.

Для наблюдений немного изменены начальные условия на кнопках: случайное поле заполняется с плотностью не 1/6, а 1/2. Оно с течением времени склонно разбиваться на небольшие «белые» и «чёрные» области, в которых живёт статика и осцилляторы. Исходная матрица блоков 2х2 с 1 лишней точкой «взрываться» не думала, поэтому добавил ещё одну точку для инициации процесса. После чего «взрыв» тоже получается красивый и своеобразный.

Долгоживущая в обычной «Жизни» фигура (кнопка «Init 'r'») оказывается в этом мире осциллятором с периодом 16. Чтобы можно было удобно задавать любые фигуры, дописан цикл выкладки фрагмента поля (например, на кнопку «Init 'r'» повешено взятое из описания "[Figure 15. Two p32 ships collide to form a rocket]" — столкновение 2 кораблей). Затем получившаяся ракета направлена на осциллятор. К 400-му ходу видим «клубящееся облако» после столкновения ракеты с осциллятором, которое вскоре пропадает.


Как оказалось, это довольно интересная модификация игры, с собственным своеобразным поведением.

Визуализация на canvas


Думать о лучшей визуализации заставляют 3 причины:
1) влияние стилей отображения текстов в браузерах на текстовые строки, составляющие игровое поле (есть непреодолимые настройки браузера «минимальный размер шрифта»);
2) время отображения больше времени вычисления шага;
3) большие поля в браузерах работать могут, но при размере клетки в 1 символ они не помещаются в окне.

Не будем испытывать нагромождения рисунков, ячеек таблиц, блоков, абсолютные позиции десятков тысяч элементов — есть сомнения в их простоте организации, следовательно — в скорости работы. Будем рисовать на холсте canvas.

Используем для начала быстрый, но не самый эффективный метод: отрисовку каждой точки при каждом ходе, чтобы оценить мощность. Понятно, что затем его можно ооптимизировать, отрисовывая спрайты (putImageData()). Но даже в первом приближении canvas даёт от него ожидаемое: скорость работы возрастает, и масштаб можно уменьшить в 2-3 раза.

Чтобы включить режим canvas («холст»), включаем чекбокс «на canvas», меняем размер точки («размер»), варианты отрисовки (чекбоксы «rect», «сетка»), затем выбираем один из «init ...», затем — «Step».
Устроим отрисовку 4 вариантов: круги, прямоугольники, с сеткой и без неё. Конечно, прямоугольники должны рисоваться быстрее, что подтверждают опыты. При малых клетках уже не имеет значения форма точки, поэтому имеем экономию без потери качества.

Сетка — явно невыгодное занятие — рисовать каждую точку. Если бы понадобилось оптимизировать, лучше делать canvas немного прозрачным, а под ним располагать сетку. Ещё лучше, объявить фоновый рисунок на самом canvas. В нашей программе мы можем оценить, насколько отрисовка сетки невыгодна. Если точек немного (100 на 200), то можно согласиться с их отрисовкой.

Заметим, что теперь чем больше точек на экране, тем медленнее работает скрипт, потому что ему надо рисовать каждую непустую точку. Ещё заметно, что рисование окантовки круга (stroke()) очень плохо переваривается скриптом. Поэтому я не стал выводить её в чекбокс, а просто закомментировал — кто интересуется, тот проверит.

Под конец выложим самые красивые достижения: «взрыв» регулярной матрицы блоков в течение 200 ходов на поле 408 на 402 в наиболее быстром для canvas браузере (Opera); таблицу скоростей обработки разных режимов в разных браузерах; картинку результата «взрыва» блочной матрицы" — из 1 точки действие выполнилось за 38 секунд, 5.3 шага в секунду.

Жизнь случайно заполненного начального поля, 100 шагов (e2180, 2.0 ГГц, ...)
Браузер, 180x150, время выполнения (с) текст, 12px canvas, круг, 6px, сетка canvas, круг, 6px canvas, квадрат, 6px canvas, квадрат, 4px
(вид клеток)
IE8 14.5 - rest in peace
Opera 10.51 7.7 18 6.8 4.5 4.2
FF 3.6.10 15 41 14.5 12 14
Chrome 8 4.2 46 7.6 5.0 4.0
Safari 5.02 5.5 61 10.8 5.3 4.8


Разрушение стабильной матрицы из блоков 2x2, вызванное одной лишней клеткой.


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

Заключение


Выполнен вариант программирования игры. Часть строк кода написаны без учёта принципов качественного кода — например, есть повторения операторов. Вероятно, к качеству кода нужно выработать чутьё, а, возможно, это есть нормальная практика — писать не используемый в будущем код «как придётся», а затем переписывать заинтересовавшие части.

Ссылки


*. Работающая реализация для статьи. (11.01.2011), дубль.

1. Выборка по тегу «игра жизнь»

2. Википедия — Жизнь (игра)

3. Опрос для программистов: писали ли Вы реализацию игры «Жизнь»Конвея?

4. John Conway Talks About the Game of Life Part 1 (4:10)
www.conwaysgameoflife.net — Conway's Game of Life

5. youtube.com, «game life conway». Наиболее интересные ролики:
5.1. Amazing Game of Life Demo (4:47) много очень сложных организованных конструкций.
5.2. Cellular Automata: Conway's Game of Life (3:56)
5.3. Conway's Life — Cyclic Universe (0:40)

6. ООП в Javascript: наследование. И. Кантор

7. Day & Night, симметричная реализация правил относительно значений полей «0» и «1» в игре, подобной «Жизни».

8. Математическая игра “Жизнь”. Другие программы и описание игры.

9. Ссылки по игре «Жизнь».

10. Описание модифицированных правил с учётом возраста клеток из статьи в «Науке и Жизни», 1975, #3, «Эволюция игры „Эволюция“», И.Сидоров (архив — DjVu). Сканы этой и похожих статей.

*) Идея для ненормального программирования: игра Жизнь на Экселе без макросов.

**) Во время написания статьи ни один рабочий час не пострадал.
spmbt @spmbt
карма
157,5
рейтинг 0,4
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (23)

  • +2
    Что плохо в коде:
    Груда кода в глобальном namespace. Зачем?
    Не нужен флаг isIE, нужно проверять возможности, которые требуются, а не браузер.
    Местами как-то странно используется JS (например, чтобы поместить элемент в начало массива можно использовать unshift).

    Более подробно не смотрел.
    • 0
      Об этом, я, конечно, подумал, прежде чем дать именно такой код. Подумал вот что: не стоит переусердствовать в применении принципов (что сам по себе тоже принцип). Если код используется на данной странице и только на ней — что вы пишете? Глобальную переменную, не задумываясь о совместимости. Библиотеки и переносимые процедуры уже сами подумали о том, как уйти из [[Global Scope]]. Сделал это и я в объекте GameLife, но зачем переусердствовать в функциях на кнопках, если они только на этой странице? Поэтому всякие doXxxx..() — в глобальных переменных.

      isIE — согласен, это по привычке, оно интуитивно понятно. Это как бы говорит: «а вот для IE поступим иначе». функция g() — это тоже, лёгкая замена $(). Есть в коде более серьёзный недостаток — функция initGL1(), на неё «не хватило сил» подойти объектно.
      • +2
        isIE — согласен, это по привычке, оно интуитивно понятно. Это как бы говорит: «а вот для IE поступим иначе»
        Ну вот ровно вот это — плохой подход. Думаю, все хаки, которые вы использовали, уже не нужны в IE9, а он скоро выйдет.
      • +1
        При моём весьма положительном отношении к Вам лично я совершенно согласен с shpaker, bolk и razon.

        Ваш код далёк от идеального и перечислять можно очень долго. Ошибки есть по всем ступеням — идеология кода, html-верстка, кучу ненужных кусков, ошибочных кусков, непонятных кусков, огромное количество комментариев.
        Идеология ООП не в том, чтобы вынести всё классы. ООП и классы это всё-равно, что война и танки. Можно сделать войну без танков, а танки могут быть и без войны.
        Если хотите написать такой код, который можно было бы называть примером для новичков следует очень много исправить.
        А теперь — малая толика ошибок из разных областей, которые я заметил беглым проглядыванием.

        Проблемы с html-кодом:

        Многословный заголовок страницы. Стандарт html5 позволяет и рекомендует намного более изящную запись, не соблюдены отступы:
        <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml">
        <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        

        Заменяем на
        <!DOCTYPE html>
        <html>
        	<head>
        		<meta charset="utf-8" />
        


        Кучу нарушений идеи ненавязчивого JavaScript.

        Кое-где забыли скобки:
        <div id=dGL></div>
        


        Фактически, хотя вы и объявили доктайп xHTML, но у вас "6 Errors, 22 warning(s)". А еще кучу семантических и стилистических ошибок.

        Проблемы с javascript-кодом:

        Тут можно критиковать почти каждую строчку. Я начну с топика.

        Давайте даже такую простую задачу оформим в лучших традициях ООП

        Это далеко не лучшие традиции ООП и такой стиль никак не рекомендуемый для новичка.. Ниже аргументирую почему.

        Ниже будет скрипт с теми самыми перегруженными методами, которые, если бы применялись в наследовании, дали бы тот самый не рекомендуемый в последней статье эффект размножения функций и траты памяти.

        Нет, эффект размножения есть даже без наследования — а при обычном создании экземпляра класса. Но в данном случае создаётся не так много экземпляров, потому оно не играет критической роли. Другое дело, что вы утверждаете, что неокрепшие умы сочтут это хорошим стилем и правильной реализацией. Потому даже(тем-более!) в маленьких демонстративных примерах нельзя использовать такой способ.
        Тем более, что из-за его использования у вас некрасиво смешались две области видимости, а в купе с однобуквенным наименованием переменных сделало код совершенно непонятным.

        if(conf.w ==null) conf.w = 402; //параметры, не будут жить как замыкания
        if(conf.h ==null) conf.h = 321;
        if(conf.o ==null) conf.o = g('dbGL1');
        

        Зачем вы меняете содержимое объекта, который передается аргументом? А если мне он будет необходим дальше и именно с значениями null? А вы взяли и поменяли мнее их.

        Вы совершенно не соблюдаете стиль.
        Почему this.cvs и this.gen объявлены с this, а w и h — с var?

        Фигурные скобки совершенно не имеют закономерности расстановки. Кое-где они стоят в одну линию с условием, кое-где в следующей, кое-где развернутые, кое-где отсутствуют. Бардак, в общем.

        Названия переменных — это просто кошмар. Однобуквенный хаос, который понятен только автору. При просмотре кода возникает впечатление, что он пропущен через обфускатор.

        То, что вы сделали с условиями в this.show — это не оптимизация, а кромешный ужас. Их надо было схлопнуть и ни в коем случае не оптимизировать. Тормозит явно не банальный «иф». Вы, блин, arc в цикле делаете, и при этом выносите if, увеличивая код в 3-4 раза! А полученный результат от этого — ускорение в 0.01%. Это если повезет.
        Если бы вы написали нормальный код, который потом пропрофилировали, то поняли, что смысла выносить ифы нету совершенно.

        Ошибка в плане скорости у вас не в месторасположении условий, а в архитектуре. Я сейчас делаю сверхтяжёлый код, с попиксельной отрисовкой канвы размером 480*225, кучей матана, риалтайм-ресайзом изображений и т.п., который, при этом, умудряется более-менее выдавать 20-30 фпс. Но там куда больше вычислений чем в вашем коде. В реализации «жизни» всё должно просто летать.

        if(!isIE)
        	o.querySelector('.gen #' + oId).innerHTML = val;
        else
        	g(oId).innerHTML = val;
        

        Что это за кромешный ужас? oId впонле достаточно для уникальной идентификации элемента, зачем замедлять получение элемента, указывая проверку на принадлежность к классу?

        Что случится в функции this.initCvs, если отсутствует метод querySelector?

        Почему вы не закешировали результат document.getElementsByName('rule') в this.checkRule

        Почему кое-где ссылки на dom-объекты передаются в GameLife аргументами, а кое-где — берутся из «Магических строк» прям внутри метода

        Ад внутри условия if(mthd =='pre'){ остался для меня сущей загадкой. А перечитал его раз 5, но так и не понял, зачем он нужен. Между прочим, тут видно один из недостатков перегруженного конструктора — непонятно к какой области видимости относится какая переменная. Долго искал, где взялась o

        Вообще названия переменных — приводят в ужас. Перечислю те, что есть в области видимости this.show
        mthd, o, s, q, l, pi2, z2, z21, z3, z6, grid, rect, x, y
        conf, o, GL, w, h, z, a, iStop, iStart, okr

        И это только те, которые в локальной области видимости. При этом z21 = z/2.3 (где логика?), q — канвас-контекст, а okr — сокращение от русского слова «окружение».

        Этот код не то что далёк от ООП — он вообще далек от хорошего кода. И то, что вы используете методы не делает его ООП.

        Без слёз на него смотреть тяжело. И это и есть пример того самого ада, в который можно попасть при поддержке проектов на javascript.

        Заключение

        Если ничего не помешает, то, под влиянием от вашей статьи, я таки опубликую вариант «жизнь» на javascript best-practice кодом и подробным объяснением.

        Если нужна помощь, можете написать мне в личку, обсудим.

  • –1
    спойлеров на хабре нет
    Спойлер — это информация. То, что вы называете спойлером, можно назвать «катом» (cut), например.
    • +1
      Спо́йлер, англ. to spoil — «гадить», «отравлять», «портить». Я вам скажу хорошую вещь спойлером не назовут! А вот то шо вы назвали «катом» у хороших людей называется «обрезанием».
      А если серьезно — автор прав, спойлеров порой ой как не хватает…
      • +1
        Автор не прав в том, что не эта вещь называется спойлером. Именно об этом мой комментарий.
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Дни «Жизни» на Хабре! :)
  • +2
  • 0
    Сколько стало интересных статей на Хабре.
  • +2
    Вы лучше бы чем в пятый раз объяснять правила жизни на хабре, алгоритмы свои поописывали, а то лично мне такие строки:
    GL.cvs.width = GL.cvs.width;

    или такие условия
    if(!GL.rect){
    if(...){

    }else{

    }else{

    }else{
    ...}
    }

    Как-то по индуски )) Без обид конечно код работает нормально, но вот исходники иногда заставляют улыбаться ))
    • 0
      Привет! Спасибо.
      Ради алгоритмов и их обсуждения как раз было написано. И всему есть своё объяснение. (Думаю, каждый найдёт образцы подобного кода у себя.)

      > GL.cvs.width = GL.cvs.width;
      Почитайте (согласен, тут мне надо было написать комментарий) документацию к canvas, и будет ясно, что это далеко не бессмысленная строчка — это способ очистки полотна (!).

      > canvas.width = canvas.width; // clears the canvas


      Более явного способа очистки почему-то не наблюдается. Было бы лучше, конечно — что-то типа canvas.clear();.

      Условия (при выводе канваса) — тоже очень нужная в данном случае вещь (это мне показалось очевидным, не стал комментировать в коде). Если бы я слепил их все в один цикл, а условия загнал внутрь, то цикл медленнее бы работал. Здесь — компромисс объёма кода и скорости выполнения.
      • 0
        Вообще-то существует clearRect()
      • 0
        canvas.width работает как минимум в 10 раз медленней, чем clearRect()
  • 0
    вот оно как
    Я просто на канве обычно всё перерисовываю и поэтому как-то не задумывался о очистке её вообще, хотя можно было бы clearRect, но да такой как у вас способ оправданней наверное
  • 0
    Добавил в работающий код (и в статью) вариацию правил — Day & Night. Описана в ссылках [7] в Википедии (англ.). Довольно интересное поведение.
  • 0
    у меня кстати тоже есть Жизнь на канвасе писанная, я как разгребусь с делами выложу. Я там правда немного отсебятины в правила вносил ))
    • 0
      Если выложите как статью — предупредите и меня заранее, я ко времени её выкладки сделаю спрайты, чтобы оценить скорость выполлнения. Когда несколько новостей по одной теме, читается интереснее.
  • +3
    Давайте даже такую простую задачу оформим в лучших традициях ООП

    Посмотрел код. Позвольте спросить, где в нем ООП?

    Про кучу дублирования я вообще молчу
    • 0
      Про дублирование там, где надо максимально ускорить циклы, я уже писал в комментариях. ООП — в определении объекта — игрового поля и методов работы с ним. Если у Вас возникли более конструктивные предложения — пожалуйста, покажите, как надо делать. Иначе это не критика, а разговор ни о чём. С уважением…
  • 0
    Вместо того, чтобы издеваться над массивом(с постоянным перемещением в начало) можно было бы сделать два массива, которые менять местами. Один используется как источник для наполнения данными второго.
    Но медлительность убивает. Все должно работать намного быстрее при должном подходе.
    • 0
      Шок в шоке ))

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