Skip to content

Commit

Permalink
Use modules and put things in classes
Browse files Browse the repository at this point in the history
  • Loading branch information
ettolrach committed Dec 31, 2023
1 parent 0f4e19a commit 0160b2b
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 139 deletions.
3 changes: 1 addition & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ <h2 class="titles" id="notice">Please select a difficulty.</h2>
<button id="retry" class="clickableButton">Retry?</button>
</footer>

<script src="scripts/game.js"></script>
<script src="scripts/selection.js"></script>
<script src="scripts/main.js" type="module"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions scripts/choice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export class Choice {
constructor(id, width, height, selected = false) {
this.Element = document.getElementById(id);
this.Width = width;
this.Height = height;
this.Selected = selected;
}

Select() {
this.Selected = true;
this.Element.classList.add("activated")
}
Deselect() {
this.Selected = false;
this.Element.classList.remove("activated")
}
ChangeSelect() {
this.Selected = !this.Selected;
if (this.Element.classList.contains("activated")) {
this.Element.classList.remove("activated")
}
else {
this.Element.classList.add("activated")
}
}
}
6 changes: 6 additions & 0 deletions scripts/contents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This is an enum-like.
export const Contents = {
Empty: "Empty",
Mine: "Mine",
Num: "Num"
};
6 changes: 6 additions & 0 deletions scripts/flag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This is an enum-like.
export const Flag = {
Flag: "Flag",
QuestionMark: "QuestionMark",
Nothing: "Nothing"
};
111 changes: 0 additions & 111 deletions scripts/game.js
Original file line number Diff line number Diff line change
@@ -1,86 +1,3 @@
class Space {
constructor(revealed = false, contentsToGive = " ", id = "spaceXXX", flag = "") {
this.Revealed = revealed;
// The contents can either be " " or "M" which stand for an empty space and a mine respectively.
this.Contents = contentsToGive;
this.ID = id;
this.Flag = flag;
}

Reveal() {
this.Revealed = true;
let spaceToChange = document.getElementById(this.ID);
if(this.Contents == "M") {
spaceToChange.innerHTML = "<img src='mine.svg' style='width:95%'>";
}
else {
spaceToChange.innerText = this.Contents;
}
spaceToChange.style.backgroundColor = "#333";
spaceToChange.classList.add("activated");
}

ChangeID(newId) {
this.ID = newId;
}

ChangeContents(newContents) {
this.Contents = newContents;
}
ChangeFlag(newFlag) {
this.Flag = newFlag;
}
}

function InitialiseGrid(columns, rows) {
// If there are already spaces in the grid, get rid of them.
container.textContent = "";

// Make a list of Space objects that will be returned at the end.
spacesToReturn = [];

// Make the grid now as wide and tall as the game grid should be. This is done by changing the variables declared in the CSS.
container.style.setProperty("--gridColumns", columns);
container.style.setProperty("--gridRows", rows);

// Add as many spaces as required.
// Keep a running total of the number of mines.
minesNumber = 0;
// Make a 2D list, so this outer loop is for the rows.
for (let currentRow = 0; currentRow < rows; currentRow++) {
let rowToAppend = [];
for (let currentColumn = 0; currentColumn < columns; currentColumn++) {
// Decide whether this space should be a mine or not. Let the initial probability of the space being a mine be 1/4. Then, if there's not already too many mines, then make it a mine (in other words, if 1/4 of the board is filled with mines, then don't add any more). Otherwise, make it an empty space.
if(Math.floor(Math.random()*4) == 0 && minesNumber <= columns*rows/4) {
rowToAppend.push(new Space(false, "M"));
minesNumber++;
}
else {
rowToAppend.push(new Space(false, " "));
}
}

spacesToReturn.push(rowToAppend);
}

// Shuffle the array to make sure that the mines are spread evenly.
spacesToReturn = Shuffle(spacesToReturn);

return spacesToReturn;
}

function IsMine(spaces, x, y, columns, rows) {
if (x < 0 || y < 0) return;
if (x >= columns || y >= rows) return;
if (spaces[y][x].Contents == "M") {
return true;
}
else {
return false;
}

}

function AddToGrid(spacesToAdd, columns, rows) {
for (let currentRow = 0; currentRow < rows; currentRow++) {
for (let currentColumn = 0; currentColumn < columns; currentColumn++) {
Expand Down Expand Up @@ -230,33 +147,5 @@ function ShowAll(spaces, columns, rows) {
}
}

// Use the Fisher-Yates shuffle algorithm, minorly modified from the implementation available at https://github.com/Daplie/knuth-shuffle . Licensed under the Apache License 2.0 available at http://www.apache.org/licenses/ .
function Shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
}

function StartGame(width, height, container) {
let spaces = InitialiseGrid(width, height);
AddToGrid(spaces, width, height);
document.getElementById("secretShowAll").addEventListener("click", () => { ShowAll(spaces, width, height); });
container.style.display = "grid";
firstMove = true;
}

// A global variable is used here to keep track of whether the current move is the first one to have been made.
var firstMove = true;
6 changes: 6 additions & 0 deletions scripts/gameState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This is an enum-like.
export const GameState = {
InProgress: "InProgress",
Loss: "Loss",
Win: "Win"
};
197 changes: 197 additions & 0 deletions scripts/grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { Space } from "./space.js";
import { Contents } from "./contents.js";
import { GameState } from "./gameState.js";
import { Flag } from "./flag.js";

// Use the Fisher-Yates shuffle algorithm, minorly modified from the implementation available at https://github.com/Daplie/knuth-shuffle . Licensed under the Apache License 2.0 available at http://www.apache.org/licenses/ .
function Shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;

// While there remain elements to shuffle...
while (0 !== currentIndex) {

// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;

// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
}

export class Grid {
constructor(width, height) {
this.Width = width;
this.Height = height;
this.FirstMove = true;
this.State = GameState.InProgress;

const container = document.getElementById("gameContainer");
// If there are already spaces in the grid, get rid of them.
container.textContent = "";

this.Spaces = [];

// Make the grid in CSS as wide and tall as the game grid should be.
container.style.setProperty("--gridColumns", width);
container.style.setProperty("--gridRows", height);

// Add as many spaces as required.
let minesNumber = 0;
for (let i = 0; i < width * height; i++) {
// Decide whether this space should be a mine or not.
// Let the initial probability of the space being a mine be 1/6.
// Then, unless there's too many mines, then make it a mine.
// In other words, if 1/4 of the board is filled with mines, then don't add any more;
// otherwise, make it an empty space.
if(Math.floor(Math.random()*6) == 0 && minesNumber <= (width * height) / 4) {
this.Spaces.push(new Space(false, Contents.Mine, `space${i}`));
minesNumber++;
}
else {
this.Spaces.push(new Space(false, Contents.Empty, `space${i}`));
}

}
// Shuffle the array to make sure that the mines are spread evenly.
this.Spaces = Shuffle(this.Spaces);
}
GetIndex(x, y) {
return x + this.Width * y;
}
GetIndexFromArr(arr) {
return arr[0] + this.Width * arr[1];
}
GetCoordinates(index) {
return [index % this.Width, Math.floor(index / this.Width)];
}
InBoundsArr([a, b]) {
return a >= 0 && b >= 0 && a < this.Width && b < this.Height;
}
NeighboursInBounds(index) {
let [x, y] = this.GetCoordinates(index);
let neighbours = [
[x + 1, y],
[x + 1, y + 1],
[x, y + 1],
[x - 1, y + 1],
[x - 1, y],
[x - 1, y - 1],
[x, y - 1],
[x + 1, y - 1],
];
return neighbours
.filter(neighbour => this.InBoundsArr(neighbour))
.map(arr => this.GetIndexFromArr(arr));
}
CountNeighbouringMines(index) {
return this.NeighboursInBounds(index)
.map(i => this.Spaces[i].Contents)
.filter(content => content == Contents.Mine)
.length;
}
FloodUncover(index) {
if (this.Spaces[index].Revealed) {
return;
}
switch (this.Spaces[index].Contents) {
case Contents.Mine:
this.State = GameState.Loss;
break;
case Contents.Num:
this.Spaces[index].Reveal();
break;
case Contents.Empty:
this.Spaces[index].Reveal();
// Go clockwise around the current space revealing all adjacent ones.
for (const neighbour of this.NeighboursInBounds(index)) {
this.FloodUncover(neighbour);
}
break;
default:
console.error(`Switch not exhaustive! Tried to switch on: ${this.Spaces[index].Contents}`);
break;
}
}
AllUncovered() {
return this.Spaces
.every(space => space.Revealed)
}
CoveredSpaces() {
return this.Spaces
.filter(space => !space.Revealed)
}
ShowAll() {
for (let i = 0; i < this.Spaces.length; i++) {
this.Spaces[i].Reveal();
}
}
Clicked(index) {
if(this.FirstMove == true) {
this.FirstMove = false;
// Make sure that the first move is not a mine and has no mines around it.
this.Spaces[index].ChangeContents(Contents.Empty);
let neighbours = this.NeighboursInBounds(index);
for (const i of this.NeighboursInBounds(index)) {
this.Spaces[i].ChangeContents(Contents.Empty);
}
// Fill the board with numbers.
for (let j = 0; j < this.Spaces.length; j++) {
// If the current space is a mine, then there's no need to put on a number.
if (this.Spaces[j].Contents == Contents.Mine) {
continue;
}
let numberToDisplay = this.CountNeighbouringMines(j);
if (numberToDisplay != 0) {
this.Spaces[j].SetNumber(numberToDisplay);
}
}
}

// Start the recursive flood uncover.
// This WILL turn the game state to loss if a mine was revealed.
this.FloodUncover(index);
// Check if the game is lost.
if (this.State == GameState.Loss) {
// TODO end the game with a loss.
document.getElementById("subtitle").innerText = "You lose!";
document.getElementById("subtitle").style.color = "red";
document.getElementById("retry").style.display = "block";
this.ShowAll();
}
// If only mines aren't revealed yet, then the game is won.
else if (this.CoveredSpaces().every(space => space.Contents == Contents.Mine)) {
this.State == GameState.Win;
document.getElementById("subtitle").innerText = "You win!";
document.getElementById("subtitle").style.color = "green";
document.getElementById("retry").style.display = "block";
this.ShowAll()
}
}
PlaceFlag(index) {
if (this.Spaces[index].Revealed) {
return;
}
switch (this.Spaces[index].Flag) {
case Flag.Nothing:
document.getElementById(this.Spaces[index].ID).innerHTML = "<img src='media/flag.svg' style='width:65%;'>";
this.Spaces[index].ChangeFlag(Flag.Flag);
break;
case Flag.Flag:
document.getElementById(this.Spaces[index].ID).innerHTML = "<p style='color: lightblue; padding: 0; margin: 0;'>?</p>";
this.Spaces[index].ChangeFlag(Flag.QuestionMark);
break;
case Flag.QuestionMark:
document.getElementById(this.Spaces[index].ID).innerHTML = "";
this.Spaces[index].ChangeFlag(Flag.Nothing);
break;
default:
console.error(`Switch not exhaustive! Tried to switch on: ${this.Spaces[index].Flag}`);
break;
}
}
}
Loading

0 comments on commit 0160b2b

Please sign in to comment.