RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1332881
Accepted
aepot
aepot
Asked:2022-09-28 16:15:12 +0000 UTC2022-09-28 16:15:12 +0000 UTC 2022-09-28 16:15:12 +0000 UTC

优化计算球场2048的方法

  • 772

我坐下来思考如何摆脱重复的代码。

2048你知道这个游戏,我正在编写我自己的实现版本。有一个瓷砖数组,每个瓷砖是

const cell = {
  row : 1,
  col : 1,
  value : 32,
  merged : false,
  node : <HTMLElement>
}

从逻辑上讲,有一个 4x4 瓷砖的运动场。

任务:当玩家移动时,你需要穿过整个场地,并沿着正确的方向移动瓷砖,将具有相同价值的瓷砖合并。

这段代码有效,但看起来很糟糕。它仅使用一组瓦片来实现移动和合并瓦片的逻辑。从技术上讲,DOM 是用另一种方法更新的,瓦片的实际合并也发生在后面,这里只在瓦片上放了一个标签,它将被合并。要删除的图块只需设置为 0。

const size = 4;

function move(direction) {
  let moved = false;
  if (direction === 'left') {
    for (let col = 1; col < size; col++) {
      for (let row = 0; row < size; row++) {
        const cell = getCell(row, col);
        if (cell && !cell.merged) {
          let targetCol;
          for (targetCol = col - 1; targetCol >= 0; targetCol--) {
            const targetCell = getCell(row, targetCol);
            if (!targetCell)
              continue;
            if (targetCell.value == cell.value && !targetCell.merged) {
              cell.value = 0;
              targetCell.merged = true;
            }
            break;
          }
          if (cell.value != 0)
            targetCol++;
          if (col != targetCol) {
            moveCol(cell, targetCol);
            moved = true;
          }
        }
      }
    }
  }
  if (direction === 'right') {
    for (let col = size - 2; col >= 0; col--) {
      for (let row = 0; row < size; row++) {
        const cell = getCell(row, col);
        if (cell && !cell.merged) {
          let targetCol;
          for (targetCol = col + 1; targetCol < size; targetCol++) {
            const targetCell = getCell(row, targetCol);
            if (!targetCell)
              continue;
            if (targetCell.value == cell.value && !targetCell.merged) {
              cell.value = 0;
              targetCell.merged = true;
            }
            break;
          }
          if (cell.value != 0)
            targetCol--;
          if (col != targetCol) {
            moveCol(cell, targetCol);
            moved = true;
          }
        }
      }
    }
  }
  if (direction === 'up') {
    for (let row = 1; row < size; row++) {
      for (let col = 0; col < size; col++) {
        const cell = getCell(row, col);
        if (cell && !cell.merged) {
          let targetRow;
          for (targetRow = row - 1; targetRow >= 0; targetRow--) {
            const targetCell = getCell(targetRow, col);
            if (!targetCell)
              continue;
            if (targetCell.value == cell.value && !targetCell.merged) {
              cell.value = 0;
              targetCell.merged = true;
            }
            break;
          }
          if (cell.value != 0)
            targetRow++;
          if (row != targetRow) {
            moveRow(cell, targetRow);
            moved = true;
          }
        }
      }
    }
  }
  if (direction === 'down') {
    for (let row = size - 2; row >= 0; row--) {
      for (let col = 0; col < size; col++) {
        const cell = getCell(row, col);
        if (cell && !cell.merged) {
          let targetRow;
          for (targetRow = row + 1; targetRow < size; targetRow++) {
            const targetCell = getCell(targetRow, col);
            if (!targetCell)
              continue;
            if (targetCell.value == cell.value && !targetCell.merged) {
              cell.value = 0;
              targetCell.merged = true;
            }
            break;
          }
          if (cell.value != 0)
            targetRow--;
          if (row != targetRow) {
            moveRow(cell, targetRow);
            moved = true;
          }
        }
      }
    }
  }
  return moved;
}

我找不到优化额头的方法。也许你需要改变算法,帮我弄清楚。

算法,例如,用于向左(左)移动:

  • 我从0开始逐列查看,也就是从左到右
  • 如果我找到一个瓷砖,我会检查左边是否有瓷砖
  • 如果有,检查它的值
  • 如果它匹配并且瓷砖之前没有被冻结,那么我合并
  • 我按照上面的条件移动瓦片,也就是要么往合并,要么往左边最远的空闲单元格移动,如果有的话
  • 继续下一行

上面代码中调用的方法,供参考

const cells = [];
const cellSize = 100 / size;

function moveRow(cell, row) {
  cell.row = row;
  cell.node.style.top = (row * cellSize) + '%';  
}

function moveCol(cell, col) {
  cell.col = col;
  cell.node.style.left = (col * cellSize) + '%';
}

function getCell(row, col) {
  for (let i = 0; i < cells.length; i++) {
    const cell = cells[i];
    if (cell.row == row && cell.col == col && cell.value != 0)
      return cell;
  }
}
javascript
  • 5 5 个回答
  • 10 Views

5 个回答

  • Voted
  1. IvaMuxa
    2022-05-07T01:08:31Z2022-05-07T01:08:31Z

    let cvalue = [[0,2,16,2],[0,0,8,0],[4,0,2,2],[4,8,16,0]];
    
    let placeHolder = document.getElementById("main");
    
    for(let i =0; i<16; i++){
        let cell = document.createElement("div");
      cell.className = "cell";
      cell.innerText = cvalue[(i-i%4)/4][i%4]||"";
      placeHolder.appendChild(cell); 
    }
    
    function update(){
        for(let i =0; i<16; i++){
            placeHolder.children[i].innerText = cvalue[(i-i%4)/4][i%4]||"";
        }
    }
    
    document.getElementById("left").addEventListener("click",()=>{move({x:0,y:1})});
    document.getElementById("right").addEventListener("click",()=>{move({x:0,y:-1})});
    document.getElementById("up").addEventListener("click",()=>{move({x:1,y:0})});
    document.getElementById("down").addEventListener("click",()=>{move({x:-1,y:0})});
    
    function move(direction){
      //в зависимости от нажатой кнопки передаем направление движения 
      //по осям и какой ряд вертикально или горизонтально не важно их всегда 4
        for(let r=0; r<4; r++){ //r - порядковый номер рядя или столбца
            row(r,direction);
        }
      update(); // ну и обновим
    }
    
    function row(row, direction){
        let lastCell = 0; // тут будем хранить ячейку в которую что то положим
        let x,y;
      
      // пройдём по элементам ряда или столбца их у нас всегда четыре
        for( let pos = 0; pos < 4; pos++ ){
        if(direction.x == 0 ){
            // если направление по х нулевое значит перемещаемя горизонтально по ряду row 
            x = row;
            y = (direction.y>=0 ? direction.y*pos + 0 : direction.y*pos + 3);
          // позиция по y будет в зависимости от направления direction.y 
          // при положительном значении будет от 0 вверх,
          // а при отрицательном от 3 вниз
        }else{
            // если направление по y нулевое значит перемещаемя вертикально по столбцу row
            y = row; 
            x = (direction.x>=0 ? direction.x*pos : direction.x*pos + 3);
        }
        
        
        if(lastCell == 0){ 
          // если это первая ячейка в нашем ряду или столбце lastCell равно нулю 
          //пока мы не определили туда ничего
            lastCell = {row: x, col: y, val: cvalue[x][y]};
          continue; //переходим к следующей
        }
            if(cvalue[x][y] == 0) { //пустая пропустим
            continue;
        }else if(cvalue[x][y] == lastCell.val){
                  //одинаковые значения перекидываем отсюда в lastCell
            cvalue[lastCell.row][lastCell.col] += cvalue[x][y];
            // эту обнуляем она стала пустая
            cvalue[x][y] = 0;
            //в lastCell уже ложить не может сдвинемся на следующую она пустая в любом случае
            lastCell.row += direction.x;
            lastCell.col += direction.y;
            lastCell.val = 0;
        }else{
            // нельзя мержить значит ложим в соседнюю с lastCell
            if(lastCell.val == 0){ //ячейка пуста просто переносим значение
                                 //и lastCell остается мы же можем в нёё мержить ?
                cvalue[lastCell.row][lastCell.col] = cvalue[x][y];
                  lastCell.val = cvalue[x][y];
                  cvalue[x][y] = 0;
            }else{ // ячейка заполнена и не подходит нужно перейти к следубщей
                lastCell.row += direction.x;
                lastCell.col += direction.y;
                if(lastCell.row == x && lastCell.col == y ) {
                    // это случай когда между ячейками нет свободных 
                    // мы ничего не перемещаем и теперь в lastCell у нас значение ячейки X:Y
                      lastCell.val = cvalue[x][y];
                    continue; //тогда просто идём дальше
                }
                cvalue[lastCell.row][lastCell.col] = cvalue[x][y];
                lastCell.val = cvalue[x][y];
                cvalue[x][y] = 0;
            }
        }
      }
    }
    .buttons{
        position: absolute;
        left: 100px;
        top: 50px;
        display: flex;
        flex-direction: column;
    }
    #main {
        width: 200px;
        border: solid 1px red;
        display: flex;
        flex-wrap: wrap;
        margin: auto;
    }
    .cell{
      width: 46px;
      height: 46px;
      border: solid 1px blue;
      margin: 1px;
      text-align: center;
      font-size: 40px;
    }
    <div class="buttons">
      <button id="left">LEFT</button>
      <button id="right">RIGHT</button>
      <button id="up">UP</button>
      <button id="down">DOWN</button>
    </div>
    
    <div id="main">
    </div>

    • 5
  2. Best Answer
    DiD
    2022-05-07T19:52:58Z2022-05-07T19:52:58Z

    如果你不关心动画,那么最简单的方法就是通过表格。存储长数字也没有多大意义。您只能存储二的幂。

    // Вообще, задача не требует ООП, но пусть будет класс игры
    class Game {
      // Свойство содержит количество колонок
      colsNum;
      // Свойство содержит количество строк
      rowsNum;
      // Свойство содержит степени двоек всех ячеек
      raw;
      // Свойство содержит ссылку на таблицу <table>
      actor;
      // Конструктор
      constructor(props) {
        // Применить свойства к this
        Object.assign(this, props);
      }
      // Метод генерирует таблицу
      init() {
        // Заполняем массив степеней двоек нолями
        this.raw = new Array(this.colsNum * this.rowsNum).fill(0);
        // На всякий отчистим таблицу
        this.actor.innerHTML = '';
        // Перебор строк
        // ri - индекс строки
        // rn - количество строк
        for (let ri = 0, rn = this.rowsNum; ri < rn; ri++) {
          // Создаем <tr>
          let tr = document.createElement('tr');
          // Перебор колонок
          // ci - индекс колонки
          // cn - количество колонок
          for (let ci = 0, cn = this.colsNum; ci < cn; ci++) {
            // Создаем <td> и помещаем в <tr>
            tr.appendChild(document.createElement('td'));
          }
          // Помещаем <tr> в <table>
          this.actor.appendChild(tr);
        }
        // Добавляем две рандомные ячейки для начала игры
        this.addRand();
        this.addRand();
        return this;
      }
      // Метод добавляет рандомное число в рандомную свободную ячейку
      addRand() {
        // Массив свободных ячеек
        let free = [];
        for (let i = 0, l = this.raw.length; i < l; i++) {
          if (!this.raw[i]) {
            free.push(i);
          }
        }
        // Выбираем рандомную ячейку
        const cell = free[~~(Math.random() * free.length)];
        // Выбираем степень новой двойки 1 или 2
        const value = ~~(Math.random() * 2) + 1;
        // Запишем ко всем остальным ячейкам
        this.raw[cell] = value;
        // Обновляем виды
        this.update();
      }
      // Свойство возвращает массив колонок
      get cols() {
        let cols = [];
        for (let ci = 0, cn = this.colsNum; ci < cn; ci++) {
          let col = [];
          for (let ri = 0, rn = this.rowsNum; ri < rn; ri++) {
            col.push(this.raw[ri * cn + ci]);
          }
          cols.push(col);
        }
        return cols;
      }
      // Свойство принимает массив колонок и записывает ко всем степеням двоек
      set cols(v) {
        for (let ci = 0, cn = this.colsNum; ci < cn; ci++) {
          for (let ri = 0, rn = this.rowsNum; ri < rn; ri++) {
            this.raw[ri * cn + ci] = v[ci][ri];
          }
        }
      }
      // Свойство возвращает массив строк
      get rows() {
        let rows = [];
        for (let ri = 0, rn = this.rowsNum; ri < rn; ri++) {
          let row = [];
          for (let ci = 0, cn = this.colsNum; ci < cn; ci++) {
            row.push(this.raw[ri * cn + ci]);
          }
          rows.push(row);
        }
        return rows;
      }
      // Свойство принимает массив строк и записывает ко всем степеням двоек
      set rows(v) {
        for (let ri = 0, rn = this.rowsNum; ri < rn; ri++) {
          for (let ci = 0, cn = this.colsNum; ci < cn; ci++) {
            this.raw[ri * cn + ci] = v[ri][ci];
          }
        }
      }
      // Метод управления игрой
      // dir - направление
      // u - вверх
      // d - вниз
      // l - влево
      // r - вправо
      swap(dir) {
        let property, method;
        // Можно было бы использовать карту наподобие keyMap как по коду внизу,
        // но для академичности можно воткнуть switch
        switch (dir) {
          case 'u':
            [property, method] = ['cols', 'reverse'];
            break;
          case 'd':
            [property, method] = ['cols', 'forward'];
            break;
          case 'r':
            [property, method] = ['rows', 'forward'];
            break;
          case 'l':
            [property, method] = ['rows', 'reverse'];
            break;
        }
        let lines = this[property];
        // Вызываем метод сдвига
        if (this.sum(lines.map(line => this[method](line)))) {
          // Если что-то сдвинулось, то записываем результат
          this[property] = lines;
          // Добавляем еще 2 или 4 в рандомную ячейку
          this.addRand();
        }
      }
      // Метод делает обновление видов
      update() {
        let tds = this.actor.querySelectorAll('td');
        for (let i = 0, l = this.raw.length; i < l; i++) {
          tds[i].innerHTML = this.raw[i] ? 2 ** this.raw[i] : '';
        }
      }
      // Метод считает сумму аргументов
      sum(...as) {
        return as.flat(Infinity).reduce((a, v) => a + v, 0);
      }
      // Метод сдвигает одну линию к началу
      // raw - одномерный массив степеней двоек
      reverse(raw) {
        // Количество сдвинутых ячеек
        let swapped = 0;
        for (let i = 0; i < raw.length - 1;) {
          if (!raw[i] && this.sum(raw.slice(i + 1, raw.length))) {
            raw.splice(i, 1);
            swapped++;
          } else i++;
        }
        // Заполняем конец массива недостающими нулями
        raw.splice(raw.length, 0, ...new Array(swapped).fill(0));
        // Количество совмещенных ячеек
        let mixed = 0;
        for (let i = 1; i < raw.length; i++) {
          if (raw[i - 1] && raw[i - 1] == raw[i]) {
            raw.splice(i - 1, 2, raw[i] + 1);
            mixed++;
          }
        }
        // Заполняем конец массива недостающими нулями
        raw.splice(raw.length, 0, ...new Array(mixed).fill(0));
        return swapped + mixed;
      }
      // Метод сдвигает одну линию к концу
      // raw - одномерный массив степеней двоек
      forward(raw) {
        // Количество сдвинутых ячеек
        let swapped = 0;
        for (let i = raw.length - 1; i >= 0; i--) {
          if (!raw[i] && this.sum(raw.slice(0, i))) {
            raw.splice(i, 1);
            swapped++;
          }
        }
        // Заполняем начало массива недостающими нулями
        raw.splice(0, 0, ...new Array(swapped).fill(0));
        // Количество совмещенных ячеек
        let mixed = 0;
        for (let i = raw.length - 2; i >= 0; i--) {
          if (raw[i] && raw[i] == raw[i + 1]) {
            raw.splice(i, 2, raw[i] + 1);
            mixed++;
            i--;
          }
        }
        // Заполняем начало массива недостающими нулями
        raw.splice(0, 0, ...new Array(mixed).fill(0));
        return swapped + mixed;
      }
    }
    
    // Инициализация игры
    const game = new Game({
      // Ссылка на готовый элемент <table> уже присутствующий на странице
      actor: document.querySelector('table'),
      // Количество колонок
      colsNum: 4,
      // Количество строк
      rowsNum: 4,
    }).init();
    
    // Клавишная карта.
    let keyMap = new Map([
      ['ArrowUp', 'u'],
      ['ArrowDown', 'd'],
      ['ArrowLeft', 'l'],
      ['ArrowRight', 'r'],
      ['KeyW', 'u'],
      ['KeyS', 'd'],
      ['KeyA', 'l'],
      ['KeyD', 'r'],
      ['KeyK', 'u'],
      ['KeyJ', 'd'],
      ['KeyH', 'l'],
      ['KeyL', 'r'],
    ]);
    // Обработчик нажатий клавиш
    window.onkeydown = e => {
      // Специально для SO, чтобы страница не дергалась если жать на стрелочки
      e.preventDefault();
      e.stopPropagation();
      // Код нажатой клавиши
      const {code} = e;
      // Если клавиша есть в списке
      if (keyMap.has(code)) {
        // То свопаем игру в правильном направлении
        game.swap(keyMap.get(code));
      }
    };
    /* Это для сопроводительной надписи */
    body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } 
    /* Выравнивание и небольшой отступ  */
    table { float: left; margin-right: 4rem;}
    /* Стили ячейки */
    td {
      width: calc(25vmin - 10px);
      height: calc(25vmin - 10px);
      border: 1px solid #ccc;
      text-align: center;
      font-size: calc(10vmin);
      font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif; }
    /* Это для визуального выделения названий клавиш */
    span { border: 1px solid #000; 
          display:inline-block; 
          height: 1rem; 
          width: 1rem; 
          line-height: 1rem; 
           text-align:center; padding: }
    <!-- Таблица уже присутствует на странице -->
    <table></table>
    
    Управлять игрой можно клавишами: 
    <ul>
      <li><span>⬅</span> <span>⬇</span> <span>⬆</span> <span>⮕</span></li> 
      <li><span>W</span> <span>A</span> <span>S</span> <span>D</span></li>
      <li><span>H</span> <span>J</span> <span>K</span> <span>L</span></li>
    </ul>.

    Проверку на выигрыш или проигрыш не делал.

    Чуть позже мне пришла идея, хранить состояние в одной строке, (один символ - одна фишка), если кодировать в шестнадцатеричной системе. Или если делать большие поля (5х5 или 10х10), то большие числа также можно хранить вплоть до 36-ричной системы.

    Переводы между строками и числами можно делать так:

    let num, str;
    
    num = 30;
    
    str = num.toString(36);
    console.log(`str = ${JSON.stringify(str)};`);
    
    num = parseInt(str, 36);
    console.log(`num = ${JSON.stringify(num)};`);

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

    • 3
  3. Nick
    2022-05-06T20:43:43Z2022-05-06T20:43:43Z

    您可以旋转比赛场地,以便始终从左到右执行移动,然后移动单元格并将场地旋转回来。

    //преобразовываем координаты чтобы сдвиг выполнялся слева направо
    if (direction === 'left') {
        cells.forEach(cell => {
            cell.oldRow = cell.row
            cell.oldCol = cell.col
            cell.col = size - 1 - cell.col
        })
    }
    if (direction === 'right') {
        //ничего
    }
    if (direction === 'up') {
        cells.forEach(cell => {
            cell.oldRow = cell.row
            cell.oldCol = cell.col
            cell.col = cell.oldRow
            cell.row = size - 1 - cell.oldCol
        })
    }
    if (direction === 'down') {
        cells.forEach(cell => {
            cell.oldRow = cell.row
            cell.oldCol = cell.col
            cell.col = cell.oldRow
            cell.row = cell.oldCol
        })
    }
    
    //сдвигаем вправо
    move('right') //Нужно почистить функцию и оставить только часть кода внутри direction === 'right'
    
    //восстанавливаем координаты обратно
    cells.forEach(cell => {
        cell.row = cell.oldRow
        cell.col = cell.oldCol
    })
    

    您可以将此坐标变换算法转换为函数getCell:

    function getCell(row, col, direction) {
        if (direction === 'left') {
            col = size - 1 - cell.col
        }
        if (direction === 'up') {
            let oldRow = row
            row = size - 1 - col
            col = oldRow
        }
        if (direction === 'down') {
            let oldRow = row
            row = col
            col = oldRow
        }
    
        for (let i = 0; i < cells.length; i++) {
            const cell = cells[i];
            if (cell.row === row && cell.col === col && cell.value !== 0) {
                return cell;
            }
        }
    }
    
    • 2
  4. user7860670
    2022-05-07T23:18:11Z2022-05-07T23:18:11Z

    Несложно заметить, что при хранении элементов в плоском массиве разные варианты перебора столбцов и строк отличаются только стартовой позицией и смещениями при переходе к следующим элементам. Соответственно имеет смысл не хардкодить эти величины, а вынести их в отдельную С++style табличку и обращаться к ней при входе в функцию. Также для практической реализации, если надо делать анимации перескакивания и схлопывания ячеек, то имеет смыл добавить в функцию третий аргумент, принимающий функцию обратного вызова для соотв. оповещений.

    type Field = Array<number>;
    
    type Direction = 'left' | 'right' | 'up' | 'down';
    
    type Navigation = { initial: number; next_col: number; next_row: number; };
    
    type Lookup = { [direction in Direction]: Navigation; };
    
    const dim = 4;
    
    const cells_count = dim * dim;
    
    const lookup: Lookup =
    {
        left : { initial: 0              , next_col:  1  , next_row: dim }
    ,   right: { initial: (dim - 1)      , next_col: -1  , next_row: dim }
    ,   up   : { initial: 0              , next_col:  dim, next_row: 1   }
    ,   down : { initial: (dim - 1) * dim, next_col: -dim, next_row: 1   }
    };
    
    function Fold(input: Field, direction: Direction): Field
    {
        const output = new Array<number>(cells_count).fill(0);
        const navigation = lookup[direction];
        let remaining = cells_count;
        let pos = navigation.initial;
        for (;;)
        {
            let read_pos = pos;
            let write_pos = pos;
            for (;;)
            {
                if (0 !== input[read_pos])
                {
                    if (output[write_pos] !== input[read_pos])
                    {
                        if (0 !== output[write_pos])
                        {
                            write_pos += navigation.next_col;
                        }
                        output[write_pos] = input[read_pos];
                    }
                    else
                    {
                        output[write_pos] *= 2;
                        write_pos += navigation.next_col;
                    }
                }
                --remaining;
                if (0 === (remaining % dim))
                {
                    break; // inner loop
                }
                read_pos += navigation.next_col;
                continue; // inner loop
            }
            if (0 === remaining)
            {
                break; // outer loop
            }
            pos += navigation.next_row;
            continue; // outer loop
        }
        return output;
    }
    
    • 1
  5. DiD
    2022-05-10T19:33:18Z2022-05-10T19:33:18Z

    Решил немного расширить ответ. А новый код вместе со старым ответом в один ответ не вместился. Потому выложу отдельным ответом.

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

    В целом, демка получилась неплохая. Над анимацией еще поработать надо.

    Будут вопросы - задавайте. Код может работать некорректно. Если заметили баг, напишите, исправим.

    // Порог срабатывания приковнования
    const TOUCH_THRESHOLD = 50;
    // Цветовая схнма
    COLOR_SCHEMA = [
      'E57373',
      'F06292',
      'BA68C8',
      '9575CD',
      '7986CB',
      '64B5F6',
      '4FC3F7',
      '4DD0E1',
      '4DB6AC',
      '81C784',
      'AED581',
      'DCE775',
      'FFF176',
      'FFD54F',
      'FFB74D',
      'FF8A65',
      'A1887F',
      '90A4AE',
      'E0E0E0',
    ];
    
    // Математические функции и константа
    const { abs, round, random, max, min, sin, cos, hypot, atan2, PI: π, E: 𝒆 } = Math;
    // Функции Object
    const { assign, entries, fromEntries } = Object;
    // Хэлпер querySelector
    const sel = (...q) =>
      !q.length
    ? null
    : Array.isArray(q[0])
    ? sel(q[0].reduce((r, s, i) => ((r += (i ? q[i] : '') + s), r), ''))
    : q.reduce((r, s) => (!r ? null : 'object' == typeof s ? s : r.querySelector(s)), document);
    // Хэлпер querySelectorAll
    const selAll = (...q) =>
      !q.length
    ? null
    : Array.isArray(q[0])
    ? selAll(q[0].reduce((r, s, i) => ((r += (i ? q[i] : '') + s), r), ''))
    : q.reduce(
    (r, s, i, q) =>
      !r ? null : 'object' == typeof s ? s : r['querySelector' + (i == q.length - 1 ? 'All' : '')](s),
    document
      );
    // Хэлперы классов
    const addCls = (el, ...cls) => (el.classList.add(...cls), el);
    const remCls = (el, ...cls) => (el.classList.remove(...cls), el);
    // Хэлпер аттрибутов
    const attr = (el, atr, val) => (typeof val == 'undefined' ? el.getAttribute(atr) : (el.setAttribute(atr, val), el));
    // Хэлпер createElement
    const create = def => {
      const el = document.createElement(def.tag || 'div');
      if (def.cls) el.className = def.cls;
      if (def.css) assign(el.style, def.css);
      if (def.evt) entries(def.evt).forEach(([evt, cb]) => el.addEventListener(evt, cb, false));
      if (def.tgt) def.tgt.appendChild(el);
      if (def.atr) entries(def.atr).forEach(([atr, val]) => attr(el, atr, val));
      if (def.txt) el.innerHTML = def.txt;
      if (def.chd) {
    if (Array.isArray(def.chd)) def.chd.forEach(subdef => create({ tgt: el, ...subdef }));
    else create({ tgt: el, ...def.chd });
      }
      return el;
    };
    // Останавливает событие
    const stop = e => {
      e.preventDefault();
      e.stopPropagation();
    };
    // Преобразователь массива в строку
    const arr2str = arr => arr.map(val => (+val).toString(36)).join``;
    // Преобразователь строки в массив
    const str2arr = str => [...str].map(chr => parseInt(chr, 36));
    // Схемы клавиатуры
    [
      ['left', 'down', 'up', 'right', 'undo', 'redo'],
      ['⬅', '⬇', '⬆', '⮕', '⌫', '⏎'],
      ['H', 'J', 'K', 'L', 'U', '.'],
      ['A', 'S', 'W', 'D', 'Z', 'R'],
    ].forEach((scheme, idx) =>
      create({
    tag: 'tr',
    chd: scheme.map(key => ({ tag: idx ? 'td' : 'th', chd: { tag: 'span', cls: 'key', txt: key } })),
    tgt: sel('table'),
      })
    );
    //Уровни сложности
    [
      [3, 8],
      [4, 11],
      [5, 14],
      [6, 16],
      [7, 19],
    ].forEach(([size, adv]) =>
      create({
    tag: 'button',
    atr: { size, adv },
    chd: [
      { tag: 'label', cls: 'size', txt: `<b>${size}</b>&times;<b>${size}</b>` },
      { tag: 'hr' },
      { tag: 'label', cls: 'advance', txt: `to <b>${adv}</b>` },
    ],
    evt: {
      click: evt => {
    let target = evt.target.closest('button');
    createGame({ size: +attr(target, 'size'), adv: +attr(target, 'adv') });
      },
    },
    tgt: sel('.new-game'),
      })
    );
    // Стили для фрейма выигрыша
    create({
      tag: 'style',
      txt: `.frame.success h3, .frame.success .score { animation: css-fizzy 12s linear 0s infinite; }
    @keyframes css-fizzy{
      ${new Array(13)
    .fill(0)
    .map(
      (v, frameIdx) =>
    `${(100 / 12) * frameIdx}% {text-shadow: ${[
      '0 0 0.2em',
      '0 0 0.2em',
      ...new Array(8)
    .fill(0)
    .map(
      (v, shdowPartIdx) =>
        `${(-1.6) ** shdowPartIdx * sin((frameIdx / 6) * π)}px ${
          (-1.6) ** shdowPartIdx * cos((frameIdx / 6) * π)
        }px ${1.6 ** shdowPartIdx + 0.2}px`
    ),
    ]
      .map((shadow, shadowIdx) => `${shadow} hsl(${frameIdx * 30}deg,99%,${70 - 5 * shadowIdx}%)`)
      .join()};}`
    ).join``}
    }`,
      tgt: document.body,
    });
    // текущая игра
    var game;
    // текущее косание
    let cur = { pageX: null, pageY: null };
    // Обработчик кнопок закрытия
    selAll('button.exit').forEach(
      btn =>
    (btn.onclick = e =>
      attr(e.target.closest('.frame'), 'mode') == 'export' ? (setFrame('game'), activateListeners()) : closeGame())
    );
    // Обработчик кнопкт Экспорт
    sel('button.export').onclick = e => {
      deactivateListeners();
      const txtAr = sel('textarea');
      txtAr.value = JSON.stringify(game.serialize());
      attr(setFrame('memo'), 'mode', 'export');
      txtAr.select();
    };
    // Обработчик кнопки import
    sel('button.import').onclick = e => {
      deactivateListeners();
      const txtAr = sel('textarea');
      txtAr.value = '';
      attr(setFrame('memo'), 'mode', 'import');
      txtAr.select();
    };
    // Обработчик кнопки OK
    sel('button.ok').onclick = e => {
      const txtAr = sel('textarea');
      if (attr(txtAr.closest('.frame'), 'mode') == 'import') {
    try {
      let obj = JSON.parse(txtAr.value);
      // TODO: Check JSON-data
      createGame(obj);
    } catch (e) {
      alert(e.message);
    }
      } else {
    setFrame('game');
    activateListeners();
      }
    };
    // Обработчик кнопки Undo
    sel('button.undo').onclick = e => {
      game.undo();
    };
    // Обработчик кнопки Redo
    sel('button.redo').onclick = e => {
      game.redo();
    };
    // отчищает текущее косание
    function clearCur() {
      cur.pageX = cur.pageY = null;
    }
    // устанавливает начало текущего косания
    function setCur(touch) {
      cur.pageX = touch.pageX;
      cur.pageY = touch.pageY;
    }
    // определяет направление косания
    function dirCur(touch) {
      const dX = touch.pageX - cur.pageX;
      const dY = touch.pageY - cur.pageY;
      const d = Math.hypot(dX, dY);
      if (d > TOUCH_THRESHOLD && (abs(dY) > 2 * abs(dX) || 2 * abs(dY) < abs(dX))) {
    let side = ((π + atan2(dX, dY)) / π) * 2;
    side = round(side);
    side = side < 4 ? side : side - 4;
    let dir = ['u', 'l', 'd', 'r'][side];
    setCur(touch);
    return dir;
      } else {
    return null;
      }
    }
    //Функция создает экземпляр игры
    function createGame(props) {
      setFrame('game');
      game = new Game(props).init();
      activateListeners();
    }
    // Функция закрывает текующий экземпляр игры
    function closeGame() {
      game = null;
      setFrame('lobby');
      deactivateListeners();
    }
    // Функция активирует заданный аргументом фрейм
    function setFrame(frame) {
      selAll('.frame').forEach(frame => remCls(frame, 'show'));
      return addCls(sel(`.${frame}`), 'show');
    }
    // Функция включает обработчики
    function activateListeners() {
      window.ontouchstart = e => {
    if (e.touches.length != 1) {
      clearCur();
    } else {
      setCur(e.touches[0]);
    }
      };
      window.ontouchmove = e => {
    if (e.touches.length != 1) {
      return clearCur();
    }
    let dir = dirCur(e.touches[0]);
    if (dir) {
      game.swap(dir);
    }
      };
      window.ontouchend = e => clearCur();
      window.ontouchcancel = e => clearCur();
      window.onkeydown = e => {
    [
      ['up', ['ArrowUp', 'KeyW', 'KeyK']],
      ['down', ['ArrowDown', 'KeyS', 'KeyJ']],
      ['left', ['ArrowLeft', 'KeyA', 'KeyH']],
      ['right', ['ArrowRight', 'KeyD', 'KeyL']],
      ['undo', ['Backspace', 'KeyU', 'KeyZ']],
      ['redo', ['Enter', 'KeyR', 'Period']],
    ].map(([fn, keys]) => {
      if (keys.includes(e.code) && !e.ctrlKey && !e.altKey && !e.metaKey) {
    game[fn]();
    stop(e);
      }
    });
      };
      window.onresize = e => {
    game.reposition();
      };
      window.onwheel = e => {
    stop(e);
      };
    }
    // Функция выключет обработчики
    function deactivateListeners() {
      // удаляем обработчики
      ['keydown', 'touchend', 'touchcancel', 'touchmove', 'touchstart', 'resize', 'wheel'].map(
    evt => (window[`on${evt}`] = null)
      );
    }
    // Функция пересоздает grid
    function regrid(size) {
      // Удаляем старые ячейки
      selAll(grid, 'div').forEach(cell => cell.remove());
      // Удаляем старые тайлы
      selAll(layer, 'div').forEach(tile => tile.remove());
      // Добавляем новые ячейки
      for (let i = 0; i < size ** 2; i++) create({ tag: 'div', tgt: grid });
      // Шаблоны grid
      assign(grid.style, {
    gridTemplateColumns: `repeat(${size} ,1fr)`,
    gridTemplateRows: `repeat(${size} ,1fr)`,
      });
      // Динамические стили для тайлов
      dynamic.innerHTML =
    Object.entries(COLOR_SCHEMA)
      .map(
    ([dib, color]) =>
      `#layer .tile[val="${+dib + 1}"]{background: #${color};}
      #layer .tile[val="${+dib + 1}"]::before{
    content:"${+dib + 1}";
      }`
      )
      .join('\n') +
    `#layer .tile {
    font-size: calc(80vmin / ${size * 1.2});
    line-height: calc(80vim / ${size * 1.2});
      }`;
    }
    class Game {
      // размер
      size;
      // сложность
      adv;
      // массив значений
      raw;
      // стек отмен
      stackUndo = [];
      // стек повторов
      stackRedo = [];
      // очки
      _score = 0;
      // флаг начала
      started = false;
      constructor(props) {
    assign(this, props);
      }
      // Инициализация
      init() {
    if (!this.size) this.size = 4;
    if (!this.adv) this.adv = round(this.size * 𝒆);
    if (!this.raw) this.raw = new Array(this.size ** 2).fill(0);
    regrid(this.size);
    if (!this.started) {
      this.started = true;
      this.addition().addition();
    } else {
      this.recreate();
    }
    this.score = this.score;
    return this;
      }
      // Дополнение
      addition() {
    // считаем пыстые ячейки
    let freeCells = [];
    for (let i = 0; i < this.size ** 2; i++) {
      if (!this.raw[i]) {
    freeCells.push(i);
      }
    }
    // выбираем рандломную
    let pos = freeCells[~~(freeCells.length * random())];
    // рандомное значение
    let value = 1 + ~~((this.size - 2) * random());
    //
    let [x, y] = [pos % this.size, ~~(pos / this.size)];
    this.raw[pos] = value;
    this.createTile(x, y, value, 'addition');
    return this;
      }
      // Сделать ход
      swap(dir) {
    const [property, method] = {
      u: ['cols', 'backward'],
      d: ['cols', 'forward'],
      r: ['rows', 'forward'],
      l: ['rows', 'backward'],
    }[dir];
    let lines = this[property];
    let newLines = [];
    let changes = 0;
    // Вызываем метод сдвига
    for (let i = 0; i < this.size; i++) {
      let line = lines[i];
      let result = this[method](line);
      let { moves, mixes, newLine } = result;
      for (let j = 0; j < moves.length; j++) {
    let [from, to] = moves[j];
    let [xFrom, yFrom, xTo, yTo] = property == 'cols' ? [i, from, i, to] : [from, i, to, i];
    let tile = this.getTile(xFrom, yFrom);
    assign(tile.style, this.getRect(xTo, yTo));
    attr(tile, 'x', xTo);
    attr(tile, 'y', yTo);
      }
      // вызываем метод мутирования
      for (let j = 0; j < mixes.length; j++) {
    let [x, y] = property == 'cols' ? [i, mixes[j]] : [mixes[j], i];
    let value = newLine[mixes[j]];
    this.score += value;
    // Удаляем старые тайлы
    this.getTile(x, y).remove();
    this.getTile(x, y).remove();
    // создаем новые тайлы
    this.createTile(x, y, value, 'mix');
      }
      changes += mixes.length + moves.length;
      newLines.push(newLine);
    }
    // Если что-то поменялось
    if (changes) {
      // Сохраняем старые значения
      this.stackUndo.push([arr2str(this.raw), this._score]);
      this.stackRedo.splice(0, this.stackRedo.length);
      // Применяем значения
      this[property] = newLines;
      // Добавляем рандомный тайл
      this.addition();
      // Чекнем на вымгрыш или проигрыш
      this.check();
      // На всякий перепозиционируем тайлы
      requestAnimationFrame(() => {
    this.reposition();
      });
    }
      }
      // биндинги сторон
      up() {
    this.swap('u');
      }
      down() {
    this.swap('d');
      }
      left() {
    this.swap('l');
      }
      right() {
    this.swap('r');
      }
      // отменить
      undo() {
    // если стек пуст
    if (!this.stackUndo.length) {
      return;
    }
    this.stackRedo.push([arr2str(this.raw), this.score]);
    let [raw, score] = this.stackUndo.pop();
    this.score = score;
    this.raw = str2arr(raw);
    this.recreate();
      }
      // повторить
      redo() {
    // если стек пуст
    if (!this.stackRedo.length) {
      return;
    }
    this.stackUndo.push([arr2str(this.raw), this.score]);
    let [raw, score] = this.stackRedo.pop();
    this.score = score;
    this.raw = str2arr(raw);
    this.recreate();
      }
      // Проверка на выигрыш или проигрыш
      check() {
    if (this.raw.find(item => item == this.adv)) {
      return setFrame('success');
    }
    if (!this.raw.includes(0)) {
      for (let y = 0; y < this.size; y++) {
    for (let x = 0; x < this.size - 1; x++) {
      if (this.raw[y * this.size + x] == this.raw[y * this.size + x + 1]) {
    return;
      }
    }
      }
      for (let x = 0; x < this.size; x++) {
    for (let y = 0; y < this.size - 1; y++) {
      if (this.raw[y * this.size + x] == this.raw[(y + 1) * this.size + x]) {
    return;
      }
    }
      }
      return setFrame('failure');
    }
      }
      // сдвигаем назад
      backward(line) {
    let moves = [];
    let mixes = [];
    let skip = [];
    let pos = -1;
    let lastValue = null;
    let lastIndex = null;
    let newLine = [];
    for (let i = 0, l = line.length; i < l; i++) {
      if (line[i] == 0) {
    skip.push(i);
      } else if (lastIndex != null && lastValue != null && line[i] == lastValue) {
    moves.push([i, lastIndex]);
    mixes.push(lastIndex);
    newLine[lastIndex]++;
    lastIndex = null;
    lastValue = null;
      } else {
    lastIndex = newLine.length;
    newLine.push(line[i]);
    lastValue = line[i];
    // }
    if (++pos != i) {
      moves.push([i, pos]);
    }
      }
    }
    let endian = this.size - newLine.length;
    newLine.splice(newLine.length, 0, ...new Array(endian).fill(0));
    return { moves, mixes, newLine };
      }
      // сдвигаем линию вперед
      forward(line) {
    let moves = [];
    let mixes = [];
    let skip = [];
    let pos = this.size;
    let lastValue = null;
    let lastIndex = null;
    let newLine = new Array(this.size).fill(0);
    for (let i = line.length - 1; i >= 0; i--) {
      if (line[i] == 0) {
    skip.push(i);
      } else if (lastIndex != null && lastValue != null && line[i] == lastValue) {
    moves.push([i, lastIndex]);
    mixes.push(lastIndex);
    newLine[lastIndex]++;
    lastIndex = null;
    lastValue = null;
      } else {
    lastIndex = --pos;
    newLine[pos] = line[i];
    lastValue = line[i];
    if (pos != i) {
      moves.push([i, pos]);
    }
      }
    }
    return { moves, mixes, newLine };
      }
      // перепозиционирование тайлов
      reposition() {
    // получаем все тайлы
    let tiles = selAll(layer, '.tile');
    for (let tile of tiles) {
      let [x, y] = [+attr(tile, 'x'), +attr(tile, 'y')];
      // циклично каждому устанавливаем позицию в соответствии с коордтнатами
      assign(tile.style, this.getRect(x, y));
    }
      }
      // пересоздать тайлы
      recreate() {
    // удаляем все тайлы
    selAll(layer, 'div').forEach(tile => tile.remove());
    // создаем тайлы
    for (let [pos, dib] of this.raw.entries()) {
      if (!dib) continue;
      let [x, y] = [pos % this.size, ~~(pos / this.size)];
      this.createTile(x, y, dib);
    }
      }
      // создать тайл
      createTile(x, y, val, anim) {
    let rect = this.getRect(x, y);
    let tile = create({
      cls: 'tile',
      atr: { x, y, val },
      css: this.getRect(x, y),
      tgt: layer,
    });
    if (anim) {
      requestAnimationFrame(() => {
    if (anim) {
      tile.classList.add(anim);
    }
    requestAnimationFrame(() => {
      tile.classList.add('transition');
      if (anim) {
    requestAnimationFrame(() => {
      tile.classList.remove(anim);
    });
      }
    });
      });
    }
    return tile;
      }
      // Найти тайл
      getTile(x, y) {
    return sel(layer, `div[x="${x}"][y="${y}"]`);
      }
      // Найти область ячейки
      getRect(x, y) {
    const rect = sel(grid, `div:nth-child(${this.size * y + x + 1})`).getBoundingClientRect();
    return fromEntries(['left', 'top', 'width', 'height'].map(prop => [prop, rect[prop] + 'px']));
      }
      // Свойство возвращает массив столбцов
      get cols() {
    const cols = [];
    for (let ci = 0, cn = this.size; ci < cn; ci++) {
      let col = [];
      for (let ri = 0, rn = this.size; ri < rn; ri++) {
    col.push(this.raw[ri * cn + ci]);
      }
      cols.push(col);
    }
    return cols;
      }
      // Свойство записывает массив столбцов
      set cols(v) {
    for (let ci = 0, cn = this.size; ci < cn; ci++) {
      for (let ri = 0, rn = this.size; ri < rn; ri++) {
    this.raw[ri * cn + ci] = v[ci][ri];
      }
    }
      }
      // Свойство возвращает массив строк
      get rows() {
    const rows = [];
    for (let ri = 0, rn = this.size; ri < rn; ri++) {
      let row = [];
      for (let ci = 0, cn = this.size; ci < cn; ci++) {
    row.push(this.raw[ri * cn + ci]);
      }
      rows.push(row);
    }
    return rows;
      }
      // Свойство записывает массив строк
      set rows(v) {
    for (let ri = 0, rn = this.size; ri < rn; ri++) {
      for (let ci = 0, cn = this.size; ci < cn; ci++) {
    this.raw[ri * cn + ci] = v[ri][ci];
      }
    }
      }
      // Свойство возвращает количество очков
      get score() {
    return this._score;
      }
      // Свойство устанавливает количество очков
      set score(v) {
    selAll('.score').forEach(el => (attr(el, 'text', v).innerHTML = v));
    this._score = v;
      }
      // Сериализация игры
      serialize() {
    return fromEntries(
      ['size', 'adv', 'raw', 'score', 'started', 'stackUndo', 'stackRedo'].map(prop => [prop, this[prop]])
    );
      }
    }
    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }
    body {
      margin: 0;
      min-height: 100vw;
      overflow: hidden;
    }
    .frame {
      width: 100vw;
      height: 100vh;
    }
    .frame {
      display: none;
    }
    .show,
    .lobby.show,
    .new-game,
    .panel {
      display: flex;
    }
    .anim {
      transition: all 100ms;
    }
    .lobby,
    .memo,
    .game {
      flex-flow: column nowrap;
      justify-content: flex-start;
      align-items: center;
    }
    .meta,
    .new-game {
      justify-content: center;
      align-items: center;
      flex-flow: row wrap;
      gap: 1rem;
    }
    
    .note,
    .kbd {
      width: 25rem;
    }
    .note::before,
    .kbd::before {
      margin-right: 1rem;
      display: block;
    }
    .note::before {
      content: '⚠';
      font-size: 3rem;
    }
    .kbd::before {
      content: '⌨';
      font-size: 5rem;
      line-height: 3rem;
    }
    .meta,
    .note,
    .kbd {
      display: flex;
      flex-flow: row nowrap;
    }
    table tr th {
      font-size: 0.8rem;
    }
    table tr td span {
      width: 2rem;
      height: 1.6rem;
      border: 1px solid #3339;
      text-align: center;
      background: #8080802b;
      border-collapse: separate;
      border-bottom: 1px solid #4449;
      border-radius: 3px;
      box-shadow: #000 0px -1px 0px 0px inset;
      color: #000;
      cursor: default;
      display: block;
      font-family: -apple-system, system-ui, sans-serif;
      font-size: 1.2rem;
      forced-color-adjust: none;
      line-height: 1rem;
      margin: 0 2px 0 0;
      padding: 3px 5px;
      user-select: none;
      white-space: nowrap;
      -webkit-border-horizontal-spacing: 0px;
      -webkit-border-vertical-spacing: 0px;
      -webkit-font-smoothing: antialiased;
    }
    h3,
    .score,
    #layer div {
      font-family: Impact, Haettenschweiler, 'Arial Narrow Bold', sans-serif;
    }
    h3,
    .score {
      font-size: 4rem;
    }
    #grid {
      background: #888;
      display: grid;
    }
    #layer,
    #grid {
      border-radius: 1vmin;
      width: calc(100vw - 2rem);
      height: calc(100vh - 2rem);
      max-width: calc(100vh - 2rem);
      max-height: calc(100vw - 2rem);
      position: fixed;
      top: 1rem;
      left: 1rem;
      padding: 1vmin;
      gap: 1vmin;
    }
    #layer div {
      z-index: 10;
      position: fixed;
      text-align: center;
    }
    #layer div,
    #grid div {
      border-radius: 0.5vmin;
    }
    #grid div {
      background: #fff;
    }
    h3,
    .failure .score,
    .success .score {
      position: fixed;
      left: 50vw;
      transform: translate(-50%, -150%);
      text-align: center;
      color: #fffa;
    }
    h3 {
      top: 45vh;
    }
    .failed .score,
    .success .score {
      top: 55vh;
    }
    .layer div {
      position: fixed;
      text-align: center;
      color: #555;
      text-shadow: 0 0 0.2rem #ff0, 0 0 0.4rem #fff;
    }
    .layer > .tile {
      transform: scale(1);
      opacity: 1;
    }
    .layer > .tile.addition {
      transform: scale(0);
    }
    .layer > .tile.mixed {
      transform: scale(1.5);
      opacity: 0;
    }
    .transition {
      transition: all 100ms ease-out;
    }
    .exit {
      margin: 1rem 1rem 0 1rem;
      width: 2rem;
      height: 2rem;
      border-radius: 1rem;
      background: #f00;
      color: #fff;
      border: 2px solid #000;
      /* box-shadow: 0 0 1rem #f00; */
      display: inline-block;
      text-align: center;
      padding: 0;
      line-height: 1.5rem;
      cursor: pointer;
    }
    .exit:hover {
      position: relative;
      border: 2px solid #fff;
      box-shadow: 0 0 1rem #f00;
      top: -2px;
    }
    .exit:active {
      border: 2px solid #fffc;
      box-shadow: 0 0 0.5rem #f00;
      top: 2px;
    }
    .exit::before {
      /* ✖× */
      content: '×';
      font-size: 2rem;
      line-height: 1.5rem;
      text-align: center;
    }
    
    button.ok {
      width: 6rem;
      height: 3rem;
      font-size: 2rem;
      display: inline-block;
    }
    
    textarea {
      height: calc(100vh - 3rem - 3rem - 3rem);
      margin: 1rem;
      width: calc(100vw - 2rem);
    }
    header {
      display: flex;
      flex-flow: row;
      justify-content: space-between;
      align-items: top;
      width: 100vw;
    }
    .game .score {
      position: fixed;
      top: 3rem;
      right: 1rem;
      color: #0008;
      cursor: default;
      user-select: none;
    }
    button {
      display: inline-block;
      vertical-align: center;
    }
    button p {
      float: right;
      /* clear: right; */
      margin: 0.8rem 0 0 0.3rem;
    }
    
    .undo::before,
    .redo::before,
    .import::before,
    .export::before {
      font-size: 2rem;
      display: block;
      float: left;
    }
    .undo::before {
      content: '◀';
    }
    .redo::before {
      content: '▶';
    }
    .export::before {
      content: '💾';
    }
    .import::before {
      content: '🗂';
    }
    .memo[mode='import'] header p::before {
      content: 'Вставте содержимое файла в это поле и нажмите ОК:';
    }
    .memo[mode='export'] header p::before {
      content: 'Сохраните содержимое файла из этого поля';
    }
    
    .memo p {
      margin: 2rem 0 0 1rem;
    }
    .panel {
      position: fixed;
      bottom: 1rem;
      right: 1rem;
    }
    
    :root {
      --f-size: 15;
      --f-unit: 1vmin;
      --f: calc(var(--f-size) * var(--f-unit));
      --bg: #222;
    }
    .failure.frame,
    .success.frame {
      background: var(--bg);
    }
    .failure.frame .score,
    .failure.frame h3 {
      flex: 1;
      line-height: 0.75;
      margin: auto;
      color: #1af0dc;
      text-align: center;
      transform: scaleX(var(--scale, 1));
      animation: glitch-p 11s infinite alternate;
      font-size: 6vmin;
      position: fixed;
      left: 50vw;
      transform: translate(-50%, -50%);
    }
    .failure.frame .loose {
      top: 45vh;
    }
    .failure.frame .score {
      top: 55vh;
    }
    .failure.frame > *::before,
    .failure.frame > *::after {
      --top: 0;
      --left: 0;
      --v-height: 30%;
      --n-tenth: calc(var(--f-size) * 0.1 * var(--top));
      --t-cut: calc(var(--n-tenth) / var(--f-size) * 100%);
      --b-cut: calc(var(--t-cut) + var(--v-height));
      content: attr(text);
      position: absolute;
      width: 100%;
      left: 0;
      text-align: center;
      transform: translateX(calc(var(--left) * 100%));
      filter: drop-shadow(0 0 transparent);
      text-shadow: calc(var(--left) * -3em) 0 0.02em lime, calc(var(--left) * -6em) 0 0.02em #ff00e1;
      background-color: var(--bg);
      clip-path: polygon(0% var(--t-cut), 100% var(--t-cut), 100% var(--b-cut), 0% var(--b-cut));
    }
    .failure.frame > *::before {
      animation: glitch-b 1.7s infinite alternate-reverse;
    }
    .failure.frame > *::after {
      animation: glitch-a 3.1s infinite alternate;
    }
    @keyframes glitch-p {
      17% {
    --scale: 0.87;
      }
      31% {
    --scale: 1.1;
      }
      37% {
    --scale: 1.3;
      }
      47% {
    --scale: 0.91;
      }
      87% {
    --scale: 1;
      }
    }
    @keyframes glitch-a {
      10%,
      30%,
      50%,
      70%,
      90% {
    --top: 0;
    --left: 0;
      }
      0% {
    --v-height: 15%;
      }
      20% {
    --left: 0.005;
      }
      40% {
    --left: 0.01;
    --v-height: 20%;
    --top: 3;
      }
      60% {
    --left: 0.03;
    --v-height: 25%;
    --top: 6;
      }
      80% {
    --left: 0.07;
    --v-height: 5%;
    --top: 8;
      }
      100% {
    --left: 0.083;
    --v-height: 30%;
    --top: 1;
      }
    }
    @keyframes glitch-b {
      10%,
      30%,
      50%,
      70%,
      90% {
    --top: 0;
    --left: 0;
      }
      0% {
    --v-height: 15%;
    --top: 10;
      }
      20% {
    --left: -0.005;
      }
      40% {
    --left: -0.01;
    --v-height: 17%;
    --top: 3;
      }
      60% {
    --left: -0.03;
    --v-height: 35%;
    --top: 6;
      }
      80% {
    --left: -0.07;
    --v-height: 5%;
    --top: 8;
      }
      100% {
    --left: -0.083;
    --v-height: 30%;
    --top: 1;
      }
    }
    
    @keyframes mixed {
      0% {
    transform: scale(1);
      }
      50% {
    transform: scale(1.5);
      }
      100% {
    transform: scale(1);
      }
    }
    <style id="dynamic"></style>
    <div class="frame lobby show">
    <div class="meta">
      <div class="note">
    <p>
      Игра отличается от оригинала!<br />Длинные числа заменены на степени двоек.<br />Например,
      <tt>2 + 2 => 3</tt> и <tt>3 + 3 => 4</tt>.<br />Управление с тачскрина не требует<br />отрыва пальца от
      экрана.<br />Чувствительность сенсора завышена.
    </p>
      </div>
      <div class="kbd">
    <table></table>
      </div>
    </div>
    <div class="new-game">
      <button class="import">
    <p>Import</p>
      </button>
    </div>
    </div>
    
    <div class="frame game">
    <header>
      <p></p>
      <button class="exit"></button>
    </header>
    <div class="score">0</div>
    <div class="panel">
      <button class="export"><p>Export</p></button>
      <button class="undo"><p>Undo</p></button>
      <button class="redo"><p>Redo</p></button>
    </div>
    <div id="grid"></div>
    <div id="layer"></div>
    <style></style>
    </div>
    
    <div class="frame success">
    <header>
      <p></p>
      <button class="exit"></button>
    </header>
    <h3 text="YOU WIN">YOU WIN</h3>
    <div class="score"></div>
    </div>
    
    <div class="frame failure">
    <header>
      <p></p>
      <button class="exit"></button>
    </header>
    <h3 text="GAME OVER">GAME OVER</h3>
    <div class="score"></div>
    </div>
    
    <div class="frame memo">
    <header>
      <p class="message"></p>
      <button class="exit"></button>
    </header>
    <textarea></textarea>
    <button class="ok">OK</button>
    </div>

    • 0

相关问题

  • 第二个 Instagram 按钮的 CSS 属性

  • 由于模糊,内容不可见

  • 弹出队列。消息显示不正确

  • 是否可以在 for 循环中插入提示?

  • 如何将 JSON 请求中的信息输出到数据表 Vuetify vue.js?

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    表格填充不起作用

    • 2 个回答
  • Marko Smith

    提示 50/50,有两个,其中一个是正确的

    • 1 个回答
  • Marko Smith

    在 PyQt5 中停止进程

    • 1 个回答
  • Marko Smith

    我的脚本不工作

    • 1 个回答
  • Marko Smith

    在文本文件中写入和读取列表

    • 2 个回答
  • Marko Smith

    如何像屏幕截图中那样并排排列这些块?

    • 1 个回答
  • Marko Smith

    确定文本文件中每一行的字符数

    • 2 个回答
  • Marko Smith

    将接口对象传递给 JAVA 构造函数

    • 1 个回答
  • Marko Smith

    正确更新数据库中的数据

    • 1 个回答
  • Marko Smith

    Python解析不是css

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5