Sliding Tiles Puzzle

The sliding tiles puzzle written in Preact: https://www.math.nmsu.edu/~pmorandi/math111f01/SlidingTiles.html

by remarkablemark

HTML

<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/preact/8.2.6/preact.min.js"></script>

CSS

body {
  font-family: 'Lucida Grande', Helvetica, sans-serif;
}

table button {
  height: 21px;
  width: 25px;
}

form {
  font-size: 12px;
}

form input[type="number"] {
  width: 25px;
}

Babel + JSX

const { h, render, Component } = preact; /** @jsx h */

/**
 * Generates an array of numbers starting at 1.
 *
 * @param  {Number} size
 * @return {Array}
 */
const generateNumbers = size => Array
  .apply(null, { length: size })
  .map((_, index) => index + 1);

/**
 * Shuffles an array.
 *
 * @param  {Array}   array
 * @param  {Boolean} [shouldMutate=false]
 * @return {Array}
 */
const shuffleArray = (array = [], shouldMutate = false) => {
	const arr = shouldMutate ? array : array.slice();
	for (let i = arr.length - 1; i > 0; i--) {
		const j = Math.floor(Math.random() * (i + 1));
		const temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}
	return arr;
};

/**
 * Generates solution.
 *
 * @param  {Number} size
 * @return {Object}
 */
const generateSolution = size => {
  const numbers = generateNumbers(size ** 2 - 1);
  numbers.push(null);
  const solution = [];

  while (numbers.length) {
    solution.push(numbers.splice(0, size));
  }

  return {
    solution,
    solutionJSON: JSON.stringify(solution),
  };
};

/**
 * Generates puzzle.
 *
 * @param  {Number} size
 * @return {Object}
 */
const generatePuzzle = size => {
  if (size < 2) {
    throw Error('Puzzle size must be an integer greater than 1.');
  }

  const numbers = generateNumbers(size ** 2 - 1);
  const random = shuffleArray(generateNumbers(size ** 2 - 1));

  // make sure numbers are random
  if (JSON.stringify(random) === JSON.stringify(numbers)) {
    return generatePuzzle(size);
  }

  random.push(null);
  const puzzle = [];

  while (random.length) {
    puzzle.push(random.splice(0, size));
  }

  return { puzzle };
};

class TileSlider extends Component {
  state = {
    size: this.props.size,
    ...generatePuzzle(this.props.size),
    ...generateSolution(this.props.size),
  };

  /**
   * Swaps tile with empty one when clicked.
   *
   * @param {SyntheticEvent}
   */
  handleClick = ({ target }) => {
    const puzzle = this.state.puzzle.map(row => row.slice());
    let { value, x, y } =...