// @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