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 '
  • '; } else { return '
  • '; } } function newRow() { let row = ''; 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 += "" + text[i].replace(" ", " ") + ""; } $(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();