Files
minesweeper.xdc/game.js
T
2024-10-04 18:12:09 +02:00

265 lines
8.3 KiB
JavaScript

const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
let current_level = 1;
let initial_mines_count = 10;
let field_width = 8;
let field_height = 8;
let mines_placed = false;
let game_finished = false;
let ui_timer;
let seconds_elapsed = 0;
let sound_reveal = new Audio("./res/reveal.wav");
let sound_mark = new Audio("./res/mark.wav");
// Game state saving.
window.addEventListener("beforeunload", function() {
if (mines_placed && !game_finished) {
// We need to save: the minefield, the elapsed time. These also
// get prefixed by the current level.
localStorage.setItem(current_level + "-minefield_html", $("#minefield").innerHTML);
localStorage.setItem(current_level + "-seconds_elapsed", seconds_elapsed);
} else {
localStorage.removeItem(current_level + "-minefield_html");
localStorage.removeItem(current_level + "-seconds_elapsed");
}
});
function restart_game() {
game_finished = false;
mines_placed = false;
field_height = 8 + Math.floor(current_level / 2);
minefield_initialize();
update_particle_canvas_size();
// The amount of mines, meanwhile, increases by one I
// think. Just a quick thought. This makes it so the amount of
// mines is around four times less than the amount of cells.
initial_mines_count = 10 + current_level;
stop_timing();
seconds_elapsed = 0;
document.body.classList.remove("main-menu", "popup-victory", "popup-failure");
document.body.classList.add ("game-view");
$("#time").innerText = get_seconds_readable(seconds_elapsed);
}
function start_or_continue_game(level) {
// Based on the level, we calculate the grid size and amount
// of mines. The height of the grid increases every two
// levels.
current_level = level;
field_height = 8 + Math.floor(current_level / 2);
let minefield_html = localStorage.getItem(current_level + "-minefield_html");
$("#level").innerText = current_level;
if (minefield_html == null) {
restart_game();
} else {
// If we have a minefield in localStorage, load it back into
// the DOM.
$("#minefield").innerHTML = minefield_html;
// We need to re-add events to the cells.
for (let y = 0; y < field_height; y++) {
for (let x = 0; x < field_width; x++) {
let cell = $("#c" + x + "-" + y);
cell.onclick = cell_onclick.bind(cell);
cell.oncontextmenu = cell_oncontextmenu.bind(cell);
cell.x = x;
cell.y = y;
}
}
mines_placed = true;
seconds_elapsed = parseInt(localStorage.getItem(current_level + "-seconds_elapsed"));
$("#time").innerText = get_seconds_readable(seconds_elapsed);
start_timing();
}
}
function get_seconds_readable(seconds) {
let date = new Date(null);
date.setSeconds(seconds);
return date.toISOString().slice(14, 19);
}
function start_timing() {
ui_timer = window.setInterval(function() {
seconds_elapsed += 1;
$("#time").innerText = get_seconds_readable(seconds_elapsed);
}, 1000);
}
function stop_timing() {
window.clearInterval(ui_timer);
}
function cell_onclick() {
if (!mines_placed) {
place_mines(this.x, this.y);
start_timing();
}
if (this.classList.contains("unknown") && !this.classList.contains("flagged")) {
if (this.classList.contains("mine")) {
this.classList.remove("mine");
this.classList.add("exploded");
lose_game();
return;
} else {
reveal_mines(this);
}
check_victory();
} else if (this.classList.contains("flagged")) {
this.classList.remove("flagged");
}
}
function cell_oncontextmenu() {
if (this.classList.contains("unknown")) {
this.classList.add("flagged");
sound_mark.play();
}
check_victory();
return false;
}
function minefield_initialize() {
$("#minefield").innerHTML = "";
for (let y = 0; y < field_height; y++) {
for (let x = 0; x < field_width; x++) {
let cell = document.createElement("div");
cell.id = "c" + x + "-" + y;
cell.x = x;
cell.y = y;
cell.classList.add("unknown");
cell.onclick = cell_onclick.bind(cell);
cell.oncontextmenu = cell_oncontextmenu.bind(cell);
$("#minefield").appendChild(cell);
}
}
}
function place_mines(avoid_x, avoid_y) {
for (let i = 0; i < initial_mines_count; i++) {
let x = Math.floor(Math.random() * field_width);
let y = Math.floor(Math.random() * field_height);
if (Math.abs(x - avoid_x) <= 1 && Math.abs(y - avoid_y) <= 1) {
--i;
} else {
$("#c" + x + "-" + y).classList.add("mine");
}
}
mines_placed = true;
}
function get_neighbours(cell) {
let cells = [];
for (let x = cell.x - 1; x <= cell.x + 1; ++x) {
for (let y = cell.y - 1; y <= cell.y + 1; ++y) {
if (x >= 0 && x < field_width && y >= 0 && y < field_height && !(cell.x == x && cell.y == y)) {
cells.push($("#c" + x + "-" + y));
}
}
}
return cells;
}
function count_neighbouring_mines(cell) {
count = 0;
get_neighbours(cell).forEach(function(c) {
if (c.classList.contains("mine") || c.classList.contains("exploded")) {
count += 1;
}
})
return count;
}
function reveal_mines(cell) {
let n_mines = count_neighbouring_mines(cell);
if (cell.classList.contains("unknown")) {
reveal_cell_cosmetic(cell);
cell.classList.remove("unknown");
cell.classList.add("known");
}
if (n_mines == 0) {
get_neighbours(cell).forEach(function(c) {
if (c.classList.contains("unknown")) {
reveal_mines(c);
}
if (c.classList.contains("flagged")) {
c.classList.remove("flagged");
}
});
} else if (n_mines > 0 && !cell.classList.contains("mine")) {
cell.innerText = "" + n_mines;
cell.classList.add("n" + n_mines);
}
}
function reveal_cell_cosmetic(cell) {
sound_reveal.play();
let r = cell.getBoundingClientRect();
let w = r.right - r.left;
let h = r.bottom - r.top;
let x = cell.offsetLeft - cell.scrollLeft;
let y = cell.offsetTop - cell.scrollTop;
spawn_particle(x, y, (Math.random() * 2 - 1) / 16, -0.3, 0, 1, dummy_image, w, h);
}
// Shows the game won/over popup.
function show_popup(victory) {
document.body.classList.remove("popup-victory", "popup-failure");
if (victory) {
document.body.classList.add("popup-victory");
$("#message").innerText = "You've won!"
} else {
document.body.classList.add("popup-failure");
$("#message").innerText = "You've lost!"
}
}
function check_victory() {
for (let y = 0; y < field_height; ++y) {
for (let x = 0; x < field_width; ++x) {
let cell = $("#c" + x + "-" + y);
if (cell.classList.contains("unknown") && !(cell.classList.contains("flagged") && cell.classList.contains("mine"))) {
return false;
}
}
}
// If we get here, we've won
game_finished = true;
stop_timing();
// Set the maximum level to be one higher, meaning the player unlocked the next level.
localStorage.setItem("maximum_level", maximum_level + 1);
show_popup(true);
// Save a high score.
let cur_score = localStorage.getItem(current_level + "-high_score");
if (cur_score == null) {
localStorage.setItem(current_level + "-high_score", seconds_elapsed)
} else if (parseInt(cur_score) > seconds_elapsed) {
localStorage.setItem(current_level + "-high_score", seconds_elapsed)
}
// Send a message proclaiming we've won.
window.webxdc.sendUpdate({
payload: {},
info: window.webxdc.selfName + " finished Level " + current_level + " in " + get_seconds_readable(seconds_elapsed) + "!"
}, window.webxdc.selfName + " won a game of Minesweeper");
return true;
}
function lose_game() {
game_finished = true;
stop_timing();
show_popup(false);
$$("#minefield div").forEach(function(cell) {
reveal_mines(cell);
if (cell.classList.contains("exploded")) {
cell.innerText = "";
}
});
}