import { LeaderBoardService, LoggerService, StorageService, TimerService } from './modules'; import { levels } from './levels.js'; const TEST_MODE = false; // set to true if you want to test the game with visual hints and separate leaderboard const VERSION = "0.3.12"; const MOBILE_BUSY_DELAY = 250; const PC_BUSY_DELAY = 500; const CASUAL_MODE = false; export const Minesweeper = function(appId) { const _this = this; const storageService = new StorageService(); const timerService = new TimerService(); const loggerService = new LoggerService(); const leaderBoard = new LeaderBoardService('mw-leaders', 'mw-all', 'mw-config'); let grid = document.createElement('table'); grid.setAttribute('id', 'grid'); let flagsDisplay = document.createElement('span'); let smileyDisplay = document.createElement('span'); let timerDisplay = document.createElement('span'); let footbar = document.createElement('div'); let customWrapper = document.createElement('div'); customWrapper.setAttribute('id', 'custom-wrapper'); let appElement = document.getElementById(appId); if (!appElement) { const body = document.getElementsByTagName('body')[0]; appElement = document.createElement('div'); body.append(appElement); } let leaderWrapper = document.createElement('div'); let isMobile = false; let isLeft = false; let isRight = false; let pressed = undefined; let bothPressed = undefined; let skip = false; let skipCondition = false; let mouseUpCallBackArray = [ clickCell, middleClickCell, ]; let mouseDownCallBackArray = [ highlightCell, // left-click down highlightSurroundingCell, // middle-click down rightClickCell // right-click down ] let firstClick = true; let isBusy = false; let clickedCell; let cachedSetting = storageService.getFromLocal('setting'); let setting = cachedSetting || levels.beginner; if (TEST_MODE) { setting = { rows: 10, cols: 10, mines: 10, name: 'test' } } storageService.saveToLocal('setting', setting); let flagsCount = setting.mines; let minesArray = []; this.initialize = function() { const headingElement = document.createElement('h1'); const gameBoard = document.createElement('div'); headingElement.innerText = `Minesweeper v${VERSION}`; gameBoard.setAttribute('id', 'game-board'); gameBoard.append(initializeToolbar(), grid, initializeFootbar()); appElement.innerHTML = ''; appElement.append(headingElement, gameBoard); appElement.append(initializeSourceLink()); generateGrid(); } function initializeSourceLink() { const sourceLink = document.createElement('a'); sourceLink.href = 'https://github.com/ayoayco/mnswpr'; sourceLink.innerText = 'Source code'; sourceLink.target = '_blank'; sourceLink.style.color = 'white'; return sourceLink; } function initializeLeaderBoard() { const title = `Best Times (${setting.name})`; leaderBoard.update(setting.name, leaderWrapper, title); appElement.append(leaderWrapper); } function initializeFootbar() { const footBar = document.createElement('div'); const resetButton = document.createElement('button'); resetButton.innerText = 'Reset'; resetButton.onmousedown = () => generateGrid(); footBar.append(resetButton); let levelsDropdown = document.createElement('select'); levelsDropdown.onchange = () => updateSetting(levelsDropdown.value); const levelsKeys = Object.keys(levels); levelsKeys.forEach(key => { const levelOption = document.createElement('option'); levelOption.value = levels[key].name; levelOption.text = capitalize(levels[key].name); if (setting.name === levelOption.value) { levelOption.selected = true; } levelsDropdown.add(levelOption, null); }); // custom level // const customOption = document.createElement('option'); // customOption.onmousedown = () => {} // customOption.value = 'custom'; // customOption.text = 'Custom'; // levelsDropdown.add(customOption); if (TEST_MODE) { const testLevel = document.createElement('span'); testLevel.innerText = 'Test Mode'; footBar.append(testLevel); } else { footBar.append(levelsDropdown); } return footBar; } function removeCustomOptions() { const customCopy = document.getElementById('custom-wrapper'); if (customCopy) { footbar.removeChild(customWrapper); } } function insertCustomOptions() { const inputElements = []; const rowsInput = document.createElement('input'); rowsInput.placeholder = 'Rows'; inputElements.push(rowsInput); const colsInput = document.createElement('input'); colsInput.placeholder = 'Columns'; inputElements.push(colsInput); const bombsInput = document.createElement('input'); bombsInput.placeholder = 'Bombs'; inputElements.push(bombsInput); const okButton = document.createElement('button'); okButton.innerText = 'Okay'; const setting = {rows: rowsInput.value, cols: colsInput.value, bombs: bombsInput.value}; okButton.onmousedown = () => updateSetting('custom-action', setting); inputElements.forEach(input => { input.style.marginRight = '15px'; input.style.width = '100px'; input.maxLength = 3; input.type = 'number'; input.width = 50; }); customWrapper.append(...inputElements, okButton); footbar.append(customWrapper); } function capitalize(str) { if (!str) return ''; return `${str[0].toUpperCase()}${str.slice(1, str.length)}`; } function initializeToolbar() { const toolbar = document.createElement('div'); const toolbarItems = []; const flagsWrapper = document.createElement('div'); flagsWrapper.append(flagsDisplay) flagsWrapper.style.height = '20px'; toolbar.append(flagsWrapper); toolbarItems.push(flagsWrapper); const smileyWrapper = document.createElement('div'); smileyWrapper.append(smileyDisplay); // toolbar.append(smileyWrapper); // toolbarItems.push(smileyWrapper); const timerWrapper = document.createElement('div'); timerWrapper.append(timerDisplay); timerWrapper.style.height = '20px'; toolbar.append(timerWrapper); toolbarItems.push(timerWrapper); toolbar.style.cursor = 'pointer'; toolbar.style.padding = '10px 35px'; toolbar.style.display = 'flex'; toolbar.style.justifyContent = 'space-between'; toolbar.onmousedown = () => generateGrid(); return toolbar; } function updateSetting(key, custom) { if (key === 'custom') { insertCustomOptions(); } else if (key === 'custom-action') { console.log('custom', custom); } else { setting = levels[key]; storageService.saveToLocal('setting', setting); removeCustomOptions(); generateGrid(); } } function generateGrid() { //generate 10 by 10 grid firstClick = true; grid.innerHTML = ''; grid.oncontextmenu = () => false; flagsCount = setting.mines; minesArray = []; for (let i = 0; i < setting.rows; i++) { let row = grid.insertRow(i); row.oncontextmenu = () => false; for (let j=0; j isBusy = false, MOBILE_BUSY_DELAY); } else { setTimeout(() => isBusy = false, PC_BUSY_DELAY); } } function updateFlagsCountDisplay(count = flagsCount) { if (grid.getAttribute('game-status') != 'win') { flagsDisplay.innerHTML = `${count}`; return; } flagsDisplay.innerHTML = '😁'; } function initializeTouchEventHandlers(_cell) { let cell = document.createElement('td'); cell = _cell; let ontouchleave = function(e) { if (clickedCell === this) { clickedCell = undefined } } cell.addEventListener('touchleave', ontouchleave); let ontouchend = function(e) { endTouchTimer(); } cell.addEventListener('touchend', ontouchend); let ontouchstart = function(e) { isMobile = true; if (!isBusy && typeof e === 'object') { startTouchTimer(this); } } cell.addEventListener('touchstart', ontouchstart); } function initializeEventHandlers(_cell) { let cell = document.createElement('td'); cell = _cell; skip = false; skipCondition = false; resetMouseEventFlags(); document.onkeydown = function(e) { if (e.keyCode == 32 || e.keyCode == 113) { generateGrid(); if ('preventDefault' in e) { e.preventDefault(); } else { return false; } } resetMouseEventFlags(); } window.onblur = function() { resetMouseEventFlags(); } grid.onmouseleave = function() { removeHighlights(); } document.oncontextmenu = () => false; document.onmouseup = function() { resetMouseEventFlags(); } document.onmousedown = function(e) { isMobile = false; switch (e.button) { case 0: pressed = 'left'; isLeft = true; break; case 1: pressed = 'middle'; break; case 2: isRight = true; break; } } // Set grid status to active on first click cell.onmouseup = function(e) { pressed = undefined; let dont = false; if (bothPressed) { bothPressed = false; if (e.button == '2') { skipCondition = true; } else if (e.button == '0') { dont = true; } if (getStatus(this) == 'clicked') { middleClickCell(this); return; } } switch(e.button) { case 0: { isLeft = false; if (skipCondition) { skip = true; } break; } case 2: isRight = false; break } removeHighlights(); if (skip || dont) { skip = false; skipCondition = false; return; } if (!isBusy && typeof e === 'object' && e.button != '2') { mouseUpCallBackArray[e.button].call(_this, this); } } cell.onmousedown = function(e) { skip = false; if (!isBusy && typeof e === 'object') { switch(e.button) { case 0: isLeft = true; break; case 2: isRight = true; break } if (isLeft && isRight) { bothPressed = true; highlightSurroundingCell(this); return; } if (e.button == '1') { pressed = 'middle'; highlightSurroundingCell(this); } else if (e.button == '0') { pressed = 'left'; if (getStatus(this) == 'clicked') { highlightSurroundingCell(this); } else { highlightCell(this); } } if (e.button == '2') mouseDownCallBackArray[e.button].call(_this, this); } } cell.onmousemove = function(e) { if ((pressed || bothPressed) && typeof e === 'object') { removeHighlights(); /* if (!isEqual(clickedCell, cell)) { clickedCell = undefined; } */ if (pressed == 'middle' || (isLeft && isRight)) { highlightSurroundingCell(this); } else if (pressed == 'left') { if (getStatus(this) == 'clicked') { highlightSurroundingCell(this); } else { highlightCell(this); } } } } cell.oncontextmenu = () => false; cell.onselectstart = () => false; cell.setAttribute('unselectable', 'on'); } function isEqual(x, y) { if (!x) return false; return x === y; } function startTouchTimer(cell) { if (isEqual(clickedCell, cell)) { return; } clickedCell = cell; setTimeout(() => { if (isEqual(clickedCell, cell)) { rightClickCell(cell); setBusy(); } }, 500); } function endTouchTimer() { clickedCell = undefined; } function resetMouseEventFlags() { pressed = undefined; bothPressed = undefined; isLeft = false; isRight = false; removeHighlights(); skip = true; } function addMines(minesCount) { //Add mines randomly for (let i=0; i -1; } function removeItem(arr, cell) { const index = getIndex(arr, cell); if (index > -1) { arr.splice(index, 1); } } function getIndex(arr, cell) { const row = cell.parentNode.rowIndex; const col = cell.cellIndex; let index = -1; for (let i = 0; i < arr.length; i++) { let rowCol = arr[i] if (rowCol[0] === row && rowCol[1] === col) { index = i; break; } } return index; } function checkLevelCompletion() { let levelComplete = true; for (let i=0; i