Простая головоломка на JavaScript

brainteaser-jsОднажды, когда я сидел в универе на скучной паре, мне почему-то вспомнилась одна забавная головоломка из квеста «Машинариум». Я давно забыл, как она решается и мне почему-то очень захотелось вспомнить решение. Рисовать ход решения на бумаге мне показалось утомительным, и я решил написать эту головоломку на js. Позднее мне захотелось поделиться этой забавной игрушкой с вами. Головоломка безымянная, но чтобы хоть как-то обозначить, о чём мы будем говорить, я решил назвать её «cтрелки».

Правила

Игровое поле в этой головоломке представляет собой семь вертикально расположенных ячеек, в трёх верхних из которых расположены стрелки, указывающие вниз, в трёх нижних — стрелки, указывающие вверх, а центральная ячейка пустая. Задача состоит в том, чтобы расположить стрелки, указывающие вниз, в трёх нижних ячейках, а стрелки указывающие вверх в трёх верхних. Указывающие вниз стрелки можно передвигать только вниз, указывающие вверх только вверх. Стрелка может переместится только в пустую ячейку перед собой, либо перепрыгнуть в пустую ячейку через заполненную.

HTML

Я решил, что игровое поле будет представляться в виде таблицы, а стрелки будут символами ↑ и ↓. Так же я решил добавить возможность делать шаг назад в решении.

<table id="game-table">
  <tr>
    <td>&darr;</td>
  </tr>
  <tr>
    <td>&darr;</td>
  </tr>
  <tr>
    <td>&darr;</td>
  </tr>
  <tr>
    <td></td>
  </tr>
  <tr>
    <td>&uarr;</td>
  </tr>
  <tr>
    <td>&uarr;</td>
  </tr>
  <tr>
    <td>&uarr;</td>
  </tr>
</table>
 
<a href="#">&larr; шаг назад</a>

CSS

Для того, чтобы игрушка выглядела более привлекательно я добавил css-код.

body {
  text-align : center;
  background-color: #2a2a2a;
  font-family: Tahoma;  
}
 
#game-table {
  width: 60px;
  height: 420px;  
  margin: 20px auto;  
}
 
#game-table td {
  border: solid 1px gray;    
  font-size: 35px;
  color: gray;
  height: 50px;
}
 
#game-table td:hover {
  color: white;
  cursor: pointer;
  border-color: white;
}
 
a {
  color: gray;
  cursor: pointer;
  text-decoration: none;
  margin: 10px;
}

На это работа над внешним видом «стрелок» была закончена.

JavaScript

Написание js-кода я начал с объявления констант. Как таковых констант в js, к сожалению, нет, но значения, которые считаются константными желательно хранить в переменных, имена которых написаны большими буквами.

var UP = 1, DOWN = -1, EMPTY = 0;

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

Затем я объявил массив, хранящий исходное положение стрелок и массив, который хранит их выигрышное положение.

var arrows = [DOWN, DOWN, DOWN, EMPTY, UP, UP, UP],
  rightArrows = [UP, UP, UP, EMPTY, DOWN, DOWN, DOWN];

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

var cells = [];

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

var steps = [];

Инициализировать игрушку будет функция arrowsGame, в качестве параметров она принимает таблицу, которая представляет собой игровое поле, и, необязательно, функцию, которая сработает, когда головоломка будет решена.

function arrowsGame(table, onWin) {
 
/*
  * Получаем стоки таблицы и
 * обявляем переменну для хранение ячейки
 */
 
var rows = table.rows, cell;
 
for (var i = 0; i < rows.length; i++) {
  cell = rows[i].cells[0];
  // Запоминаем номер ячейки
  cell.setAttribute('data', i);
  // Обрабатываем клик по ячейке
  cell.onclick = cellClick;
  // Сохраняем ячейки
  cells.push(cell);
}
 
/*
 * Если параметр onWin был передан,
 * то переопределяем заданную по умолчанию функцию,
 * вызывающеюся, если решение найдено.
 */
 
  if (typeof onWin == 'function') {
    onWinDefault = onWin;
  }
}

Функция onWinDefault выглядит просто, но сложнее и не нужно, ведь её всегда можно переопределить передав в параметре onWin другую функцию.

function onWinDefault() {
  alert('Ура!');
}

Функция cellClick, вызывающаяся при клике по ячейке таблицы выглядит вот так:

function cellClick(e) {
  var e = e || window.event,
    el = e.srcElement || e.target,
    i = parseInt(el.getAttribute('data'));
 
 
  if (arrows[i] == UP) {
    if (arrows[i - 1] == EMPTY) {
    swap(el, cells[i - 1]);
    } else if (arrows[i - 2] == EMPTY) {
    swap(el, cells[i - 2]);
    }
  } else if (arrows[i] == DOWN) {
    if (arrows[i + 1] == EMPTY) {
      swap(el, cells[i + 1]);
    } else if (arrows[i + 2] == EMPTY) {
      swap(el, cells[i + 2]);
    }
  }
 
  if (isWin()) {
    onWinDefault();
  }
}

В качестве параметра эта функция в полноценных браузерах принимает объект события в котором хранится элемент, с которым это событие произошло (источник события), и прочие данные. Для обеспечения кроссбраузерного получения объекта события нужна строка:

var e = e || window.event,

Для кроссбраузерного получения источника события нужен следующий код:

el = e.srcElement || e.target

Далее происходит извлечение номера ячейки и приведение его к целочисленному типу.

i = parseInt(el.getAttribute('data'));

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

if (arrows[i] == UP) {
  if (arrows[i - 1] == EMPTY) {
   swap(el, cells[i - 1]);
  } else if (arrows[i - 2] == EMPTY) {
   swap(el, cells[i - 2]);
  }
} else if (arrows[i] == DOWN) {
  if (arrows[i + 1] == EMPTY) {
   swap(el, cells[i + 1]);
  } else if (arrows[i + 2] == EMPTY) {
   swap(el, cells[i + 2]);
  }
}

Далее, если положение стрелок выигрышное вызывается функция onWinDefault.

if (isWin()) {
  onWinDefault();
}

Функция swap меняет местами содержимое двух ячеек, задействованных при ходе и производит соответствующие изменения в массиве arrows.

function swap(cell1, cell2, isUndo) {
// Извлекаем номера ячеек
  var i1 = parseInt(cell1.getAttribute('data')),
    i2 = parseInt(cell2.getAttribute('data')),
    t;
 
// меняем местами их содержимое
t = cell1.innerHTML;  
  cell1.innerHTML = cell2.innerHTML;
  cell2.innerHTML = t;
 
// меняем местами соответствующие элемены массива arrows 
  t = arrows[i1];
  arrows[i1] = arrows[i2];
  arrows[i2] = t;
 
// Запоминаем ход, если функция не вызвана для возвращения на шаг назад
  if (!isUndo) {
    steps.push([i1, i2]);
  }
}

Функция, проверяющая, является ли положение стрелок выигрышным.

function isWin() {
  for (var i = 0; i < arrows.length; i++) {
    if (arrows[i] != rightArrows[i]) {
    return false;
    }
  }
  return true;
}

И, наконец, функция, реализующая возвращение на шаг назад:

arrowsGame.undo = function () {
  var step = steps.pop();
  if (step) {
    swap(cells[step[0]], cells[step[1]], true);
  }
}

Весь описанный выше код я заключил в анонимную функцию, которую тут же вызвал.

var arrowsGame = (function () {
// весь код тут
  return arrowsGame;
})();

Подобного рода функции в js называются замыканиями. В моем случае эта функция так же возвращает функцию, необходимую для инициализации игры. Обратите внимание, что в свойство undo возвращаемой функции записана функция, реализующая возвращение на шаг назад.

Замыкание необходимо для того, чтобы не засорять глобальную область видимости переменными, которые там не нужны. Всё, что доступно извне этого замыкания — это функция arrowsGame со своим свойством undo.

Для инициализации этой игрушки необходимо просто подключить файл с описанным выше кодом в html-документ, в котором находится игровое поле и вызвать функцию arrowsGame передав ей в качестве параметра таблицу, которая будет игровым полем. Сделать это нужно, естественно, после загрузки документа.

window.onload = function () {
  arrowsGame(document.getElementById('game-table'));
}

Для реализации возможности возвращаться на шаг назад в ходе решения нужно повесить на ссылку или кнопку обработчик события:

<a href="#" onclick="return undo()">&larr; шаг назад</a>

Функция undo может выглядеть вот так:

function undo() {
  arrowsGame.undo();
  return false;
}

На этом всё. Как всегда, успехов вам!

Исходники | Демо

  • Денчик

    А как ее пройти не подскажете?

  • Денчик

    Я прошел ее 🙂

  • Денчик

    После выйгрыша при клике на любую стрелку выдает «Ура!»

  • Semen Alekseev

    Головоломка не такая сложная. Я рад, что прошёл ее.