minesweeper/ms.js

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