// @ts-check /** * import styles for vite bundling */ import './mnswpr.css' import { LoggerService, StorageService, TimerService } from './modules/index.js' import { levels } from './levels.js' const TEST_MODE = false // set to true if you want to test the game with visual hints const MOBILE_BUSY_DELAY = 250 const PC_BUSY_DELAY = 500 /** * Create Minesweeper game board * @param {String} appId * @param {String} version * @param {{ * levelChanged: () => {}, * gameDone: (game: any) => {} * }} hooks */ const Minesweeper = function(appId, version, hooks) { const _this = this const storageService = new StorageService() const timerService = new TimerService() const loggerService = new LoggerService() 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 appElement = document.getElementById(appId) if (!appElement) { const body = document.getElementsByTagName('body')[0] appElement = document.createElement('div') body.append(appElement) } 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, id: 'test', 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.innerHTML = `Minesweeper${version}` document.title = `mnswpr [${version}]` gameBoard.setAttribute('id', 'game-board') gameBoard.append(initializeToolbar(), grid, initializeFootbar()) if(appElement) { appElement.innerHTML = '' appElement.append(headingElement, gameBoard) appElement.append(initializeSourceLink()) } generateGrid(true) } 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 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].id levelOption.text = levels[key].name if (setting.id === levelOption.value) { levelOption.selected = true } levelsDropdown.add(levelOption, null) }) if (TEST_MODE) { const testLevel = document.createElement('span') testLevel.innerText = 'Test Mode' footBar.append(testLevel) } else { footBar.append(levelsDropdown) } return footBar } 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 } /** * Updates the game level * @param {String} key */ function updateSetting(key) { setting = levels[key] storageService.saveToLocal('setting', setting) generateGrid(true) } function generateGrid(initial = false) { 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 = '😁' } /** * * @param {HTMLTableCellElement} cell */ function initializeTouchEventHandlers(cell) { let ontouchleave = function() { if (clickedCell === this) { clickedCell = undefined } } cell.addEventListener('touchleave', ontouchleave) let ontouchend = function() { 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 = _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