152 lines
4.3 KiB
JavaScript
152 lines
4.3 KiB
JavaScript
|
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');
|
||
|
};
|