var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {}; // Model constructor builds a solveable, shuffled slide puzzle model. SLIDE_PUZZLE.Model = function(rows, columns) { 'use strict'; this.ROWS = rows; this.COLUMNS = columns; this.EMPTY = this.ROWS * this.COLUMNS - 1; this._position = []; this._moves = []; for (var i = 0; i < this.ROWS * this.COLUMNS; i++) { this._position.push(i); } do { this._shuffle(); } while (!this._isSolveable()); }; // getPosition returns the current layout of the puzzle pieces. SLIDE_PUZZLE.Model.prototype.getPosition = function() { 'use strict'; return this._position.slice(); }; SLIDE_PUZZLE.Model.prototype.getPositionOf = function(piece) { 'use strict'; for (var i = 0; i < this._position.length; i++) { if (this._position[i] === piece) { return i; } } return -1; } // getMoves returns the history of all moves. SLIDE_PUZZLE.Model.prototype.getMoves = function() { 'use strict'; return this._moves.slice(); }; // isSolved indicates whether the puzzle is in its solved order. SLIDE_PUZZLE.Model.prototype.isSolved = function() { 'use strict'; for (var i = 0; i < this._position.length - 1; i++) { if (this._position[i] > this._position[i+1]) return false; } return true; }; // move moves piece into the blank space and logs the move in the move history. // move throws an error if the move is invalid or if the piece doesn't exist. SLIDE_PUZZLE.Model.prototype.move = function(piece) { 'use strict'; if (!this._isValidMove(piece)) { throw new Error('Invalid move'); } var iPiece = this._position.indexOf(piece); var iEmpty = this._position.indexOf(this.EMPTY); this._swap(iPiece, iEmpty); this._moves.push(piece); }; // undo reverses the last move and removes it from the move history. // undo throws an error if there are no moves in the history. SLIDE_PUZZLE.Model.prototype.undo = function() { 'use strict'; if (this._moves.length === 0) { throw new Error('No moves to undo'); } var piece = this._moves.pop(); var iPiece = this._position.indexOf(piece); var iEmpty = this._position.indexOf(this.EMPTY); this._swap(iPiece, iEmpty); }; // _isValidMove determines whether piece can move, that is, whether it is next to the empty space. SLIDE_PUZZLE.Model.prototype._isValidMove = function(piece) { 'use strict'; if (!this._isValidPiece(piece)) { return false; } var iPiece = this._position.indexOf(piece); var iEmpty = this._position.indexOf(this.EMPTY); return Math.abs(iPiece - iEmpty) === this.COLUMNS || Math.abs(iPiece - iEmpty) === 1; }; // _isValidPiece indicates whether piece is present in this puzzle. SLIDE_PUZZLE.Model.prototype._isValidPiece = function(piece) { 'use strict'; return piece >= 0 && piece < this.EMPTY; }; // _isSolveable indicates whether the puzzle is solveable, using Mark Ryan's algorithm. // See https://www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html. SLIDE_PUZZLE.Model.prototype._isSolveable = function() { 'use strict'; function even(n) { return n % 2 === 0; } // Count inversions var inversions = 0; for (var i = 0; i < this._position.length; i++) { for (var j = i + 1; j < this._position.length; j++) { if (this._position[j] < this._position[i]) { inversions++; } } } var iEmpty = this._position.indexOf(this.EMPTY); var isEmptyOddFromBottom = !even(this.ROWS - Math.floor(iEmpty / this.ROWS)); var width = this.COLUMNS; return (!even(width) && even(inversions)) || (even(width) && isEmptyOddFromBottom === even(inversions)); }; // _swap swaps the pieces at indices i1 and i2 in this._position. SLIDE_PUZZLE.Model.prototype._swap = function(i1, i2) { 'use strict'; var tmp = this._position[i1]; this._position[i1] = this._position[i2]; this._position[i2] = tmp; }; // _shuffle performs a Fisher-Yates shuffle on this._position. SLIDE_PUZZLE.Model.prototype._shuffle = function() { 'use strict'; // Returns a random int n where 0 ≤ n < max function randomInt(max) { return Math.floor(Math.random() * max); } //TODO: should possible random ints include i? for (var i = this._position.length - 1; i > 0; i--) { this._swap(randomInt(i), i); } }; SLIDE_PUZZLE.Model.prototype.toString = function() { 'use strict'; var lines = []; for (var i = 0; i < this.ROWS; i++) { var rowStart = i * this.COLUMNS; lines.push(this._position.slice(rowStart, rowStart + this.COLUMNS).join('\t')); } return lines.join('\n'); };