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

В одно прекрасное воскресенье, потягивая пивко и думая о жизни, я вдруг подумал: а можно ли в один твит уместить реализацию Игры Жизни на JavaScript? И он не мог не попробовать.

это не настольная игра

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

Настольная игра The Game of Life, издание 1991 года (источник: amazon.com)

Настольная игра «Игра жизни», издание 1991 года (источник: amazon.com)

С большой долей вероятности вам это покажется достаточно сложным, и вы подумаете — какого хрена я вообще пытаюсь впихнуть всю логику этой игры в 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)

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

ЧИТАТЬ   17 известных женщин, которые пробовали себя на подиуме и почти превзошли моделей
Код запускается в Node.js и делает то, что ему говорят

Код запускается в Node.js и делает то, что ему говорят

Итак, позвольте мне кратко объяснить, что здесь происходит. я создал функцию 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).

Скриншот поста на Mastodon с кодом игры

Скриншот поста на Mastodon с кодом игры

Вы можете увидеть само сообщение. по этой ссылке.

((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, заполненный пустыми значениями, но обратный вызов может их заменить.

ЧИТАТЬ   Более или менее $ 555 в месяц. Посетительница рассказала, сколько стоит жить на Шри-Ланке

Потому что и 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.

ЧИТАТЬ   Точки - Это случается в жизни (XM Remix)

На самом деле нет необходимости передавать размер как два отдельных аргумента — это может быть один аргумент, используемый как для 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)
Скрипт всего 280 символов и он работает!

Скрипт всего 280 символов и он работает!

Твиттер

А вот и твит

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

Source

От admin