commit 3ed4ec91564af8c4425cbf17789850f5732ba73d Author: Eduardo Zardo Date: Tue Apr 19 19:18:32 2022 -0300 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7038709 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Eduardo Zardo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d835cfb --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Minesweeper +A minesweeper game made with pure HTML, CSS and Javascript diff --git a/assets/icons/flag.svg b/assets/icons/flag.svg new file mode 100644 index 0000000..b275ccc --- /dev/null +++ b/assets/icons/flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/mine.png b/assets/icons/mine.png new file mode 100644 index 0000000..eea47f2 Binary files /dev/null and b/assets/icons/mine.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..bef76ee --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + Minesweeper + + +
+
+ + +
+ +
+
+
+ 0 + +
+ +
+ +
+ +
+

00:00

+
+
+ +
+
+ + +
+ + + + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..6aa3cf4 --- /dev/null +++ b/main.js @@ -0,0 +1,384 @@ +const difficulties = { + 'easy': { + 'size': [10, 10], + 'mines': 10, + }, + 'medium': { + 'size': [16, 16], + 'mines': 40, + }, + 'hard': { + 'size': [16, 30], + 'mines': 99, + }, +}; + +const squareClassNames = { + '.': 'unopened', + 'B': 'unopened', + 'F': 'unopened flag', + 'BF': 'unopened flag', + 'X': 'bomb', + '0': 'opened empty', + '1': 'opened one', + '2': 'opened two', + '3': 'opened three', + '4': 'opened four', + '5': 'opened five', + '6': 'opened six', + '7': 'opened seven', + '8': 'opened eight', +}; + +const squareTexts = { + '.': '', + 'B': '', + 'F': '', + 'BF': '', + 'X': '', + '0': '', + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + '6': '6', + '7': '7', + '8': '8', +}; + +const directions = [ + [-1, -1], + [-1, 0], + [-1, 1], + [0, -1], + [0, 0], + [0, 1], + [1, -1], + [1, 0], + [1, 1] +]; + +let board; +let size; +let mines; +let flagCount; +let timeInSeconds; +let timerInterval; +let unopenedCount; +let bombRevealInterval; +let squareListeners = {}; + +let isFirstClick = true; + +function initializeBoard() { + board = []; + unopenedCount = 0; + + for (let i = 0; i < size[0]; i++) { + const row = []; + + for (let j = 0; j < size[1]; j++) { + row.push('.'); + unopenedCount += 1; + } + + board.push(row); + } +} + +function generateMines(firstClickX, firstClickY) { + let i = 0; + while (i < mines) { + const mineX = randomInt(0, size[0]); + const mineY = randomInt(0, size[1]); + console.log(mineX); + console.log(mineY); + + if (!((mineX >= firstClickX - 1 && mineX <= firstClickX + 1 && mineY >= firstClickY - 1 && mineY <= firstClickY + 1) || board[mineX][mineY] === 'B')) { + board[mineX][mineY] = 'B'; + i++; + unopenedCount -= 1; + } + } +} + +function countMinesAroundSquare(row, column) { + let mines = 0; + + for (const direction of directions) { + const rowToCheck = row + direction[0]; + const columnToCheck = column + direction[1]; + + if (rowToCheck < 0 || rowToCheck >= size[0] || columnToCheck < 0 || columnToCheck >= size[1]) { + continue; + } + + if (board[rowToCheck][columnToCheck].includes('B')) { + mines += 1; + } + } + + return mines; +} + +function stopGame(hasWon) { + if (timerInterval) { + clearInterval(timerInterval); + } + + for (const square of document.getElementsByClassName('square')) { + const listener = squareListeners[square.id]; + + square.removeEventListener('mousedown', listener); + } + + squareListeners = {}; + + const gameResult = document.getElementById('result'); + + if (hasWon) { + gameResult.innerText = 'You win!'; + gameResult.className = 'win'; + } else { + gameResult.innerText = 'You lose.'; + gameResult.className = 'defeat'; + } + + document.getElementById('result-container').classList.remove('hide'); +} + +function hasWon() { + return unopenedCount === 0; +} + +function revealSquare(row, column) { + const mineCount = countMinesAroundSquare(row, column); + + if (board[row][column] == 'F') { + flagCount += 1; + renderFlagsCount(); + } + + if (board[row][column] == '.') { + unopenedCount -= 1; + } + + board[row][column] = mineCount.toString(); + + const squareElement = document.getElementById(`${row}-${column}`); + + squareElement.innerHTML = squareTexts[board[row][column]]; + squareElement.className = 'square ' + squareClassNames[board[row][column]]; + + if (hasWon()) { + stopGame(true); + } + + if (mineCount === 0) { + for (const direction of directions) { + const rowToOpen = row + direction[0]; + const columnToOpen = column + direction[1]; + + if (rowToOpen < 0 || rowToOpen >= size[0] || columnToOpen < 0 || columnToOpen >= size[1]) { + continue; + } + + if (['.', 'F'].includes(board[rowToOpen][columnToOpen])) { + revealSquare(rowToOpen, columnToOpen); + } + } + } +} + +function switchFlag(row, column) { + const squareValue = board[row][column]; + if (['.', 'B'].includes(squareValue)) { + board[row][column] = squareValue === 'B' ? 'BF' : 'F'; + + flagCount -= 1; + renderFlagsCount(); + + const squareElement = document.getElementById(`${row}-${column}`); + + squareElement.innerHTML = squareTexts[squareValue === 'B' ? 'BF' : 'F']; + squareElement.className = 'square ' + squareClassNames[squareValue === 'BF' ? 'B' : '.']; + } else if (['BF', 'F'].includes(squareValue)) { + board[row][column] = squareValue === 'BF' ? 'B' : '.'; + + flagCount += 1; + renderFlagsCount(); + + const squareElement = document.getElementById(`${row}-${column}`); + + squareElement.innerHTML = squareTexts[squareValue === 'BF' ? 'B' : '.']; + squareElement.className = 'square ' + squareClassNames[squareValue === 'BF' ? 'B' : '.']; + } +} + +function loseGame() { + const bombPositions = []; + + for (let i = 0; i < size[0]; i++) { + for (let j = 0; j < size[1]; j++) { + if (['B', 'BF'].includes(board[i][j])) { + bombPositions.push(`${i}-${j}`); + } + } + } + + bombRevealInterval = setInterval(() => { + if (bombPositions.length == 0) { + clearInterval(bombRevealInterval); + } else { + const squareElement = document.getElementById(bombPositions.shift()); + + squareElement.innerHTML = squareTexts['X']; + squareElement.className = 'square ' + squareClassNames['X']; + } + }, 100); + + stopGame(false); +} + +function configureSquareClickEvent(square) { + const onClick = event => { + const row = parseInt(square.id.split('-')[0]); + const column = parseInt(square.id.split('-')[1]); + + if (event.which == 3) { + switchFlag(row, column); + } else { + if (isFirstClick) { + isFirstClick = false; + + generateMines(row, column); + } + + const squareValue = board[row][column]; + + switch (squareValue) { + case '.': + revealSquare(row, column); + + break; + case 'B': + loseGame(); + break; + } + } + } + + squareListeners[square.id] = onClick; + + square.addEventListener('mousedown', onClick); +} + +function renderBoard() { + const boardElement = document.getElementById('board'); + boardElement.innerHTML = ''; + + for (const row in board) { + const rowElement = document.createElement('div'); + + rowElement.classList.add('row'); + + for (const column in board[row]) { + const squareValue = board[row][column]; + const squareElement = document.createElement('div'); + + squareElement.id = `${row}-${column}`; + squareElement.className = `square ${squareClassNames[squareValue]}`; + squareElement.innerHTML = squareTexts[squareValue]; + + configureSquareClickEvent(squareElement); + + rowElement.appendChild(squareElement); + } + + boardElement.appendChild(rowElement); + } + + boardElement.classList.remove('hide'); + document.getElementById('board-header').classList.remove('hide'); +} + +function renderFlagsCount() { + const flagsCountContainerElement = document.getElementById('flags-count-container'); + const flagsCountElement = document.getElementById('flags-count'); + + flagsCountElement.innerText = flagCount; + flagsCountContainerElement.classList.remove('hide'); +} + +function renderResultText() { + const gameResult = document.getElementById('result'); + + gameResult.innerText = ''; + + document.getElementById('result-container').classList.add('hide'); +} + +function startTimer() { + timeInSeconds = 0; + + document.getElementById('timer-container').classList.remove('hide'); + const timer = document.getElementById('timer'); + + timer.innerText = '00:00'; + + if (timerInterval) { + clearInterval(timerInterval); + } + + timerInterval = setInterval(function() { + timeInSeconds += 1; + + const hours = Math.floor(timeInSeconds / 3600).toString(); + const minutes = Math.floor((timeInSeconds % 3600) / 60).toString(); + const seconds = (timeInSeconds % 60).toString(); + + let timerText = `${hours > 0 ? hours.padStart(2, '0') + ':' : ''}${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`; + + timer.innerText = timerText; + }, 1000); +} + +function loadGame() { + isFirstClick = true; + + initializeBoard(); + renderBoard(); + renderFlagsCount(); + renderResultText(); + startTimer(); +} + +document.getElementById('start-game').addEventListener('click', function() { + const difficulty = difficulties[document.getElementById('difficulty').value]; + + console.log(difficulty); + + size = difficulty['size']; + mines = difficulty['mines']; + flagCount = mines; + + if (bombRevealInterval) { + clearInterval(bombRevealInterval); + } + + loadGame(); +}); + +document.getElementById('board').addEventListener('contextmenu', function(e) { + e.preventDefault(); + + return false; +}); + +function randomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + + return Math.floor(Math.random() * (max - min)) + min; +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..57327ea --- /dev/null +++ b/style.css @@ -0,0 +1,192 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,500;0,700;1,400&display=swap'); + +* { + padding: 0; + margin: 0; + outline: 0; + box-sizing: border-box; +} + +body { + font: 400 16px Roboto, sans-serif; + background: radial-gradient(#777, #333); + color: #eee; + -webkit-font-smoothing: antialiased; +} + +input, button, textarea { + font: 400 18px Roboto, sans-serif; +} + +button { + cursor: pointer; +} + +#container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 100%; + height: 100vh; +} + +#difficulty-container { + display: flex; + align-items: center; + justify-content: space-evenly; + + margin-top: 16px; +} + +#difficulty { + margin-left: 8px; + padding: 4px; + border: 0; + border-radius: 5px; +} + +#board-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + margin: 16px 16px 0 16px; + background: black; + border-radius: 8px; +} + +#board-header { + display: flex; + align-items: center; + justify-content: space-between; + + width: 100%; + margin-top: 10px; + padding: 0 8px; +} + +#board { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + margin: 4px; + border: 2px solid black; +} + +#start-game { + margin-top: 16px; + padding: 4px 16px; + border: 0; + border-radius: 5px; + background: #0F0; + color: #555; + transition: all 1s ease; +} + +#start-game:hover { + transform: scale(1.1) perspective(1px) +} + +#start-game:active { + position: relative; + top: 4px; +} + +.row { + display: flex; + align-items: center; + justify-content: center; +} + +.square { + display: flex; + align-items: center; + justify-content: center; + + width: 26px; + height: 26px; + border-top: 1px solid black; + border-left: 1px solid black; + + color: #555; +} + +.row .square:first-child { + border-left: 0; +} + +.row:first-child .square { + border-top: 0; +} + +.square:hover { + background: #CCC; + cursor: pointer; +} + +.unopened { + background: #AAA; +} + +.opened { + background: #EEE; +} + +.bomb { + background: #E00; +} + +.bomb img { + width: 20px; + height: 20px; + color: #FFF; +} + +.one { + color: #00F; +} + +.two { + color: #070; +} + +.three { + color: #F00; +} + +.four { + color: #007; +} + +.five { + color: #700; +} + +.six { + color: #077; +} + +.seven { + color: #000; +} + +.eight { + color: #777; +} + +.win { + color: #0D0; +} + +.defeat { + color: #D00; +} + +.hide { + display: none !important; +} \ No newline at end of file