init the awkward code

This commit is contained in:
Bao Nguyen
2023-02-13 19:32:10 +07:00
commit 27170afcac
5426 changed files with 1244579 additions and 0 deletions

View File

@@ -0,0 +1,514 @@
/**
* Constructor for a Minesweeper game object.
*
* @param {String} containerId HTML ID for an empty element to contain this
* minesweeper game's display.
*/
var Minesweeper = function(containerId) {
this.mainContainer = $(containerId);
this.MINE = '\uD83D\uDCA3'; // bomb emoji
this.FLAG = '\uD83D\uDEA9'; // triangle flag emoji
this.MAX_TIME = 999;
this.SETTINGS = {
BEGINNER: {
rows: 8,
cols: 8,
mines: 10
},
INTERMEDIATE: {
rows: 16,
cols: 16,
mines: 40
},
EXPERT: {
rows: 16,
cols: 30,
mines: 99
}
};
};
/**
* Attaches Minesweeper display to screen and sets up click listeners.
*/
Minesweeper.prototype.init = function(settings) {
this.debug = false;
this.settings = settings;
this.won = false;
this.lost = false;
this.firstClickOccurred = false;
this.cellsRevealed = 0;
this.cellsFlagged = 0;
this.cellsToReveal = (settings.rows * settings.cols) - settings.mines;
this.elapsedTime = 0;
clearInterval(this.timeInterval);
this.misclickCount = 0;
this.initField(settings.rows, settings.cols);
this.initDisplay();
};
/**
* Only generates a 2-D array with the given rows and cols as dimensions; does
* not actually add mines to field. Mines are added after the first click.
*
* Each cell is an object with two properties: val and flagged. Each val is
* either MINE or a number representing how many MINEs are adjacent to that
* cell. flagged starts out as false and is toggled when the user marks a cell
* as a flag.
*/
Minesweeper.prototype.initField = function(rows, cols) {
// Initialize 2-D array
this.field = [];
for (var i = 0; i < rows; i++) {
var row = new Array(cols);
for (var j = 0; j < cols; j++) {
row[j] = { val: 0, flagged: false };
}
this.field.push(row);
}
};
/**
* Updates this.field to contain the number of mines requested.
*
* Should be called after the user has made their first click so that mines can
* be placed while avoiding that location.
*/
Minesweeper.prototype.setMines = function(mines, firstClickRow, firstClickCol) {
var field = this.field;
var rows = field.length;
var cols = field[0].length;
// Must count mines actually planted so that mines are not placed in
// previously selected locations.
var minesPlanted = 0;
while (minesPlanted != mines) {
var row = Math.floor(Math.random() * rows);
var col = Math.floor(Math.random() * cols);
// Cannot use cell if the user just clicked it or if it was already a mine
if ((row == firstClickRow && col == firstClickCol)
|| (field[row][col] && field[row][col].val == this.MINE)) continue;
field[row][col] = {
val: this.MINE,
flagged: false
};
minesPlanted++;
}
// Fill field with mine counts
for (var i = 0; i < rows; i++) {
for (var j = 0; j < cols; j++) {
field[i][j].val = this.countAdjacentMines(i, j);
}
}
// console.log(field);
};
/**
* For a given coordinate, returns the number of mines it is next to.
*/
Minesweeper.prototype.countAdjacentMines = function(row, col) {
var field = this.field;
if (field[row][col] && field[row][col].val == this.MINE) return this.MINE;
var count = 0;
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
var r = neighbors[i].row;
var c = neighbors[i].col;
if (field[r][c] && field[r][c].val == this.MINE) count++;
}
return count;
};
/**
* Every cell calls this click handler the first time it's clicked. If it was
* the first cell to be clicked (flagged cells do not count), this finally adds
* mines to the screen and excludes this click. Otherwise, a first click was
* already made, so we can just change this cell's click handler for the
* duration of the game.
*/
Minesweeper.prototype.firstClickHandler = function(row, col) {
if (this.field[row][col].flagged) return;
if (this.firstClickOccurred) {
var that = this;
var cell = this.getCell(row, col);
cell.unbind('click');
cell.click(function(event) {
that.mainClickHandler(row, col);
});
} else {
this.firstClickOccurred = true;
this.setMines(this.settings.mines, row, col);
this.initDisplay();
this.startTimer();
}
this.mainClickHandler(row, col);
};
/**
* Adds HTML table for this object's already initialized field. Control panel is
* added after mines are added in order to set width correctly.
*/
Minesweeper.prototype.initDisplay = function() {
// for resetting the game
if (this.display) this.display.empty();
this.display = $('#game-container');
this.gameTable = $(document.createElement('table'));
this.gameTable.addClass('no-highlight inset');
this.gameTable.attr('cellspacing', 0);
var that = this;
this.field.forEach(function(row, r) {
var tr = document.createElement('tr');
row.forEach(function(cell, c) {
var td = $(document.createElement('td'));
td.html(that.createElemForValue(that.field[r][c].val));
// left click
td.click(function(event) {
that.firstClickHandler(r, c);
});
// right click
td.get(0).oncontextmenu = function(event) {
event.preventDefault();
that.toggleFlag(r, c);
};
// styling
td.addClass('cell outset');
if (that.debug) td.addClass('debug');
if (that.field[r][c].flagged) {
td.addClass('flagged');
td.html(that.FLAG);
}
tr.appendChild(td.get(0));
});
that.gameTable.append(tr);
});
this.display.prepend(this.gameTable);
this.initControlPanel();
this.initHelper();
};
/**
* Control panel is the top portion that has the timer, mine count, and reset
* button. This adds those components to the top of the container. Must be
* called after the game cells are added so that the total width can be used.
*/
Minesweeper.prototype.initControlPanel = function() {
var that = this;
this.controlPanel = $(document.createElement('div'));
this.controlPanel.resetButton = $(document.createElement('div'));
this.controlPanel.flagCount = $(document.createElement('div'));
this.controlPanel.timer = $(document.createElement('div'));
var controlPanel = this.controlPanel;
var resetButton = this.controlPanel.resetButton;
resetButton.html('🙂');
var flagCount = this.controlPanel.flagCount;
var timer = this.controlPanel.timer;
controlPanel.append(resetButton);
this.display.prepend(controlPanel);
// overall panel styling
controlPanel.addClass('control-panel inset');
// debug link styling
var debugLink = $(document.createElement('a'));
debugLink.html('debug');
debugLink.click(this.toggleDebug);
debugLink.addClass('debug-link');
this.display.append(debugLink);
// reset button styling and clicks
resetButton.addClass('reset-button outset');
resetButton.css('margin-left',
(controlPanel.innerWidth() - resetButton.width()) / 2);
resetButton.click(function(event) {
that.init(that.settings);
});
// counter for mines left
controlPanel.prepend(flagCount);
flagCount.addClass('counter');
flagCount.html(this.zeroFill(this.settings.mines, 2));
flagCount.css('float', 'left');
// counter for time elapsed
controlPanel.prepend(timer);
timer.addClass('counter');
timer.html(this.zeroFill(0));
timer.css('float', 'right');
timer.css('text-align', 'right');
};
Minesweeper.prototype.initHelper = function() {
};
/**
* Returns the HTML element for this cell.
*/
Minesweeper.prototype.getCell = function(row, col) {
return $(this.gameTable[0].rows[row].cells[col]);
};
/**
* Click (left) usually means reveal the cell selected. For revealed cells,
* behavior depends on its neighbors. If the number of unrevealed neighbors is
* equal to this cell's value, flag all of them. If the user has already flagged
* exactly as many mines as this cell's value, expand the rest (potentially
* resulting in a loss).
*/
Minesweeper.prototype.mainClickHandler = function(row, col) {
var field = this.field;
// do nothing if already lost or if a flagged cell is clicked
if (this.gameEnded() || field[row][col].flagged) return;
if (!this.getCell(row, col).hasClass('revealed')) {
this.revealCell(row, col);
} else if (this.flagAllNeighborsRequested(row, col)) {
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
this.toggleFlag(neighbors[i].row, neighbors[i].col, true);
}
} else if (this.expandRequested(row, col)) {
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
this.revealCell(neighbors[i].row, neighbors[i].col);
if (this.gameEnded()) return;
}
} else {
this.misclickCount++;
this.misclickDisplay.html(this.misclickCount);
}
};
/**
* Reveals cell at (row, col), then recursively expands its neighbors if it
* doesn't have any neighboring mines (i.e., its field value is 0).
*
* It is possible to lose if the user manually tries to expand all neighbors on
* a cell by incorrectly flagging neighboring cells (see mainClickHandler);
* otherwise, it shouldn't be possible for this to result in a loss.
*/
Minesweeper.prototype.revealCell = function(row, col) {
// base case: don't reveal flagged or already revealed cells
if (this.field[row][col].flagged
|| this.getCell(row, col).hasClass('revealed')) return;
this.revealSingleCell(row, col);
// base case: stop expanding if cell is non-zero or its reveal lead to a loss
if (this.gameEnded() || this.field[row][col].val != 0) return;
// recursive step: reveal neighbors
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
this.revealCell(neighbors[i].row, neighbors[i].col);
if (this.gameEnded()) return;
}
};
/**
* Updates styling for a cell so that it shows its number on the screen.
* Checks whether this reveal resulted in a win or loss.
*/
Minesweeper.prototype.revealSingleCell = function(row, col) {
var displayCell = this.getCell(row, col);
var cell = this.field[row][col];
if (displayCell.hasClass('revealed') || cell.flagged) return;
displayCell.html(this.createElemForValue(cell.val));
displayCell.addClass('revealed cell-' + (cell.val == this.MINE ? 'X' : cell.val));
displayCell.removeClass('outset');
if (this.debug) displayCell.removeClass('debug');
this.cellsRevealed++;
if (cell.val == this.MINE) {
this.displayLoss();
} else if (this.cellsRevealed == this.cellsToReveal) {
this.displayWin();
}
};
/**
* Returns a list of coordinates for cells adjacent to this row and column.
*/
Minesweeper.prototype.getNeighbors = function(row, col) {
var field = this.field;
var neighbors = [];
for (var r = row - 1; r <= row + 1; r++) {
for (var c = col - 1; c <= col + 1; c++) {
if (0 <= r && r < field.length
&& 0 <= c && c < field[0].length
&& !(r == row && c == col)) {
neighbors.push({row: r, col: c});
}
}
}
return neighbors;
};
/**
* Non-traditional feature: If user clicks on a revealed cell and it has exactly
* as many unrevealed cells as its mine count, this returns true so that all of
* its cells can be flagged immediately.
*/
Minesweeper.prototype.flagAllNeighborsRequested = function(row, col) {
if (!this.getCell(row, col).hasClass('revealed')) return false;
var unrevealedCount = 0;
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
var neighbor = this.getCell(neighbors[i].row, neighbors[i].col);
if (!neighbor.hasClass('revealed')) {
unrevealedCount++;
}
}
return unrevealedCount == this.field[row][col].val;
};
/**
* A valid expansion request is one where the cell clicked has already been
* revealed and has exactly as many flagged neighbors as its own value.
*/
Minesweeper.prototype.expandRequested = function(row, col) {
if (!this.getCell(row, col).hasClass('revealed')) return false;
var flagCount = 0;
var neighbors = this.getNeighbors(row, col);
for (var i = 0; i < neighbors.length; i++) {
var r = neighbors[i].row;
var c = neighbors[i].col;
var neighbor = this.getCell(r, c);
if (!neighbor.hasClass('revealed') && this.field[r][c].flagged) {
flagCount++;
}
}
return flagCount == this.field[row][col].val;
};
/**
* Switches styling to display a flagged or unflagged cell.
* @param {Boolean} forceFlag allows an already-flagged cell to stay flagged.
*/
Minesweeper.prototype.toggleFlag = function(row, col, forceFlag) {
var displayCell = this.getCell(row, col);
if (this.gameEnded() || displayCell.hasClass('revealed')) return;
var cell = this.field[row][col];
if (!this.field[row][col].flagged) {
displayCell.addClass('flagged');
displayCell.html(this.createElemForValue(this.FLAG));
this.cellsFlagged++;
cell.flagged = true;
} else if (!forceFlag) {
displayCell.removeClass('flagged');
displayCell.html(this.createElemForValue(cell.val));
this.cellsFlagged--;
cell.flagged = false;
}
this.controlPanel.flagCount.html(this.settings.mines - this.cellsFlagged);
};
/**
* Starts timer so that display clock ticks every second.
*/
Minesweeper.prototype.startTimer = function() {
var that = this;
this.timeInterval = setInterval(function() {
that.controlPanel.timer.html(++that.elapsedTime);
if (that.elapsedTime == that.MAX_TIME)
clearInterval(that.timeInterval);
}, 1000);
};
/**
* Stops timer and shows win message.
*/
Minesweeper.prototype.displayWin = function() {
this.won = true;
this.controlPanel.resetButton.html('😎');
clearInterval(this.timeInterval);
};
/**
* Reveals all unflagged mines, shows loss message, and sets this.lost to true.
*/
Minesweeper.prototype.displayLoss = function() {
if (this.lost) return;
this.lost = true;
this.controlPanel.resetButton.html('😵'); // dizzy face emoji
clearInterval(this.timeInterval);
for (var r = 0; r < this.field.length; r++) {
for (var c = 0; c < this.field[0].length; c++) {
var cell = this.field[r][c];
if (cell.val == this.MINE && !cell.flagged) {
this.revealSingleCell(r, c);
}
}
}
};
/**
* For debugging purposes, allow the user to continue playing as if they haven't
* lost, even if mines have been revealed.
*/
Minesweeper.prototype.gameEnded = function() {
return !this.debug && (this.won || this.lost);
};
/**
* Returns the given value as a string padded with zeroes up to length.
*/
Minesweeper.prototype.zeroFill = function(value, length) {
if (length === undefined) length = 3;
return value;
};
/**
* When called, reveals (or hides) values of all cells.
*/
Minesweeper.prototype.toggleDebug = function() {
this.debug = !this.debug;
$('.cell:not(.revealed)').toggleClass('debug');
};
/**
* Creates the div that must wrap cell values for min-width/height reasons.
*/
Minesweeper.prototype.createElemForValue = function(val) {
var div = $(document.createElement('div'));
if (val == this.MINE) {
div.addClass('mine');
}
div.html(val);
return div.get(0);
}