355 lines
9.3 KiB
JavaScript
355 lines
9.3 KiB
JavaScript
class Minesweeper {
|
|
//Minesweeper constructor to represent the game
|
|
constructor(opts = {}) {
|
|
let loadedData = {};
|
|
|
|
Object.assign(
|
|
this,
|
|
{
|
|
grid: [], //will hold an array of Cell objects
|
|
minesFound: 0, //number of mines correctly flagged by user
|
|
falseMines: 0, //number of mines incorrectly flagged
|
|
status_msg: "Playing...", //game status msg, 'Won','Lost', or 'Playing'
|
|
playing: true,
|
|
movesMade: 0, //keep track of the number of moves
|
|
options: {
|
|
rows: 8, //number of rows in the grid
|
|
cols: 8, //number of columns in the grid
|
|
mines: 10 //number of mines in the grid
|
|
} },
|
|
|
|
{ options: opts },
|
|
loadedData);
|
|
|
|
this.init();
|
|
}
|
|
|
|
//setup the game grid
|
|
init() {
|
|
//populate the grid with cells
|
|
for (let r = 0; r < this.options["rows"]; r++) {
|
|
this.grid[r] = [];
|
|
for (let c = 0; c < this.options["cols"]; c++) {
|
|
this.grid[r].push(new Cell({ xpos: c, ypos: r }));
|
|
}
|
|
}
|
|
|
|
//randomly assign mines
|
|
let assignedMines = 0;
|
|
while (assignedMines < this.options.mines) {
|
|
var rowIndex = Math.floor(Math.random() * this.options.rows);
|
|
var colIndex = Math.floor(Math.random() * this.options.cols);
|
|
//assign and increment if cell is not already a mine
|
|
let cell = this.grid[rowIndex][colIndex];
|
|
if (!cell.isMine) {
|
|
cell.isMine = true;
|
|
cell.value = "M";
|
|
assignedMines++;
|
|
}
|
|
}
|
|
|
|
//update cell values, check for adjacent mines
|
|
for (let r = 0; r < this.options["rows"]; r++) {
|
|
for (let c = 0; c < this.options["cols"]; c++) {
|
|
//no need to update mines
|
|
if (!this.grid[r][c].isMine) {
|
|
let mineCount = 0,
|
|
adjCells = this.getAdjacentCells(r, c);
|
|
for (let i = adjCells.length; i--;) {
|
|
if (adjCells[i].isMine) {
|
|
mineCount++;
|
|
}
|
|
}
|
|
|
|
this.grid[r][c].value = mineCount;
|
|
}
|
|
}
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
//game was lost
|
|
gameLost() {
|
|
const gameStatus = document.getElementById("game_status");
|
|
|
|
this.status_msg = "Sorry, you lost!";
|
|
this.playing = false;
|
|
stopTimer();
|
|
gameStatus.textContent = this.status_msg;
|
|
gameStatus.style.color = "#EE0000";
|
|
}
|
|
|
|
//game was won
|
|
gameWon() {
|
|
const gameStatus = document.getElementById("game_status");
|
|
|
|
this.status_msg = "You won!!";
|
|
this.playing = false;
|
|
gameStatus.textContent = this.status_msg;
|
|
gameStatus.style.color = "#00CC00";
|
|
}
|
|
|
|
//construct the DOM representing the grid
|
|
render() {
|
|
const gameContainer = document.getElementById("game_container");
|
|
|
|
//clear old DOM
|
|
gameContainer.innerHTML = "";
|
|
|
|
let content = "";
|
|
|
|
//create status bar
|
|
content += '<div class="status_row">';
|
|
content += `<div class="mine_count" id="mine_count" title="Mine count">10</div>`;
|
|
content += `<div class="moves_made" id="moves_made" title="Moves made">0</div>`;
|
|
content += `<div class="timer" id="timer" title="Time remaining">0</div>`;
|
|
content += "</div>";
|
|
|
|
//create cells
|
|
for (let r = 0; r < this.options.rows; r++) {
|
|
content += '<div class="row">';
|
|
for (let c = 0; c < this.options.cols; c++) {
|
|
let cellObj = this.grid[r][c];
|
|
|
|
//assign proper text and class to cells (needed when loading a game)
|
|
let add_class = "",
|
|
txt = "";
|
|
if (cellObj.isFlagged) {
|
|
add_class = "flagged";
|
|
} else if (cellObj.isRevealed) {
|
|
add_class = `revealed adj-${cellObj.value}`;
|
|
txt = !cellObj.isMine ? cellObj.value || "" : "";
|
|
}
|
|
|
|
content += `<div class="cell ${add_class}" data-xpos="${c}" data-ypos="${r}">${txt}</div>`;
|
|
}
|
|
content += "</div>";
|
|
}
|
|
|
|
gameContainer.innerHTML = content;
|
|
|
|
//setup status message
|
|
document.getElementById("mine_count").textContent =
|
|
this.options["mines"] - (this.falseMines + this.minesFound);
|
|
document.getElementById("moves_made").textContent = this.movesMade;
|
|
document.getElementById("game_status").textContent = this.status_msg;
|
|
document.getElementById("game_status").style.color = "black";
|
|
}
|
|
|
|
//returns an array of cells adjacent to the row,col passed in
|
|
getAdjacentCells(row, col) {
|
|
let results = [];
|
|
for (
|
|
let rowPos = row > 0 ? -1 : 0;
|
|
rowPos <= (row < this.options.rows - 1 ? 1 : 0);
|
|
rowPos++)
|
|
{
|
|
for (
|
|
let colPos = col > 0 ? -1 : 0;
|
|
colPos <= (col < this.options.cols - 1 ? 1 : 0);
|
|
colPos++)
|
|
{
|
|
results.push(this.grid[row + rowPos][col + colPos]);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
//reveal a cell
|
|
revealCell(cell) {
|
|
if (!cell.isRevealed && !cell.isFlagged && this.playing) {
|
|
const cellElement = cell.getElement();
|
|
|
|
cell.isRevealed = true;
|
|
cellElement.classList.add("revealed", `adj-${cell.value}`);
|
|
cellElement.textContent = !cell.isMine ? cell.value || "" : "";
|
|
|
|
//end the game if user clicked a mine
|
|
if (cell.isMine) {
|
|
this.gameLost();
|
|
} else if (!cell.isFlagged && cell.value == 0) {
|
|
//if the clicked cell has 0 adjacent mines, we need to recurse to clear out all adjacent 0 cells
|
|
const adjCells = this.getAdjacentCells(cell.ypos, cell.xpos);
|
|
for (let i = 0, len = adjCells.length; i < len; i++) {
|
|
this.revealCell(adjCells[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//flag a cell
|
|
flagCell(cell) {
|
|
if (!cell.isRevealed && this.playing) {
|
|
const cellElement = cell.getElement(),
|
|
mineCount = document.getElementById("mine_count");
|
|
|
|
if (!cell.isFlagged) {
|
|
cell.isFlagged = true;
|
|
cellElement.classList.add("flagged");
|
|
mineCount.textContent = parseFloat(mineCount.textContent) - 1;
|
|
if (cell.isMine) {
|
|
this.minesFound++;
|
|
} else {
|
|
this.falseMines++;
|
|
}
|
|
} else {
|
|
cell.isFlagged = false;
|
|
cellElement.classList.remove("flagged");
|
|
cellElement.textContent = "";
|
|
mineCount.textContent = parseFloat(mineCount.textContent) + 1;
|
|
if (cell.isMine) {
|
|
this.minesFound--;
|
|
} else {
|
|
this.falseMines--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//check if player has won the game
|
|
validate() {
|
|
if (this.minesFound === this.options.mines && this.falseMines === 0) {
|
|
this.gameWon()
|
|
} else {
|
|
this.gameLost();
|
|
}
|
|
}
|
|
|
|
//debugging function to print the grid to console
|
|
gridToString() {
|
|
let result = "";
|
|
for (let r = 0, r_len = this.grid.length; r < r_len; r++) {
|
|
for (let c = 0, c_len = this.grid[r].length; c < c_len; c++) {
|
|
result += this.grid[r][c].value + " ";
|
|
}
|
|
result += "\n";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//Cell constructor to represent a cell object in the grid
|
|
class Cell {
|
|
constructor({
|
|
xpos,
|
|
ypos,
|
|
value = 0,
|
|
isMine = false,
|
|
isRevealed = false,
|
|
isFlagged = false })
|
|
{
|
|
Object.assign(this, {
|
|
xpos,
|
|
ypos,
|
|
value, //value of a cell: number of adjacent mines, F for flagged, M for mine
|
|
isMine,
|
|
isRevealed,
|
|
isFlagged });
|
|
|
|
}
|
|
|
|
getElement() {
|
|
return document.querySelector(
|
|
`.cell[data-xpos="${this.xpos}"][data-ypos="${this.ypos}"]`);
|
|
|
|
}}
|
|
|
|
|
|
//create a new game
|
|
function newGame(opts = {}) {
|
|
game = new Minesweeper(opts);
|
|
startTimer()
|
|
|
|
console.log(game.gridToString());
|
|
}
|
|
|
|
//stop the timer
|
|
function stopTimer() {
|
|
clearInterval(countdown);
|
|
}
|
|
|
|
//start the timer
|
|
function startTimer() {
|
|
stopTimer();
|
|
document.getElementById('timer').textContent = 70;
|
|
countdown = setInterval(updateTimer, 1000);
|
|
}
|
|
|
|
//update remaining time every second and at the end validate game
|
|
function updateTimer() {
|
|
const timerDisplay = document.getElementById('timer');
|
|
|
|
const currentTime = parseInt(timerDisplay.textContent, 10);
|
|
if (currentTime > 0) {
|
|
timerDisplay.textContent = currentTime - 1;
|
|
// Call your function here every second
|
|
} else {
|
|
stopTimer();
|
|
game.validate()
|
|
alert('Time run out!');
|
|
}
|
|
}
|
|
|
|
//create new game
|
|
function createNewGame() {
|
|
const opts = {
|
|
rows: 8,
|
|
cols: 8,
|
|
mines: 10 };
|
|
|
|
newGame(opts);
|
|
}
|
|
|
|
window.onload = function () {
|
|
//attach click event to cells - left click to reveal
|
|
document.getElementById("game_container").addEventListener("click", function (e) {
|
|
const target = e.target;
|
|
|
|
if (target.classList.contains("cell")) {
|
|
const cell =
|
|
game.grid[target.getAttribute("data-ypos")][
|
|
target.getAttribute("data-xpos")];
|
|
|
|
|
|
if (!cell.isRevealed && game.playing) {
|
|
game.movesMade++;
|
|
document.getElementById("moves_made").textContent = game.movesMade;
|
|
game.revealCell(cell);
|
|
}
|
|
}
|
|
});
|
|
|
|
//right click to flag
|
|
document.getElementById("game_container").addEventListener("contextmenu", function (e) {
|
|
e.preventDefault();
|
|
const target = e.target;
|
|
|
|
if (target.classList.contains("cell")) {
|
|
const cell =
|
|
game.grid[target.getAttribute("data-ypos")][
|
|
target.getAttribute("data-xpos")];
|
|
|
|
if (!cell.isRevealed && game.playing) {
|
|
game.movesMade++;
|
|
document.getElementById("moves_made").textContent = game.movesMade;
|
|
game.flagCell(cell);
|
|
}
|
|
}
|
|
});
|
|
|
|
//attach click to validate button
|
|
document.getElementById("validate_button").addEventListener("click", function () {
|
|
game.validate();
|
|
});
|
|
|
|
//create a game
|
|
createNewGame();
|
|
};
|
|
|
|
//global vars
|
|
var game;
|
|
var countdown
|
|
|