632 lines
16 KiB
JavaScript
632 lines
16 KiB
JavaScript
|
const $buttonCancel = $('button.cancel');
|
||
|
const $game = $('#game');
|
||
|
const $gameOver = $('#gameOver');
|
||
|
const $ul = $('ul');
|
||
|
const $tutorial = $('#tutorial');
|
||
|
|
||
|
let gameBoardWidth = 10;
|
||
|
let gameBoardHeight = 9;
|
||
|
let score = 0;
|
||
|
let firstClick = true;
|
||
|
let mineChance = 0.2; // normal
|
||
|
let clickHoldMs = 200;
|
||
|
let mouseHeld = false;
|
||
|
let timeout; // hold timer
|
||
|
let viewportSizingAvailable = (parseInt($game.height) === parseInt($(window).height())); // only run once
|
||
|
let tutorialPages = [];
|
||
|
|
||
|
function drawGameBoard() {
|
||
|
$game.html("");
|
||
|
$('#tutorial .gameBoard').html("");
|
||
|
|
||
|
gameBoardWidth = 10;
|
||
|
|
||
|
// determine aspect ratio so as to fit the screen
|
||
|
gameBoardHeight = determineGameBoardHeight();
|
||
|
|
||
|
for (let i = 0; i < gameBoardHeight; i++) {
|
||
|
$game.append(newRow());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function determineGameBoardHeight() {
|
||
|
return Math.max(
|
||
|
Math.floor((1 / getAspectRatio()) * 10) - 1,
|
||
|
9 // minimum
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function newTile() {
|
||
|
if (Math.random() < mineChance) {
|
||
|
return '<li class="mine"></li>';
|
||
|
} else {
|
||
|
return '<li></li>';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function newRow() {
|
||
|
let row = '<ul>';
|
||
|
|
||
|
for (let i = 0; i < gameBoardWidth; i++) {
|
||
|
row += newTile();
|
||
|
}
|
||
|
|
||
|
row += '</ul>';
|
||
|
|
||
|
return row;
|
||
|
}
|
||
|
|
||
|
function saveGame() {
|
||
|
if (typeof(Storage) === "undefined") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// variables
|
||
|
localStorage.setItem("score", score);
|
||
|
localStorage.setItem("mineChance", mineChance);
|
||
|
|
||
|
// game board
|
||
|
localStorage.setItem("gameBoard", $game.html());
|
||
|
}
|
||
|
|
||
|
function loadGame() {
|
||
|
// check if storage available and if saved game exists
|
||
|
if (typeof(Storage) === "undefined" || localStorage.getItem("gameBoard") === null) {
|
||
|
drawGameBoard();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// variables
|
||
|
score = parseInt(localStorage.getItem("score"));
|
||
|
mineChance = parseFloat(localStorage.getItem("mineChance"));
|
||
|
firstClick = false;
|
||
|
|
||
|
// game board
|
||
|
$game.html(localStorage.getItem("gameBoard"));
|
||
|
|
||
|
gameBoardHeight = determineGameBoardHeight();
|
||
|
|
||
|
// add rows if there is empty space
|
||
|
while ($ul.length < gameBoardHeight) {
|
||
|
$game.append(newRow());
|
||
|
}
|
||
|
|
||
|
// remove any rows the screen cannot display
|
||
|
$('#game ul:gt(' + (gameBoardHeight - 1) + ')').remove();
|
||
|
|
||
|
refreshMineCounts();
|
||
|
updateScore();
|
||
|
updateMinesLeft();
|
||
|
|
||
|
// click blank tiles
|
||
|
$('li.revealed:not(.mine):empty').mouseup();
|
||
|
|
||
|
$('#setup').hide();
|
||
|
}
|
||
|
|
||
|
$.fn.check = function() {
|
||
|
// unclicked tiles
|
||
|
if ($(this).filter('li:not(.revealed):not(.flagged)').length > 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// incorrectly flagged tiles
|
||
|
if ($(this).filter('li.flagged:not(.mine)').length > 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// clicked mines
|
||
|
if ($(this).filter('li.revealed.mine').length > 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
$.fn.checkRow = function() {
|
||
|
return $(this).children().check();
|
||
|
}
|
||
|
|
||
|
$.fn.checkColumn = function() {
|
||
|
return $(this).column().check();
|
||
|
}
|
||
|
|
||
|
window.removeClearedRows = function() {
|
||
|
let rowsToRemove = $('ul:not(.removing)').filter(function() {
|
||
|
return $(this).checkRow();
|
||
|
});
|
||
|
|
||
|
if (rowsToRemove.length > 0) {
|
||
|
suddenDeath();
|
||
|
}
|
||
|
|
||
|
rowsToRemove.each(function() {
|
||
|
score += $(this).children('.mine').length;
|
||
|
|
||
|
$(this).addClass("removing");
|
||
|
|
||
|
$(this).slideUp("slow", function() {
|
||
|
// add new row on bottom
|
||
|
$(this).closest("div").append(newRow());
|
||
|
|
||
|
$(this).remove();
|
||
|
|
||
|
refreshMineCounts();
|
||
|
|
||
|
// click blank tiles
|
||
|
$('li.revealed:not(.mine):empty').mouseup();
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
window.removeClearedColumns = function() {
|
||
|
let columnsToRemove = $('ul:not(.removing):eq(0) li:not(.removing)').filter(function() {
|
||
|
return $(this).checkColumn();
|
||
|
});
|
||
|
|
||
|
if (columnsToRemove.length > 0) {
|
||
|
suddenDeath();
|
||
|
}
|
||
|
|
||
|
columnsToRemove.each(function() {
|
||
|
score += $(this).column().filter('.mine').length;
|
||
|
|
||
|
$(this).column().addClass("removing");
|
||
|
|
||
|
// animation for top row + deletion of column
|
||
|
$(this).animate({width: 0, borderRadius: 0, padding: 0}, "slow", function() {
|
||
|
$(this).column().remove();
|
||
|
|
||
|
$ul.each(function() {
|
||
|
$(this).append(newTile());
|
||
|
});
|
||
|
|
||
|
refreshMineCounts();
|
||
|
|
||
|
// click blank tiles
|
||
|
$('li.revealed:not(.mine, .removing):empty').mouseup();
|
||
|
});
|
||
|
|
||
|
// then just animation for others
|
||
|
$(this).column().animate({width: 0, borderRadius: 0, padding: 0}, "slow");
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function refreshMineCounts() {
|
||
|
$('ul:not(.removing) li.revealed:not(.mine, .removing)').each(function() {
|
||
|
let mineCount = $(this).countMinesText();
|
||
|
|
||
|
$(this).text(mineCount);
|
||
|
|
||
|
// remove "mines1" etc
|
||
|
$(this).attr(
|
||
|
'class',
|
||
|
$(this).attr('class').replace(
|
||
|
/mines[0-9]/, "mines" + mineCount
|
||
|
)
|
||
|
);
|
||
|
|
||
|
$(this).removeClass("mines mines0");
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function updateScore() {
|
||
|
$('#score').text("Score: " + score);
|
||
|
}
|
||
|
|
||
|
function isGameOver() {
|
||
|
return $ul.has('.mine.revealed').length === gameBoardHeight
|
||
|
&& $ul.first().children().filter(function() {
|
||
|
return $(this).column().filter('.mine.revealed').length > 0;
|
||
|
}).length === gameBoardWidth;
|
||
|
}
|
||
|
|
||
|
function checkGameOver() {
|
||
|
if (isGameOver()) {
|
||
|
$gameOver.show();
|
||
|
|
||
|
let currentHiScore = 0;
|
||
|
|
||
|
if ($.isNumeric(localStorage.getItem("hiscore"))) {
|
||
|
currentHiScore = parseInt(localStorage.getItem("hiscore"));
|
||
|
}
|
||
|
|
||
|
if (score > currentHiScore) {
|
||
|
currentHiScore = score;
|
||
|
}
|
||
|
|
||
|
$('#gameOver .score').text(score);
|
||
|
$('#gameOver .hiscore').text(currentHiScore);
|
||
|
|
||
|
// clear saved game
|
||
|
localStorage.clear();
|
||
|
localStorage.setItem("hiscore", currentHiScore);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function checkTutorial() {
|
||
|
if ($tutorial.is(':hidden')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let currentPage = $('#tutorial div:visible');
|
||
|
|
||
|
if (
|
||
|
currentPage.find('.toFlag' ).length === currentPage.find('.toFlag.flagged' ).length
|
||
|
&& currentPage.find('.toClick').length === currentPage.find('.toClick.revealed' ).length
|
||
|
&& (currentPage.find('.flagAny' ).length === 0 || currentPage.find('.flagAny.flagged' ).length > 0)
|
||
|
&& (currentPage.find('.clickAny').length === 0 || currentPage.find('.clickAny.revealed').length > 0)
|
||
|
) {
|
||
|
currentPage.nextPage();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$.fn.nextPage = function() {
|
||
|
$('.gameBoard').html("");
|
||
|
$(this).hide();
|
||
|
|
||
|
if ($(this).is(':last-child')) {
|
||
|
// end of tutorial!
|
||
|
$tutorial.hide();
|
||
|
$('#setup, #game, #stats').show();
|
||
|
$('#setup button.cancel').hide();
|
||
|
|
||
|
gameBoardWidth = 10;
|
||
|
gameBoardHeight = determineGameBoardHeight();
|
||
|
} else {
|
||
|
$(this).next().show();
|
||
|
$(this).next().children().show();
|
||
|
|
||
|
let nextPage = parseInt($(this).attr('id').replace("page", ""));
|
||
|
|
||
|
$(this).next().children('.gameBoard').html(tutorialPages[nextPage]);
|
||
|
|
||
|
if ($(this).next().children('.gameBoard').hasClass("clearRowsColumns")) {
|
||
|
removeClearedRows();
|
||
|
removeClearedColumns();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function updateMinesLeft() {
|
||
|
// un-flagged mines - revealed mines - flagged not-mines
|
||
|
let count = $('.mine:not(.flagged)').length - $('.mine.revealed').length - $('li:not(.mine).flagged').length;
|
||
|
$('#mines').text("Mines left: " + count);
|
||
|
}
|
||
|
|
||
|
function getAspectRatio() {
|
||
|
return $(window).width() / $(window).height();
|
||
|
}
|
||
|
|
||
|
function resizeToWindow() {
|
||
|
if (!viewportSizingAvailable) {
|
||
|
$('html').css('font-size', Math.min($(window).width(), $(window).height()) / 10);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$.fn.updateMineCount = function() {
|
||
|
$(this).text(
|
||
|
$(this).countMinesText
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$.fn.rowScore = function() {
|
||
|
return $(this).children('.mine').length;
|
||
|
}
|
||
|
|
||
|
$.fn.rowAbove = function() {
|
||
|
return $(this).parent('ul').prev();
|
||
|
}
|
||
|
|
||
|
$.fn.rowBelow = function() {
|
||
|
return $(this).parent('ul').next();
|
||
|
}
|
||
|
|
||
|
$.fn.column = function() {
|
||
|
let x = $(this).getX();
|
||
|
|
||
|
let column = $('');
|
||
|
|
||
|
$ul.each(function() {
|
||
|
column = column.add($(this).children().eq(x));
|
||
|
});
|
||
|
|
||
|
return column;
|
||
|
}
|
||
|
|
||
|
window.oneColumnLeft = function() {
|
||
|
return $ul.first().children().filter(function() {
|
||
|
return $(this).column().filter('.mine:not(.revealed, .flagged)').length === 0;
|
||
|
}).length >= (gameBoardWidth - 1)
|
||
|
}
|
||
|
|
||
|
window.oneRowLeft = function() {
|
||
|
return $ul.filter(function() {
|
||
|
return $(this).children('.mine:not(.revealed, .flagged)').length === 0;
|
||
|
}).length >= (gameBoardHeight - 1);
|
||
|
}
|
||
|
|
||
|
window.isSuddenDeath = function() {
|
||
|
return oneRowLeft() && oneColumnLeft();
|
||
|
}
|
||
|
|
||
|
window.suddenDeath = function() {
|
||
|
if (isSuddenDeath()) {
|
||
|
mineChance += 0.03;
|
||
|
|
||
|
// don't want the chance to get to 100% because that's completely predictable
|
||
|
// ⅔ chance is the most likely to create an unsolvable repeating pattern (xxoxxoxxo, etc.)
|
||
|
if (mineChance > 0.666) {
|
||
|
mineChance = 0.666;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$.fn.getX = function() {
|
||
|
return $(this).index();
|
||
|
}
|
||
|
|
||
|
$.fn.getY = function() {
|
||
|
return $(this).parent('ul').index();
|
||
|
}
|
||
|
|
||
|
$.fn.isMine = function() {
|
||
|
return $(this).hasClass("mine");
|
||
|
}
|
||
|
|
||
|
$.fn.countMinesAdjacent = function() {
|
||
|
return $(this).getAdjacentTiles().filter('.mine').length;
|
||
|
}
|
||
|
|
||
|
$.fn.getAdjacentTiles = function() {
|
||
|
let adjacentTiles = $('');
|
||
|
let x = $(this).getX();
|
||
|
let y = $(this).getY();
|
||
|
|
||
|
if (y > 0) {
|
||
|
let tileAbove = $(this).rowAbove().children().eq(x);
|
||
|
|
||
|
adjacentTiles = adjacentTiles.add(tileAbove.prev());
|
||
|
adjacentTiles = adjacentTiles.add(tileAbove );
|
||
|
adjacentTiles = adjacentTiles.add(tileAbove.next());
|
||
|
}
|
||
|
|
||
|
adjacentTiles = adjacentTiles.add($(this).prev());
|
||
|
adjacentTiles = adjacentTiles.add($(this).next());
|
||
|
|
||
|
if (y < (gameBoardHeight - 1)) {
|
||
|
let tileBelow = $(this).rowBelow().children().eq(x);
|
||
|
|
||
|
adjacentTiles = adjacentTiles.add(tileBelow.prev());
|
||
|
adjacentTiles = adjacentTiles.add(tileBelow );
|
||
|
adjacentTiles = adjacentTiles.add(tileBelow.next());
|
||
|
}
|
||
|
|
||
|
return adjacentTiles;
|
||
|
}
|
||
|
|
||
|
$.fn.countMinesText = function() {
|
||
|
return $(this).countMinesAdjacent().toString().replace("0", "");
|
||
|
}
|
||
|
|
||
|
function detachTutorials() {
|
||
|
// can't have several game boards in play at the same time
|
||
|
// because it messes with mine counts, row counts, etc.
|
||
|
tutorialPages = [];
|
||
|
|
||
|
$('#tutorial .gameBoard').each(function() {
|
||
|
tutorialPages.push($(this).html());
|
||
|
|
||
|
$(this).html("");
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$.fn.cracktro = function() {
|
||
|
// trim long whitespaces to 1 space, remove line breaks, split to individual chars
|
||
|
let text = $(this).html().replace(/ +/g, " ").replace(/(\r\n|\n|\r)/gm, "").replace("&", "&").split("");
|
||
|
let cracktro = "";
|
||
|
|
||
|
for (let i = 0; i < text.length; i++) {
|
||
|
cracktro += "<span>" + text[i].replace(" ", " ") + "</span>";
|
||
|
}
|
||
|
|
||
|
$(this).html(cracktro);
|
||
|
}
|
||
|
|
||
|
$('#gameOver button').on("click", function() {
|
||
|
drawGameBoard();
|
||
|
|
||
|
$gameOver.hide();
|
||
|
$buttonCancel.hide();
|
||
|
$('button.start').removeAttr('style');
|
||
|
$('#setup').show();
|
||
|
});
|
||
|
|
||
|
$('button.reset').on("click", function() {
|
||
|
// prompt user with setup screen
|
||
|
$('#setup').show();
|
||
|
$buttonCancel.show();
|
||
|
$('button.start').css('left', '20vmin');
|
||
|
});
|
||
|
|
||
|
$buttonCancel.on("click", function() {
|
||
|
// prompt user with setup screen
|
||
|
$('#setup').hide();
|
||
|
$buttonCancel.hide();
|
||
|
$('button.start').removeAttr('style');
|
||
|
});
|
||
|
|
||
|
$('button.start').on("click", function() {
|
||
|
drawGameBoard();
|
||
|
|
||
|
// reset stats
|
||
|
firstClick = true;
|
||
|
|
||
|
// set difficulty
|
||
|
if ($(this).hasClass("easy")) {
|
||
|
mineChance = 0.13;
|
||
|
} else if ($(this).hasClass("normal")) {
|
||
|
mineChance = 0.2;
|
||
|
} else if ($(this).hasClass("hard")) {
|
||
|
mineChance = 0.285;
|
||
|
}
|
||
|
|
||
|
score = 0;
|
||
|
|
||
|
updateScore();
|
||
|
updateMinesLeft();
|
||
|
|
||
|
$('#setup').hide();
|
||
|
});
|
||
|
|
||
|
$('button.tutorial').on("click", function() {
|
||
|
$tutorial.show();
|
||
|
$tutorial.siblings().hide();
|
||
|
|
||
|
const $page1 = $('#page1');
|
||
|
|
||
|
$page1.show();
|
||
|
$('#page1 *').show();
|
||
|
$page1.siblings('div').hide();
|
||
|
|
||
|
$('#page1 .gameBoard').html(tutorialPages[0]);
|
||
|
|
||
|
$game.html("");
|
||
|
|
||
|
gameBoardWidth = 5;
|
||
|
gameBoardHeight = 5;
|
||
|
});
|
||
|
|
||
|
$.fn.leftClick = function(automated) {
|
||
|
if (!automated) automated = false;
|
||
|
|
||
|
// don't want first click to be a mine
|
||
|
if (firstClick) {
|
||
|
let x = $(this).getX();
|
||
|
x = (x >= 1) ? x : 1;
|
||
|
x = (x <= gameBoardWidth - 2) ? x : gameBoardWidth - 2;
|
||
|
|
||
|
let y = $(this).getY();
|
||
|
y = (y >= 1) ? y : 1;
|
||
|
y = (y <= gameBoardHeight - 2) ? y : gameBoardHeight - 2;
|
||
|
|
||
|
$ul.eq(y - 1).children().slice(x - 1).filter(':lt(3)').removeClass("mine");
|
||
|
$ul.eq(y ).children().slice(x - 1).filter(':lt(3)').removeClass("mine");
|
||
|
$ul.eq(y + 1).children().slice(x - 1).filter(':lt(3)').removeClass("mine");
|
||
|
|
||
|
firstClick = false;
|
||
|
}
|
||
|
|
||
|
if ($(this).hasClass("flagged")) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($(this).isMine()) {
|
||
|
$(this).addClass("revealed");
|
||
|
} else if (!automated && parseInt($(this).text()) === $(this).getAdjacentTiles().filter('.flagged, .revealed.mine').length) {
|
||
|
// already clicked; use middle click reveal functionality
|
||
|
|
||
|
// number of flags matches number of adjacent mines
|
||
|
$(this).getAdjacentTiles().filter(':not(.flagged, .revealed)').mouseup();
|
||
|
} else {
|
||
|
$(this).addClass("revealed");
|
||
|
$(this).updateMineCount();
|
||
|
$(this).addClass("mines" + $(this).countMinesAdjacent());
|
||
|
|
||
|
// if no mines adjacent, cascade!
|
||
|
if (parseInt($(this).countMinesAdjacent()) === 0) {
|
||
|
$(this).getAdjacentTiles().filter(':not(.revealed)').mouseup();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
firstClick = false;
|
||
|
}
|
||
|
|
||
|
$.fn.middleClick = function() {
|
||
|
// number of flags matches number of adjacent mines
|
||
|
if (parseInt($(this).text()) === $(this).getAdjacentTiles().filter('.flagged, .revealed.mine').length) {
|
||
|
$(this).getAdjacentTiles().filter(':not(.flagged, .revealed)').mouseup();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$.fn.rightClick = function() {
|
||
|
if (!$(this).hasClass("revealed")) {
|
||
|
$(this).toggleClass("flagged");
|
||
|
}
|
||
|
|
||
|
clearTimeout(timeout);
|
||
|
}
|
||
|
|
||
|
$(document).on("contextmenu", "body", function(event) {
|
||
|
event.preventDefault();
|
||
|
});
|
||
|
|
||
|
$(document).on("mousedown touchstart", "li", function() {
|
||
|
let x = $(this).getX();
|
||
|
let y = $(this).getY();
|
||
|
|
||
|
timeout = setTimeout(function() {
|
||
|
$('ul:eq(' + y + ') li:eq(' + x + ')').rightClick();
|
||
|
|
||
|
mouseHeld = true;
|
||
|
}, clickHoldMs);
|
||
|
});
|
||
|
|
||
|
$(document).on("mouseleave", "li", function() {
|
||
|
clearTimeout(timeout);
|
||
|
});
|
||
|
|
||
|
$(document).on("mouseup touchend", "li", function(event) {
|
||
|
event.preventDefault();
|
||
|
|
||
|
clearTimeout(timeout);
|
||
|
|
||
|
if (mouseHeld) {
|
||
|
mouseHeld = false;
|
||
|
|
||
|
// rightClick has already been called in a callback, so nothing to do here
|
||
|
} else {
|
||
|
switch (event.which) {
|
||
|
case 3:
|
||
|
$(this).rightClick();
|
||
|
break;
|
||
|
case 2:
|
||
|
$(this).middleClick();
|
||
|
break;
|
||
|
case 1:
|
||
|
$(this).leftClick();
|
||
|
break;
|
||
|
default:
|
||
|
$(this).leftClick(true); // automated
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
checkTutorial();
|
||
|
|
||
|
if ($tutorial.is(':hidden')) {
|
||
|
removeClearedRows();
|
||
|
removeClearedColumns();
|
||
|
|
||
|
saveGame();
|
||
|
checkGameOver();
|
||
|
|
||
|
updateScore();
|
||
|
updateMinesLeft();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
$(window).resize(resizeToWindow);
|
||
|
|
||
|
resizeToWindow();
|
||
|
detachTutorials();
|
||
|
|
||
|
// instantiate the game
|
||
|
loadGame(); // loadGame will draw the game board if no saved game is found
|
||
|
|
||
|
$gameOver.hide();
|
||
|
$tutorial.hide();
|
||
|
$buttonCancel.hide();
|
||
|
|
||
|
$('#cracktro').cracktro();
|