first commit

This commit is contained in:
Auric Vente 2024-07-31 23:43:40 -06:00
commit 40d4f315d0
14 changed files with 929 additions and 0 deletions

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2021 madprops
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.

5
README.md Normal file
View File

@ -0,0 +1,5 @@
[Live Demo Here](https://madprops.github.io/minesweeper/)
![](https://i.imgur.com/tHRnZb4.jpg)
![](https://i.imgur.com/EEs7g4E.jpg)

BIN
audio/click.mp3 Normal file

Binary file not shown.

BIN
audio/explosion.mp3 Normal file

Binary file not shown.

BIN
audio/start.mp3 Normal file

Binary file not shown.

BIN
audio/victory.mp3 Normal file

Binary file not shown.

164
css/style.css Normal file
View File

@ -0,0 +1,164 @@
:root {
--size: 720px;
--padding1: 5px;
--padding2: 10px;
--bgcolor1: rgb(69, 73, 103);
--bgcolor2: lightblue;
--bgcolor3: #a8c8d2;
--bgcolor4: #87c2f6;
--bgcolor5: rgb(209, 227, 233);
--bgcolor6: #456784;
}
body, html {
margin: 0;
padding: 0;
font-size: 16px;
height: 100%;
font-family: monospace;
}
#main {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: var(--bgcolor1);
overflow: hidden;
}
#top {
background-color: var(--bgcolor2);
width: var(--size);
font-size: 18px;
color: white;
}
#top > div {
padding: var(--padding1);
}
#levels {
display: flex;
flex-direction: row;
margin-bottom: 6px;
background-color: var(--bgcolor6);
user-select: none;
align-items: center;
justify-content: center;
}
#levels > div {
cursor: pointer;
padding: var(--padding1);
padding-left: var(--padding2);
padding-right: var(--padding2);
}
.level_selected {
text-decoration: underline;
}
#info {
display: flex;
flex-direction: row;
margin-bottom: 8px;
user-select: none;
background-color: var(--bgcolor6);
}
#time {
margin-left: auto;
cursor: pointer;
padding: var(--padding1);
}
#grid {
background-color: var(--bgcolor2);
width: var(--size);
height: var(--size);
position: relative;
user-select: none;
border-bottom-left-radius: 1%;
border-bottom-right-radius: 1%;
cursor: pointer;
}
.block {
box-shadow: 0 0 0.29rem rgb(95, 120, 189);
position: absolute;
background-color: var(--bgcolor5);
display: flex;
align-items: center;
justify-content: center;
border-radius: 33.3%;
}
.revealed {
background-color: var(--bgcolor3);
}
.block .number {
display: none;
color: white;
}
.revealed .number {
display: block;
}
.flag .number {
display: block;
}
.mine.revealed {
background-color: #cd8787
}
.minehit {
background-color: pink;
}
.flag {
background-color: var(--bgcolor4);
}
.mine.flag {
background-color: var(--bgcolor4);;
}
#mines {
padding: var(--padding1);
}
.minehit {
border: 5px #b34de2 solid;
box-sizing: border-box;
}
@keyframes bgchange {
0% {
background-color: var(--bgcolor1);
}
50% {
background-color: red;
}
100% {
background-color: var(--bgcolor1);
}
}
.boom {
animation: bgchange 500ms forwards;
}
.face {
width: 30px;
height: 30px;
cursor: pointer;
margin-right: 8px;
}

BIN
img/face_lost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
img/face_pressing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
img/face_waiting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
img/face_won.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

42
index.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Minesweeper</title>
<link rel="shortcut icon" href="img/face_won.png" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="css/style.css">
<script src="js/libs/dom.js"></script>
<script src="js/main/base.js"></script>
<script>
window.onload = function () {
App.init()
}
</script>
</head>
<body>
<div id="main">
<div id="top">
<div id="levels">
<div id="level_easy" data-level="easy">Easy</div>
<div id="level_normal" data-level="normal" class="level_selected">Normal</div>
<div id="level_hard" data-level="hard">Hard</div>
<div id="level_expert" data-level="expert">Expert</div>
</div>
<div id="info">
<img id="face" src="img/face_waiting.png" class="face">
<div id="mines"></div>
<div id="time"></div>
</div>
</div>
<div id="grid"></div>
</div>
<audio id="audio_explosion" src="audio/explosion.mp3"></audio>
<audio id="audio_click" src="audio/click.mp3"></audio>
<audio id="audio_victory" src="audio/victory.mp3"></audio>
<audio id="audio_start" src="audio/start.mp3"></audio>
</body>

125
js/libs/dom.js Normal file
View File

@ -0,0 +1,125 @@
// DOM v1.0.0
const DOM = {}
DOM.dataset_obj = {}
DOM.dataset_id = 0
// Select a single element
DOM.el = (query, root = document) => {
return root.querySelector(query)
}
// Select an array of elements
DOM.els = (query, root = document) => {
return Array.from(root.querySelectorAll(query))
}
// Select a single element or self
DOM.el_or_self = (query, root = document) => {
let el = root.querySelector(query)
if (!el) {
if (root.classList.contains(query.replace(`.`, ``))) {
el = root
}
}
return el
}
// Select an array of elements or self
DOM.els_or_self = (query, root = document) => {
let els = Array.from(root.querySelectorAll(query))
if (els.length === 0) {
if (root.classList.contains(query.replace(`.`, ``))) {
els = [root]
}
}
return els
}
// Clone element
DOM.clone = (el) => {
return el.cloneNode(true)
}
// Clone element children
DOM.clone_children = (query) => {
let items = []
let children = Array.from(DOM.el(query).children)
for (let c of children) {
items.push(DOM.clone(c))
}
return items
}
// Data set manager
DOM.dataset = (el, value, setvalue) => {
if (!el) {
return
}
let id = el.dataset.dataset_id
if (!id) {
id = DOM.dataset_id
DOM.dataset_id += 1
el.dataset.dataset_id = id
DOM.dataset_obj[id] = {}
}
if (setvalue !== undefined) {
DOM.dataset_obj[id][value] = setvalue
}
else {
return DOM.dataset_obj[id][value]
}
}
// Create an html element
DOM.create = (type, classes = ``, id = ``) => {
let el = document.createElement(type)
if (classes) {
let classlist = classes.split(` `).filter(x => x != ``)
for (let cls of classlist) {
el.classList.add(cls)
}
}
if (id) {
el.id = id
}
return el
}
// Add an event listener
DOM.ev = (element, event, callback, extra) => {
element.addEventListener(event, callback, extra)
}
// Add multiple event listeners
DOM.evs = (element, events, callback, extra) => {
for (let event of events) {
element.addEventListener(event, callback, extra)
}
}
// Like jQuery's nextAll
DOM.next_all = function* (e, selector) {
while (e = e.nextElementSibling) {
if (e.matches(selector)) {
yield e;
}
}
}
// Get item index
DOM.index = (el) => {
return Array.from(el.parentNode.children).indexOf(el)
}

586
js/main/base.js Normal file
View File

@ -0,0 +1,586 @@
const App = {}
App.level = `normal`
App.init = () => {
let style = getComputedStyle(document.body)
App.size = parseInt(style.getPropertyValue(`--size`))
App.main_el = DOM.el(`#main`)
App.grid_el = DOM.el(`#grid`)
App.mines_el = DOM.el(`#mines`)
App.time_el = DOM.el(`#time`)
App.levels_el = DOM.el(`#levels`)
App.explosion_fx = DOM.el(`#audio_explosion`)
App.click_fx = DOM.el(`#audio_click`)
App.victory_fx = DOM.el(`#audio_victory`)
App.start_fx = DOM.el(`#audio_start`)
App.face_el = DOM.el(`#face`)
App.start_events()
App.start_info()
App.start_levels()
App.prepare_game()
}
App.start_events = () => {
DOM.ev(App.grid_el, `contextmenu`, (e) => {
e.preventDefault()
})
DOM.ev(document, `visibilitychange`, () => {
if (document.hidden) {
App.pause()
}
},
)
DOM.ev(App.grid_el, `mousedown`, () => {
App.change_face(`pressing`)
})
DOM.ev(App.grid_el, `mouseup`, () => {
App.change_face(`waiting`)
})
DOM.ev(document, `keyup`, (e) => {
if (e.key === `Enter`) {
App.ask_restart()
}
else if (e.key === ` `) {
App.toggle_pause()
}
})
}
App.change_face = (s, force = false) => {
if (!force && App.over) {
return
}
App.face_el.src = `img/face_${s}.png`
}
App.prepare_game = () => {
App.game_started = false
App.check_level()
App.over = false
App.num_revealed = 0
App.num_clicks = 0
App.main_el.classList.remove(`boom`)
App.playing = true
App.num_mines = App.initial_mines
App.create_grid()
clearInterval(App.time_interval)
App.time = 0
App.update_info()
App.change_face(`waiting`)
}
App.create_grid = () => {
App.grid_el.innerHTML = ``
App.grid = []
let size = App.size / App.grid_size
let x = 0
let y = 0
let row = []
for (let xx = 0; xx < App.grid_size; xx++) {
for (let yy = 0; yy < App.grid_size; yy++) {
let block = document.createElement(`div`)
block.style.width = size + `px`
block.style.height = size + `px`
block.style.left = x + `px`
block.style.top = y + `px`
block.classList.add(`block`)
DOM.ev(block, `click`, () => {
App.onclick(xx, yy)
})
DOM.ev(block, `contextmenu`, (e) => {
App.flag(xx, yy)
e.preventDefault()
})
App.grid_el.append(block)
let item = {}
item.x = xx
item.y = yy
item.block = block
item.revealed = false
row.push(item)
x += size
}
App.grid.push(row)
row = []
x = 0
y += size
}
}
App.shuffle = (arr) => {
if (arr.length === 1) {
return arr
}
const rand = Math.floor(Math.random() * arr.length)
return [arr[rand], ...App.shuffle(arr.filter((_, i) => i != rand))]
}
App.start_game = (x, y) => {
if (App.game_started) {
return
}
let pairs = []
for (let xx = 0; xx < App.grid_size; xx++) {
for (let yy = 0; yy < App.grid_size; yy++) {
if (xx === x && yy === y) {
continue
}
if (xx >= x - 1 && xx <= x + 1) {
if (yy >= y - 1 && yy <= y + 1) {
continue
}
}
pairs.push([xx, yy])
}
}
let num = 0
for (let p of App.shuffle(pairs)) {
let item = App.grid[p[0]][p[1]]
item.mine = true
item.block.classList.add(`mine`)
num += 1
if (num >= App.num_mines) {
break
}
}
App.game_started = true
App.check_mines()
App.start_time()
App.playsound(App.start_fx)
}
App.check_mines = () => {
for (let x = 0; x < App.grid_size; x++) {
for (let y = 0; y < App.grid_size; y++) {
let number = 0
if (y > 0) {
if (App.grid[x][y - 1].mine) {
number += 1
}
if (x > 0) {
if (App.grid[x - 1][y - 1].mine) {
number += 1
}
}
if (x < App.grid_size - 1) {
if (App.grid[x + 1][y - 1].mine) {
number += 1
}
}
}
if (x > 0) {
if (App.grid[x - 1][y].mine) {
number += 1
}
}
if (x < App.grid_size - 1) {
if (App.grid[x + 1][y].mine) {
number += 1
}
}
if (y < App.grid_size - 1) {
if (App.grid[x][y + 1].mine) {
number += 1
}
if (x > 0) {
if (App.grid[x - 1][y + 1].mine) {
number += 1
}
}
if (x < App.grid_size - 1) {
if (App.grid[x + 1][y + 1].mine) {
number += 1
}
}
}
let item = App.grid[x][y]
item.number = number
let text = document.createElement(`div`)
text.classList.add(`number`)
if (item.mine) {
text.textContent = `💣️`
}
else if (number > 0) {
text.textContent = number
}
item.og_number = text.textContent
item.block.append(text)
}
}
}
App.random_int = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min)
}
App.onclick = (x, y) => {
if (App.over) {
return
}
if (!App.playing) {
App.unpause()
}
App.start_game(x, y)
let item = App.grid[x][y]
if (item.revealed) {
return
}
App.num_clicks += 1
if (item.mine) {
item.block.classList.add(`minehit`)
App.gameover(`explosion`)
return
}
else {
if (item.revealed) {
return
}
App.floodfill(x, y)
}
if (!App.check_status()) {
App.playsound(App.click_fx)
}
}
App.setnumber = (item, s) => {
DOM.el(`.number`, item.block).textContent = s
}
App.gameover = (mode) => {
App.over = true
App.playing = false
for (let row of App.grid) {
for (let item of row) {
if (item.mine) {
item.block.classList.add(`flag`)
App.setnumber(item, item.og_number)
}
}
}
if (mode === `won`) {
App.mines_el.textContent = `You cleared all the mines!`
App.playsound(App.victory_fx)
App.change_face(`won`, true)
}
else if (mode === `explosion`) {
App.mines_el.textContent = `You stepped on a mine!`
App.playsound(App.explosion_fx)
App.change_face(`lost`, true)
}
else if (mode === `timeout`) {
App.mines_el.textContent = `You ran out of time!`
App.playsound(App.explosion_fx)
App.change_face(`lost`, true)
}
if (mode !== `won`) {
App.main_el.classList.add(`boom`)
}
}
App.floodfill = (x, y) => {
let item = App.grid[x][y]
if (item.number > 0) {
App.reveal(x, y)
return
}
App.fill(x, y)
}
App.fill = (x, y) => {
if (x < 0) {
return
}
if (y < 0) {
return
}
if (x > App.grid.length - 1) {
return
}
if (y > App.grid[x].length - 1) {
return
}
let item = App.grid[x][y]
if (item.revealed) {
return
}
let cont = item.number === 0
if (!item.revealed) {
App.reveal(x, y)
}
if (cont) {
App.fill(x - 1, y)
App.fill(x + 1, y)
App.fill(x, y - 1)
App.fill(x, y + 1)
App.fill(x - 1, y + 1)
App.fill(x + 1, y - 1)
App.fill(x + 1, y + 1)
App.fill(x - 1, y - 1)
}
}
App.reveal = (x, y) => {
let item = App.grid[x][y]
if (item.flag) {
App.flag(x, y)
}
item.block.classList.add(`revealed`)
item.revealed = true
App.num_revealed += 1
}
App.flag = (x, y) => {
if (App.over) {
return
}
if (!App.playing) {
App.unpause()
}
App.start_game(x, y)
let item = App.grid[x][y]
if (item.revealed) {
return
}
item.flag = !item.flag
if (item.flag) {
item.block.classList.add(`flag`)
App.setnumber(item, ``)
App.num_mines -= 1
}
else {
item.block.classList.remove(`flag`)
App.setnumber(item, item.og_number)
App.num_mines += 1
}
App.update_mines()
}
App.update_mines = () => {
let s
if (App.num_mines === 1) {
s = `mines`
}
else {
s = `mines`
}
App.mines_el.textContent = `${App.num_mines} / ${App.initial_mines} ${s} (${App.grid_size} x ${App.grid_size})`
}
App.start_info = () => {
DOM.ev(App.face_el, `click`, () => {
App.ask_restart()
})
DOM.ev(App.time_el, `click`, () => {
App.toggle_pause()
})
}
App.update_info = () => {
App.update_mines()
App.update_time()
}
App.timestring = (n) => {
return n.toString().padStart(3, `0`)
}
App.update_time = () => {
App.time_el.textContent = `Time: ` + App.timestring(App.time) + ` / ` + App.timestring(App.max_time)
}
App.start_time = () => {
clearInterval(App.time_interval)
App.time_interval = setInterval(() => {
if (App.playing) {
App.time += 1
App.update_time()
if (App.time >= App.max_time) {
App.gameover(`timeout`)
}
}
}, 1000)
}
App.check_status = () => {
if (App.num_revealed == (App.grid_size * App.grid_size) - App.initial_mines) {
App.gameover(`won`)
return true
}
return false
}
App.playsound = (el) => {
el.pause()
el.currentTime = 0
el.play()
}
App.toggle_pause = () => {
if (App.playing) {
App.pause()
}
else {
App.unpause()
}
}
App.pause = () => {
if (App.over) {
return
}
if (!App.game_started) {
return
}
if (!App.playing) {
return
}
App.playing = false
App.time_el.textContent += ` (Paused)`
}
App.unpause = () => {
if (App.over) {
return
}
if (App.playing) {
return
}
App.playing = true
App.update_time()
}
App.start_levels = () => {
DOM.ev(App.levels_el, `click`, (e) => {
let level = e.target.dataset.level
if (level) {
if (level === App.level) {
App.ask_restart()
return
}
for (let div of DOM.els(`div`, App.levels_el)) {
div.classList.remove(`level_selected`)
if (div.dataset.level === level) {
div.classList.add(`level_selected`)
}
else {
div.classList.remove(`level_selected`)
}
}
App.level = level
App.ask_restart()
}
})
}
App.check_level = () => {
if (App.level === `easy`) {
App.initial_mines = 10
App.grid_size = 10
App.max_time = 100
}
else if (App.level === `normal`) {
App.initial_mines = 30
App.grid_size = 15
App.max_time = 300
}
else if (App.level === `hard`) {
App.initial_mines = 60
App.grid_size = 20
App.max_time = 600
}
else if (App.level === `expert`) {
App.initial_mines = 80
App.grid_size = 20
App.max_time = 300
}
}
App.ask_restart = () => {
if (!App.over) {
if (App.num_clicks > 1) {
if (confirm(`Restart Game?`)) {
App.prepare_game()
}
return
}
}
App.prepare_game()
}