Commit old files

This commit is contained in:
2024-06-21 10:04:10 -06:00
commit 95bd0ea6ed
14 changed files with 1265 additions and 0 deletions

114
js/SlidePuzzleController.js Normal file
View File

@ -0,0 +1,114 @@
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
SLIDE_PUZZLE.VideoController = function(rows, columns) {
this.STOPWATCH_DELAY = 500;
this._rows = rows;
this._columns = columns;
this._model = null;
this._view = null;
this._stopwatch = new SLIDE_PUZZLE.Stopwatch(this.STOPWATCH_DELAY);
this._undoing = false;
}
SLIDE_PUZZLE.VideoController.prototype.initView = function() {
var self = this;
this._view = new SLIDE_PUZZLE.VideoView(this._rows, this._columns);
this._view.addEventListener("pieceClicked", this._tryMove.bind(this));
this._view.addEventListener('undo', this._undo.bind(this));
this._view.addEventListener('newGame', this._newGame.bind(this));
this._stopwatch.addEventListener('tick', function() {
self._view.timerText = self._stopwatch.elapsedString;
});
this._newGame();
}
SLIDE_PUZZLE.VideoController.prototype._tryMove = function(event) {
var index = this._rowColumnToIndex(event.detail.row, event.detail.column);
var piece = this._rowColumnToIndex(event.detail.correctRow, event.detail.correctColumn);
try {
this._model.move(piece);
if (this._model.isSolved()) {
var listener;
listener = function() {
this._endGame();
this._view.removeEventListener('doneMoving', listener);
}.bind(this);
this._view.addEventListener('doneMoving', listener);
}
this._view.movePiece(event.detail.row, event.detail.column);
this._view.canUndo = true;
} catch (err) {
console.log(err);
}
}
SLIDE_PUZZLE.VideoController.prototype._undo = function() {
if (!this._undoing && !this._view.moving) {
this._undoing = true;
try {
var piece = this._model.getMoves().pop();
var index = this._model.getPositionOf(piece);
var rowCol = this._indexToRowColumn(index);
this._model.undo();
this._view.canUndo = this._model.getMoves().length !== 0;
this._view.movePiece(rowCol.row, rowCol.column);
} catch (err) {
console.log("can't undo: " + err.message);
}
}
this._undoing = false;
};
SLIDE_PUZZLE.VideoController.prototype._newGame = function() {
this._model = new SLIDE_PUZZLE.Model(this._rows, this._columns);
var modelPos = this._model.getPosition();
var viewPos = [];
for (var row = 0; row < this._rows; row++) {
viewPos.push([]);
for (var col = 0; col < this._columns; col++) {
var modelPosIndex = row * this._columns + col;
viewPos[row].push(modelPos[modelPosIndex]);
}
}
this._view.setBoard(viewPos);
this._view.showEmptyPiece = false;
this._view.canMovePieces = true;
this._view.canUndo = false;
if (this._stopwatch.isRunning) {
this._stopwatch.stop();
}
this._stopwatch.reset();
this._view.timerText = this._stopwatch.elapsedString;
this._stopwatch.start();
}
SLIDE_PUZZLE.VideoController.prototype._endGame = function() {
// TODO
this._stopwatch.stop();
this._view.showEmptyPiece = true;
this._view.canMovePieces = false;
this._view.canUndo = false;
console.log("end game");
}
SLIDE_PUZZLE.VideoController.prototype._rowColumnToIndex = function(row, column) {
return (row * this._columns) + column;
}
SLIDE_PUZZLE.VideoController.prototype._indexToRowColumn = function(index) {
var row = Math.floor(index / this._columns);
var column = index % this._columns;
return {row:row, column:column};
}

151
js/SlidePuzzleModel.js Normal file
View File

@ -0,0 +1,151 @@
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');
};

118
js/SlidePuzzleStopwatch.js Normal file
View File

@ -0,0 +1,118 @@
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
SLIDE_PUZZLE.Stopwatch = function(tickMillis) {
var self = this;
this._tickMillis = tickMillis;
this._startTime = null;
this._intervalID = null;
this._priorElapsed = 0;
this._currentElapsed = 0;
this._listeners = [];
this.__defineGetter__("isRunning", function() {
return self._intervalID !== null;
});
this.__defineGetter__("elapsedMillis", function() {
return self._priorElapsed + self._currentElapsed;
});
function divideInt(a, b) {
return Math.floor(a/b);
}
this.__defineGetter__("elapsedString", function() {
var totalMillis = self._priorElapsed + self._currentElapsed;
var seconds = divideInt(totalMillis, 1000) % 60;
var minutes = divideInt(totalMillis, 60000) % 60;
var hours = divideInt(totalMillis, 3600000);
var showHours = hours > 0;
var showMinutes = minutes > 0 || showHours;
var fixMinutes = showHours;
var fixSeconds = showMinutes;
var hoursString;
if (showHours) {
hoursString = hours + ':';
} else {
hoursString = '';
}
var minutesString;
if (showMinutes) {
if (fixMinutes && minutes < 10) {
minutesString = '0' + minutes + ':';
} else {
minutesString = minutes + ':';
}
} else {
minutesString = '';
}
var secondsString;
if (fixSeconds && seconds < 10) {
secondsString = '0' + seconds;
} else {
secondsString = seconds.toString();
}
return hoursString + minutesString + secondsString;
});
};
SLIDE_PUZZLE.Stopwatch.prototype.start = function() {
if (!this.isRunning) {
var self = this;
function tick() {
self._currentElapsed = window.performance.now() - self._startTime;
self._dispatchTickEvent();
}
this._startTime = window.performance.now();
this._intervalID = window.setInterval(tick, this._tickMillis);
} else {
throw new Error("can't start a started stopwatch");
}
};
SLIDE_PUZZLE.Stopwatch.prototype.stop = function() {
if (this.isRunning) {
window.clearInterval(this._intervalID);
this._intervalID = null;
this._startTime = null;
this._priorElapsed += this._currentElapsed;
this._currentElapsed = 0;
} else {
throw new Error("can't stop a stopped stopwatch");
}
};
SLIDE_PUZZLE.Stopwatch.prototype.reset = function() {
if (!this.isRunning) {
this._priorElapsed = 0;
this._currentElapsed = 0;
this._dispatchTickEvent();
} else {
throw new Error("can't reset a started stopwatch");
}
};
SLIDE_PUZZLE.Stopwatch.prototype.addEventListener = function(type, listener) {
if (type === 'tick') {
this._listeners.push(listener);
}
};
SLIDE_PUZZLE.Stopwatch.prototype._dispatchTickEvent = function() {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('tick', false, false, null);
for (var i = 0; i < this._listeners.length; i++) {
this._listeners[i](event);
}
}

369
js/SlidePuzzleView.js Normal file
View File

@ -0,0 +1,369 @@
var SLIDE_PUZZLE = window.SLIDE_PUZZLE || {};
SLIDE_PUZZLE.VideoView = function(rows, columns) {
var self = this;
this.FRAME_DELAY = 50;
this._rows = rows;
this._columns = columns;
this._videoListURL = "video/videolist.json";
this._videoList = null;
// DOM elements
this._container = document.getElementById('slidepuzzle-container');
this._board = document.getElementById('slidepuzzle-board');
this._video = document.getElementById('slidepuzzle-video');
this._copyright = document.getElementById('slidepuzzle-copyright');
this._videoSelect = document.getElementById('slidepuzzle-videolist');
this._timer = document.getElementById('slidepuzzle-timer');
this._newGameButton = document.getElementById('slidepuzzle-newgame');
this._undoButton = document.getElementById('slidepuzzle-undo');
// _origPosition is the unshuffled pieces, used for slicing the video.
// Its order never changes during a game
this._origPosition = [];
// _currPosition is the shuffled position of the pieces.
this._currPosition = [];
this._updateInterval = null;
this._moving = false;
this._listeners = {
pieceClicked: [],
undo: [],
newGame: [],
doneMoving: []
};
// VideoView.moving
this.__defineGetter__('moving', function() {
return this._moving;
});
// VideoView.timerText
this.__defineGetter__('timerText', function() {
return this._timer.textContent;
});
this.__defineSetter__('timerText', function(value) {
this._timer.textContent = value;
});
// VideoView.showEmptyPiece
this.__defineGetter__('showEmptyPiece', function() {
document.getElementById('slidepuzzle-empty-piece').classList.contains('hidden');
});
this.__defineSetter__('showEmptyPiece', function(value) {
var classes = document.getElementById('slidepuzzle-empty-piece').classList;
if (value) {
classes.remove('hidden');
} else {
classes.add('hidden')
}
});
// VideoView.canUndo
this.__defineGetter__('canUndo', function() {
return !this._undoButton.disabled;
});
this.__defineSetter__('canUndo', function(value) {
this._undoButton.disabled = !value;
});
// VideoView.canMovePieces
this.canMovePieces = true;
// Create gameboard cells
for (var i = 0; i < this._rows; i++) {
var row = document.createElement('tr');
for (var j = 0; j < this._columns; j++) {
row.appendChild(document.createElement('td'));
}
this._board.appendChild(row);
}
// Create puzzle pieces
for (var i = 0; i < this._rows; i++) {
this._currPosition.push([]);
this._origPosition.push([]);
for (var j = 0; j < this._columns; j++) {
var piece = document.createElement('canvas');
piece.dataset.correctRow = i;
piece.dataset.correctColumn = j;
if (i === this._rows - 1 && j === this._columns - 1) {
piece.id = 'slidepuzzle-empty-piece';
}
this._currPosition[i].push(piece);
this._origPosition[i].push(piece);
}
}
this._placePieces();
this.startUpdates();
function makePieceClickHandler(cellIndex) {
var row = Math.floor(cellIndex / self._columns);
var column = cellIndex % self._columns;
var listener = function() {
self._pieceClicked(row, column);
}
return listener;
}
var cells = this._board.getElementsByTagName("td");
for (var i = 0; i < cells.length; i++) {
cells[i].addEventListener('click', makePieceClickHandler(i));
}
this._videoSelect.addEventListener("change", function() {
if (self._videoList) {
console.log("selected video: " + self._videoSelect.value);
var name = self._videoSelect.value;
self.loadVideo(name);
}
});
this._loadVideoList();
this._undoButton.addEventListener('click', function() {
self._dispatchEvent('undo');
});
this._newGameButton.addEventListener('click', function() {
self._dispatchEvent('newGame');
});
};
SLIDE_PUZZLE.VideoView.prototype._loadVideoList = function() {
// Create an XMLHttpRequest to this._videoListURL.
// Parse the JSON and store the resulting object as this._videoList.
var self = this;
this._videoSelect.textContent = '';
this._videoList = {};
var req = new XMLHttpRequest();
//req.responseType = "json";
req.overrideMimeType("application/json");
req.onload = function() {
console.log("loaded video list");
var videos = JSON.parse(req.responseText);
for (var i = 0; i < videos.length; i++) {
var opt = document.createElement('option');
//opt.value = videos[i].url;
opt.text = videos[i].name;
self._videoSelect.appendChild(opt);
self._videoList[videos[i].name] = videos[i];
}
var changeEvent = document.createEvent("Event");
changeEvent.initEvent("change", true, true);
self._videoSelect.dispatchEvent(changeEvent);
};
req.onerror = function() {
console.log("error loading video list");
};
req.open("GET", this._videoListURL);
req.send();
}
SLIDE_PUZZLE.VideoView.prototype.addEventListener = function(type, listener) {
this._listeners[type].push(listener);
}
SLIDE_PUZZLE.VideoView.prototype.removeEventListener = function(type, listener) {
for (var i = 0; i < this._listeners[type].length; i++) {
if (this._listeners[type][i] === listener) {
this._listeners[type].splice(i, 1);
break;
}
}
}
SLIDE_PUZZLE.VideoView.prototype._dispatchEvent = function(type, detail) {
if (detail === undefined) {
detail = null;
console.log("detail is undefined");
}
var event = document.createEvent('CustomEvent');
event.initCustomEvent(type, false, false, detail);
for ( var i = 0; i < this._listeners[type].length; i++ ) {
this._listeners[type][i](event);
}
}
SLIDE_PUZZLE.VideoView.prototype._pieceClicked = function(row, column) {
var self = this;
if (!self._moving) {
var piece = self._currPosition[row][column];
var detail = {
row: row,
column: column,
correctRow: parseInt(piece.dataset.correctRow, 10),
correctColumn: parseInt(piece.dataset.correctColumn, 10),
};
self._dispatchEvent('pieceClicked', detail);
}
}
SLIDE_PUZZLE.VideoView.prototype.loadVideo = function(name) {
// We can't reference `this` normally in the closure unless we use bind, which we won't.
var self = this;
function setBoardSize() {
// The board will stretch to match the container.
self._container.style.height = self._video.videoHeight + "px";
self._container.style.width = self._video.videoWidth + "px";
self._board.style.height = self._container.style.height;
self._board.style.width = self._container.style.width;
for (var row = 0; row < self._rows; row++) {
for (var col = 0; col < self._columns; col++) {
var piece = self._currPosition[row][col];
piece.width = self._video.videoWidth / self._columns;
piece.height = self._video.videoHeight / self._rows;
}
}
}
this._video.onloadeddata = setBoardSize;
this._video.src = this._videoList[name].url;
var copyright = document.createElement('a');
copyright.href = this._videoList[name].infoUrl;
copyright.text = this._videoList[name].copyright;
this._copyright.textContent = '';
this._copyright.appendChild(copyright);
};
// setBoard arranges the pieces to the given position.
//
// boardPosition is a 2-dimensional array indexed by row, then column.
// The values in boardPosition are numbers representing the pieces'
// order in the solved puzzle, with the highest number being the empty space.
// E.g. a solved 4×4 puzzle is:
// [[0,1,2,3],
// [4,5,6,7],
// [8,9,10,11],
// [12,13,14,15]]
// boardPosition must have the proper dimensions and valid piece values,
// or bad things will happen.
// TODO rewrite setBoard to not use _origPosition
SLIDE_PUZZLE.VideoView.prototype.setBoard = function(boardPosition) {
for (var row = 0; row < this._rows; row++) {
for (var col = 0; col < this._columns; col++) {
var pieceNum = boardPosition[row][col];
var origRow = Math.floor(pieceNum / this._rows);
var origCol = pieceNum % this._columns;
this._currPosition[row][col] = this._origPosition[origRow][origCol];
}
}
this._placePieces();
}
SLIDE_PUZZLE.VideoView.prototype.movePiece = function(row, column) {
var self = this;
if (!self._moving && self.canMovePieces) {
self._moving = true;
var empty = this._getEmptyPosition();
var boardRect = this._board.getBoundingClientRect()
var movingPiece = this._currPosition[row][column];
var leftDistance = (empty.column - column) * boardRect.width / this._columns;
var topDistance = (empty.row - row) * boardRect.height / this._rows;
movingPiece.style.left = '0px';
movingPiece.style.top = '0px';
function animateMove() {
var moveFactor = 6; // Must be an integer
var left = parseInt(movingPiece.style.left, 10);
var top = parseInt(movingPiece.style.top, 10);
if (Math.abs(left) < Math.abs(leftDistance) && Math.abs(leftDistance - left) > Math.abs(leftDistance / moveFactor)) {
left += leftDistance / moveFactor;
movingPiece.style.left = left + "px";
window.setTimeout(animateMove, self.FRAME_DELAY);
} else if (Math.abs(top) < Math.abs(topDistance) && Math.abs(topDistance - top) > Math.abs(topDistance / moveFactor)) {
top += topDistance / moveFactor;
movingPiece.style.top = top + "px";
window.setTimeout(animateMove, self.FRAME_DELAY);
} else {
self._currPosition[row][column] = self._currPosition[empty.row][empty.column];
self._currPosition[empty.row][empty.column] = movingPiece;
movingPiece.style.left = '0px';
movingPiece.style.top = '0px';
self._placePieces();
self._moving = false;
self._dispatchEvent('doneMoving');
}
}
animateMove();
}
}
SLIDE_PUZZLE.VideoView.prototype.startUpdates = function() {
var self = this;
if (this._updateInterval === null) {
this._updateInterval = window.setInterval(function() {
self._updatePieces();
}, this.FRAME_DELAY);
}
}
SLIDE_PUZZLE.VideoView.prototype.stopUpdates = function() {
if (this._updateInterval !== null) {
window.clearInterval(this._updateInterval);
this._updateInterval = null;
}
}
SLIDE_PUZZLE.VideoView.prototype._getEmptyPosition = function() {
var pos = {};
for (var row = 0; row < this._rows; row++) {
for (var col = 0; col < this._columns; col++) {
if (this._currPosition[row][col].id === 'slidepuzzle-empty-piece') return {row:row,column:col};
}
}
throw new Error('no empty position found');
}
SLIDE_PUZZLE.VideoView.prototype._updatePieces = function() {
// If the pieces' videos end up out of sync, we can optimize
// this method by first taking a still of the video and then slicing it.
for (var row = 0; row < this._rows; row++) {
for (var col = 0; col < this._columns; col++) {
/*
var piece = this._origPosition[row][col];
var ctx = piece.getContext('2d');
var w = piece.width;
var h = piece.height;
var sourceX = col * w;
var sourceY = row * h;
ctx.drawImage(this._video, sourceX, sourceY, w, h, 0, 0, w, h);
*/
var piece = this._currPosition[row][col];
var origRow = parseInt(piece.dataset.correctRow, 10);
var origCol = parseInt(piece.dataset.correctColumn, 10);
var ctx = piece.getContext('2d');
var w = piece.width;
var h = piece.height;
var sourceX = origCol * w;
var sourceY = origRow * h;
ctx.drawImage(this._video, sourceX, sourceY, w, h, 0, 0, w, h);
}
}
}
SLIDE_PUZZLE.VideoView.prototype._placePieces = function() {
for (var row = 0; row < this._rows; row++) {
for (var col = 0; col < this._columns; col++) {
var cell = this._board.rows[row].cells[col];
cell.innerHTML = '';
var piece = this._currPosition[row][col];
cell.appendChild(piece);
}
}
}