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

С большой долей вероятности вам это покажется достаточно сложным, и вы подумаете — какого хрена я вообще пытаюсь впихнуть всю логику этой игры в 280 символов кода? ТАК. Это не та игра жизни, которую вы ищете.
Игра “Жизнь” Джона Конвея (Game Of Life) – вот обо всем и пойдет речь в этой статье. Все действие происходит на двухмерном поле с ячейками. Каждая клетка может быть мертвой или живой. Состояние клетки может меняться после каждого хода в зависимости от состояния ее соседей (соседние клетки по горизонтали, вертикали или диагонали):
-
живая клетка останется живой на следующий ход, если у нее есть два или три живых соседа, иначе она погибнет;
-
мертвая клетка становится живой на следующий ход, если у нее ровно три живых соседа, в противном случае она остается мертвой.

Вот собственно и все. Для тех, кто хочет узнать больше об игре, есть статья в Википедия.
Отправная точка
Что я имею в виду, когда говорю о реализации JavaScript в Game of Life? Конечно, я мог бы просто написать базовую функцию, которая берет текущее состояние игры, выполняет некоторые магические действия и возвращает состояние для следующего раунда. Это легко впишется в твит. Но мне хотелось добиться чего-то более сложного и самостоятельного. На мой взгляд, код должен был генерировать начальное (случайное) состояние игры, запускать игру в бесконечном цикле и давать визуальное представление каждого хода.
Я сел перед своим ноутбуком и начал писать код. Через несколько минут у меня была работающая реализация JavaScript, которая делала именно то, что я хотел.
function gameOfLife(sizeX, sizeY) {
let state = [];
for (let y = 0; y < sizeY; y++) {
state.push([])
for (let x = 0; x < sizeX; x++) {
const alive = !!(Math.random() < 0.5);
state[y].push(alive)
}
}
setInterval(() => {
console.clear()
const consoleOutput = state.map(row => {
return row.map(cell => cell ? 'X' : ' ').join('')
}).join('\n')
console.log(consoleOutput)
const newState = []
for (let y = 0; y < sizeY; y++) {
newState.push([])
for (let x = 0; x < sizeX; x++) {
let aliveNeighbours = 0
for (let ny = y - 1; ny <= y + 1; ny++) {
if (state[ny]) {
for (let nx = x - 1; nx <= x + 1; nx++) {
if (!(nx === x && ny === y) && state[ny][nx]) {
aliveNeighbours++
}
}
}
}
if (state[y][x] && (aliveNeighbours < 2 || aliveNeighbours > 3)) {
newState[y].push(false)
} else if (!state[y][x] && aliveNeighbours === 3) {
newState[y].push(true)
} else {
newState[y].push(state[y][x])
}
}
}
state = newState
}, 1000)
}
gameOfLife(20, 20)
Не могли бы вы написать лучший код? Я так думаю. Но у меня не было цели с первой попытки добиться идеала. Я не ставил перед собой задачу написать идеальную реализацию игры с точки зрения кода. Все, что мне было нужно, — это отправная точка, какой-то основной код, который я хотел максимально сократить и сжать.

Итак, позвольте мне кратко объяснить, что здесь происходит. я создал функцию gameOfLife
который принимает два аргумента: sizeX
И sizeY
. Они используются для создания двумерного массива state
который заполняется случайными булевыми значениями (это делается во вложенных циклах for
. True
означает, что клетка жива, false
– умер).
Затем с помощью setInterval
Каждую секунду выполняется анонимная функция. Он очищает текущий вывод консоли и генерирует новый вывод на основе текущего состояния. В этом выводе символ X
живые клетки обозначаются, а мертвые клетки обозначаются пробелом.
Затем другой набор вложенных циклов for создает новое состояние (newState
). Для каждой клетки (представленной координатами x, y) функция проверяет всех возможных соседей (от x-1 до x+1 и от y-1 до y+1) и подсчитывает количество живых (aliveNeighbours
). Для страховки из цикла исключается текущая ячейка, а также несуществующие соседи (например, x=-1, y=-1). На основании информации о количестве живых соседей устанавливается новое состояние ячейки. Перезапись конечного состояния newState
.
Наконец функция вызывается gameOfLife
с параметрами 20 строк по 20 столбцов. Вот и все.
Цель
Если да, то я объясню. Под твитом я подразумеваю пост в Твиттере (социальная сеть с птичкой), который ограничен 280 символами.
Здесь мне нужно поместить свой код. Конечно, отступы и длинные имена переменных не облегчают задачу, поэтому я оставлю их в исходном коде для удобочитаемости, а затем воспользуюсь uglify-js убрать лишние пробелы/строчки и сократить имена переменных (будет проще решить задачу с односимвольными длинными именами).
После просмотра uglifier я получил исходный код из 549 символов. Чтобы втиснуть его в твит, мне пришлось бы сократить его почти пополам.
function gameOfLife(t,f){let s=[];for(let o=0;o<f;o++){s.push([]);for(let e=0;e<t;e++){const l=!!(Math.random()<.5);s[o].push(l)}}setInterval(()=>{console.clear();const e=s.map(e=>{return e.map(e=>e?"X":" ").join("")}).join("\n");console.log(e);const o=[];for(let l=0;l<f;l++){o.push([]);for(let f=0;f<t;f++){let t=0;for(let o=l-1;o<=l+1;o++){if(s[o]){for(let e=f-1;e<=f+1;e++){if(!(e===f&&o===l)&&s[o][e]){t++}}}}if(s[l][f]&&(t<2||t>3)){o[l].push(false)}else if(!s[l][f]&&t===3){o[l].push(true)}else{o[l].push(s[l][f])}}}s=o},1e3)}gameOfLife(20,20);
Рефакторинг
Итак, требования сформулированы, время терять нельзя — приступаем к сокращению кода!
Декларации
Во-первых, нет необходимости сначала объявлять именованную функцию, а затем вызывать ее. Я могу преобразовать его в функцию автоматического вызова, например ((sizeX, sizeY) => {...})(20, 20)
– этого вполне достаточно, да и места займет меньше.
Следующий пункт касается объявлений переменных. В настоящее время я определяю переменные, когда они мне нужны, но это приводит к множественным вхождениям в код. let
И const
(слова до 5 символов!). Давайте просто использовать старый добрыйvar
‘ и объявить все переменные в начале функции.
((sizeX, sizeY) => {
var state = [],
y, x, consoleOutput, ny, nx, aliveNeighbours, newState;
...
})(20, 20)
Теперь позвольте uglify-js сделать свою работу, и… мы получим 499 символов! Это все еще далеко от предела Twitter, но этого достаточно для Mastodon (еще одной социальной сети, конкурирующей с Twitter).

Вы можете увидеть само сообщение. по этой ссылке.
((o,e)=>{var r=[],s,f,n,a,l,p,u;for(s=0;s<e;s++){r.push([]);for(f=0;f<o;f++){const h=!!(Math.random()<.5);r[s].push(h)}}setInterval(()=>{console.clear();n=r.map(o=>{return o.map(o=>o?"X":" ").join("")}).join("\n");console.log(n);u=[];for(s=0;s<e;s++){u.push([]);for(f=0;f<o;f++){p=0;for(a=s-1;a<=s+1;a++){if(r[a]){for(l=f-1;l<=f+1;l++){if(!(l===f&&a===s)&&r[a][l]){p++}}}}if(r[s][f]&&(p<2||p>3)){u[s].push(false)}else if(!r[s][f]&&p===3){u[s].push(true)}else{u[s].push(r[s][f])}}}r=u},1e3)})(20,20);
Комбинирование генерации начального состояния
Использование вложенных циклов for для установки начального состояния работает достаточно хорошо, но можно сделать еще лучше. Например, используйте метод Array.from
.
var state = Array.from(Array(sizeY), () => Array.from(Array(sizeX), () => Math.random() < .5 ? 'X' : ' ' ))
Array.from
принимает два аргумента. Первый является обязательным и представляет собой итерируемый объект, который будет преобразован в массив. Второй, необязательный, является напоминанием. Значение, возвращаемое обратным вызовом, помещается в выходной массив. Array(n)
возвращает массив длины n, заполненный пустыми значениями, но обратный вызов может их заменить.
Потому что и Array.from
я Array
используются дважды, я могу сэкономить место с переменными.
var array = Array,
arrayFrom = array.from,
state = arrayFrom(array(sizeY), () => arrayFrom(array(sizeX), () => Math.random() < .5 ? 'X' : ' ' )),
Сейчас это может быть не видно, но как только имена переменных будут искажены uglifier, код станет на несколько символов короче.
Вы могли заметить, что я больше не использую логические значения. Поскольку мне нужны символы X и пробел для вывода на консоль, их также проще использовать в состоянии. Благодаря этому код обработки консоли можно сократить:
console.clear()
console.log(state.map(row => row.join('')).join('\n'))
Как видите, я избавился от переменной consoleOutput
. Кроме того, чтобы не загромождать место, я могу поместить консоль в переменную, так как она используется в коде дважды:
...
conzole = console,
...
conzole.clear()
conzole.log(...)
Еще несколько мелких настроек (из-за использования X и пробела вместо логических значений) и минимизированный код составляет… 448 символов. Осталось меньше 200.
((r,o)=>{var e=Array,f=e.from,s=console,a=f(e(o),()=>f(e(r),()=>Math.random()<.5?"X":" ")),i,l,n,h,p,m,t;setInterval(()=>{s.clear();s.log(a.map(r=>r.join("")).join("\n"));t=[];for(i=0;i<o;i++){t.push([]);for(l=0;l<r;l++){m=0;for(n=i-1;n<=i+1;n++){if(a[n]){for(h=l-1;h<=l+1;h++){if(!(h===l&&n===i)&&a[n][h]==="X"){m++}}}}p=a[i][l].trim();if(p&&(m<2||m>3)){t[i].push(" ")}else if(!p&&m===3){t[i].push("X")}else{t[i].push(a[i][l])}}}a=t},1e3)})(20,20);
Переход в новое состояние
С самого начала мне не очень понравилась моя реализация newState
. Я сторонник использования методов массива при работе с ними, поэтому я решил применить reduce
и уменьшить количество циклов for
. Кроме того, я присвоил новым переменным флаги состояния (символы X/пробел). Кроме того, назначение нового состояния ячейки теперь выполняется более эффективно. Последним улучшением в этой итерации является замена тройного знака равенства (===) двойным знаком равенства (==) для сравнений.
...
alive="X",
dead = ' '
...
setInterval(() => {
...
state = state.map((row, y) => row.reduce((newRow, cell, x) => {
aliveNeighbours = 0
for (ny = y - 1; ny <= y + 1; ny++) {
for (nx = x - 1; nx <= x + 1; nx++) {
if (!(nx == x && ny == y) && state[ny]?.[nx] == alive) aliveNeighbours++
}
}
newRow.push(cell.trim()
? [2,3].includes(aliveNeighbours) ? alive : dead
: aliveNeighbours == 3 ? alive : dead
)
return newRow
}, []))
}, 1000)
После всех этих манипуляций у меня получилось 367 символов (естественно, в минифицированном виде). Неплохой результат, но все же недостаточно большой для Twitter.
Цените то, что у вас уже есть
Как я уже сказал, я большой поклонник методов массива (особенно reduce
). Однако здесь я активно использую и map
я reduce
, и имена этих методов занимают много места. Выше я уже подал заявку Array.from
и поместил его в переменную, и после еще нескольких минут изучения кода я понял, что могу использовать это вместо этого map
И reduce
следующее :
state = arrayFrom(state, (row, y) => arrayFrom(row, (cell, x) => {
aliveNeighbours = 0
for (ny = y - 1; ny <= y + 1; ny++) {
for (nx = x - 1; nx <= x + 1; nx++) {
if (!(nx == x && ny == y) && state[ny]?.[nx] == alive) aliveNeighbours++
}
}
return cell.trim()
? [2,3].includes(aliveNeighbours) ? alive : dead
: aliveNeighbours == 3 ? alive : dead
)
}))
Также мне все еще не нравился код, определяющий новое состояние каждой ячейки (хотя он работал нормально), поэтому через некоторое время я пришел к такому решению:
return aliveNeighbours == 3
? alive
: aliveNeighbours == 2 ? cell : dead
После минификации у меня получился код длиной 321 символ.
((r,o)=>{var a=Array,n=a.from,e=console,f="X",l=" ",t=n(a(o),()=>n(a(r),()=>Math.random()<.5?f:l)),i,m,c;setInterval(()=>{e.clear();e.log(t.map(r=>r.join("")).join("\n"));t=n(t,(r,a)=>n(r,(r,o)=>{c=0;for(i=a-1;i<=a+1;i++){for(m=o-1;m<=o+1;m++){if(!(m==o&&i==a)&&t[i]?.[m]==f)c++}}return c===3?f:c===2?r:l}))},1e3)})(20,20);
Погрузитесь еще глубже
Что ж, теперь я дошел до того, что 40 символов начали казаться мне целой книгой. Что еще можно сократить и упростить? Следуя практике повторного использования существующих инструментов (Array.from
), я могу переписать этот отрывок:
conzole.log(state.map(row => row.join('')).join('\n'))
следующее :
conzole.log(arrayFrom(state, (row) => row.join('')).join('\n'))
Конечно, в неминифицированном виде этот код длиннее исходного. Однако после минификации он сократился до 319 символов, а я сэкономил до 2 символов, что, мягко говоря, не так уж и много. Осталось еще 38.
На самом деле нет необходимости передавать размер как два отдельных аргумента — это может быть один аргумент, используемый как для x, так и для y. И вообще, вместо 20 я могу использовать 9, что уменьшит значение аргумента на один символ. Так сколько же мы выиграли? Минифицированный код из 311 символов.
И после? Допустим, я могу использовать числа — 0 для мертвой клетки и 1 для живой клетки. Мы получаем один символ вместо громоздкого трехсимвольного представления (0 вместо ‘ ‘ и 1 вместо ‘X’). А поскольку это всего лишь один символ, мне не нужно хранить его в отдельной переменной. 299 символов. Победа близка.
Теперь, используя числа в качестве флагов состояния, я могу немного изменить логику, отвечающую за подсчет числа. aliveNeightbours
:
...
for (ny = y - 1; ny < y + 2; ny++) {
for (nx = x - 1; nx < x + 2; nx++) {
if (state[ny]?.[nx] == 1) aliveNeighbours++
}
}
return aliveNeighbours - cell == 3
? 1
: aliveNeighbours - cell == 2 ? cell : 0
Я больше не проверяю, совпадают ли координаты потенциального соседа с ячейкой, для которой я считаю живых соседей. Вместо этого я вычитаю значение этой ячейки из суммы. Также я заменил nx <= x + 1
на nx < x + 2
(то же самое для y) – результат тот же, но на один символ короче. 286 символов. Больше 6!
Я посмотрел на код, сгенерированный uglify-js, и понял, что он сохраняет фигурные скобки для циклов. for - for (...){for(...){...}}
. Но можно их удалить и написать все одной строкой:
for (ny = y - 1; ny < y + 2; ny++) for (nx = x - 1; nx < x + 2; nx++) state[ny]?.[nx] == 1 && aliveNeighbours++
Давайте пропустим это через uglify-js и…
Окончательно
Ровно 280 символов. Ну, технически 281 символ, но uglify-js добавляет в конце точку с запятой, которая мне не особо нужна.
Вот окончательный код:
((size) => {
var array = Array,
arrayFrom = array.from,
conzole = console,
state = arrayFrom(array(size), () => arrayFrom(array(size), () => Math.random() < .5 ? 1 : 0 )),
ny, nx, aliveNeighbours;
setInterval(() => {
conzole.clear()
conzole.log(arrayFrom(state, (row) => row.join('')).join('\n'))
state = arrayFrom(state, (row, y) => arrayFrom(row, (cell, x) => {
aliveNeighbours = 0
for (ny = y - 1; ny < y + 2; ny++) for (nx = x - 1; nx < x + 2; nx++) state[ny]?.[nx] == 1 && aliveNeighbours++
return aliveNeighbours - cell == 3
? 1
: aliveNeighbours - cell == 2 ? cell : 0
}))
}, 1000)
})(9)

Твиттер

Ожидайте обратную связь — я уверен, вы найдете способ «вырезать» несколько лишних символов. Вы можете даже лучше, чем я!