initial commit

This commit is contained in:
Ayo 2019-12-08 16:36:29 +08:00
commit fded93a525
21 changed files with 5384 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
dist/
*.*~
*.*swp

43
ayo.html Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
html,
body {
background: black;
}
</style>
</head>
<body>
<div style="max-width: 270px; margin: 0px auto;">
<h3 style="border-bottom: 1px solid rgb(192, 192, 192); padding-bottom: 10px;">Best Times (beginner)</h3>
<ol style="list-style: none; text-align: left; margin-left: -40px; margin-top: -15px;">
<li>#1: <em>isji</em> 02.0</li>
<li>#2: <em>jenn</em> 04.1</li>
<li>#3: <em>isji</em> 06.5</li>
<li>#4: <em>isji</em> 06.9</li>
<li>#5: <em>isji</em> 06.9</li>
<li>#6: <em>Andi</em> 07.5</li>
<li>#7: <em>Kardz</em> 08.0</li>
<li>#8: <em>Kardz</em> 08.2</li>
<li>#9: <em>isji</em> 08.2</li>
<li>#10: <em>isji</em> 08.4</li>
</ol>
</div>
<button onclick="updateLeaders()">Update Leaders</button>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-firestore.js"></script>
<script type=module src="ayo.js"></script>
</body>
</html>

7
ayo.js Normal file
View file

@ -0,0 +1,7 @@
import { LeaderBoardService } from '../services/leader-board.service.js';
const leaderBoard = new LeaderBoardService('mw-leaders', 'mw-all');
window.updateLeaders = () => {
leaderBoard.updateTimeStampsLeaders();
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

50
index.html Normal file
View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!-- Tell the browser not to cache -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="Description" content="Play Minesweeper online for FREE!" />
<title>Minesweeper</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-113797180-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-113797180-1');
</script>
<link rel="stylesheet" href="/main.css" />
<link rel="stylesheet" href="/services/loading/loading.css" />
<link rel="shortcut icon" type="image/png" href="/favicon.ico">
</head>
<body>
<div id="body-wrapper">
<div id="app">
Please use Chrome or Firefox. Sorry for the inconvenience. Please buy me coffee.
<br />
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
<br />
<br />
</div>
<script type='text/javascript' src='https://ko-fi.com/widgets/widget_2.js'></script><script type='text/javascript'>kofiwidget2.init('Buy me a coffee', '#29abe0', 'ayoayco');kofiwidget2.draw();</script>
</div>
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-firestore.js"></script>
<script type="module" src="/dist/main.js"></script>
</body>
</html>

11
instructions Normal file
View file

@ -0,0 +1,11 @@
<div id="instructions" class="hint-wrapper">
<h1 class="pointer">Instructions</h1>
<ol class="body instructions">
<li>Clicking a cell which doesn't have a bomb reveals the number of surrounding bombs. Use this information plus some guess work to avoid opening the bombs.</li>
<li>To open a cell, click on it. To flag a cell you think is a bomb, right-click.</li>
</ol>
</div>
<div id="pro-tip" class="hint-wrapper">
<h1 class="pointer">Pro Tip</h1>
<span class="body hint">Clicking an open cell that has the correct number of flagged neighboring bombs will open all remaining unopened neighbor cells all at once. If an incorrect number of neighbors are flagged, or all neighbors are flagged or open, clicking the cell has no effect. If an incorrect neighbor is flagged, this will cause instant death.</span>
</div>

229
main.css Normal file
View file

@ -0,0 +1,229 @@
/*
initial code from: https://codepen.io/101Computing/pen/wEbEqx
*/
/* helpers */
.float-left {
float: left;
}
.float-right {
float: right;
}
.clear-both {
clear: both;
}
body {
background: black;
color: #DDDDDD;
font-family: medium-content-sans-serif-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif !important;
text-align: center;
}
em {
font-weight: bold;
}
h1 {
text-align: center;
font-size: 24px;
font-weight: bold;
text-transform: uppercase;
background: -webkit-linear-gradient(90deg,#ff8a00,#e52e71);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#grid {
margin-left: auto;
margin-right: auto;
border-spacing: 0px;
border: 4px solid #c0c0c0;
background: #c0c0c0;
font-family: courier new;
}
#grid tr td span {
font-size: 16px;
font-weight: bold;
position: absolute;
margin: -8.5px 0 0 -4.5px;
}
#grid tr td {
width: 15px;
height: 15px;
max-width: 15px;
max-height: 15px;
min-width: 15px;
min-height: 15px;
text-align: center;
font-size: xx-small;
cursor: default !important;
padding: 0px;
background: #c0d0c0;
border: 1px solid #c0c0c0;
-moz-user-select: none; -webkit-user-select: none; -ms-user-select:none; user-select:none;-o-user-select:none;
}
#grid TR TD[data-status=default],
#grid TR TD[data-status=flagged] {
border: 1px outset #ececec;
padding: 0px;
}
#grid TR TD.mine {
background: #dd5050;
border: 1px inset orange;
padding: 0px;
}
/*
#0000ff, #008100, #ff1300, #000083, #810500, #2a9494, #000000, #808080;
*/
#grid TR TD[data-value="1"] {
color: #0000ff !important;
}
#grid TR TD[data-value="2"] {
color: #008100;
}
#grid TR TD[data-value="3"] {
color: #ff1300;
}
#grid TR TD[data-value="4"] {
color: #000083;
}
#grid TR TD[data-value="5"] {
color: #810500;
}
#grid TR TD[data-value="6"] {
color: #2a9494;
}
#grid TR TD[data-value="7"] {
color: #000000;
}
#grid TR TD[data-value="8"] {
color: #808080;
}
#grid TR TD.flag {
background: #80d080;
}
#grid TR TD.wrong span,
#grid TR TD.correct span {
font-size: xx-small !important;
}
#grid TR TD.wrong {
background: orange;
color: #FF0000;
}
#grid TR TD.correct {
background: #00FF00 !important;
color: #000000;
}
div.hint-wrapper {
max-width: 500px;
padding: 0 30px;
margin: 0 auto 30px;
}
span.hint,
ol.instructions li {
font-size: small;
text-align: left !important;
}
button {
margin: 12px;
}
/* The Modal (background) */
.modal {
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content/Box */
.modal-content {
background: #efffef;
margin: 5% auto 0; /* 15% from the top and centered */
padding: 20px;
border: 5px solid #c0e0d0;
width: 60%; /* Could be more or less, depending on screen size */
box-shadow: 1px 1px 0px rgba(0, 0, 0, 0.2);
border-radius: 7px;
line-height: 36px!important;
color: #888;
}
.modal-btn {
box-shadow: 1px 1px 0px rgba(0, 0, 0, 0.2);
border-radius: 7px;
line-height: 36px!important;
min-width: 150px;
display: inline-block!important;
background-color: #29abe0;
padding: 2px 12px !important;
text-align: center !important;
color: #fff;
cursor: pointer;
overflow-wrap: break-word;
vertical-align: middle;
border: 0 none #fff !important;
font-family: 'Quicksand',Helvetica,Century Gothic,sans-serif !important;
text-decoration: none;
text-shadow: none;
font-weight: 700!important;
font-size: 14px !important;
}
p.announcement-action {
font-size: normal;
font-weight: bold;
}
#body-wrapper {
display: inline-block;
}
.btn-container {
position: fixed;
bottom: 30px;
right: 30px;
}
/** mobile **/
@media only screen and (max-width: 823px) {
#grid tr td {
width: 20px;
height: 20px;
max-width: 20px;
max-height: 20px;
min-width: 20px;
min-height: 20px;
}
.btn-container {
position: inherit;
margin: 20px 0;
}
}

3676
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

20
package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "mw",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "http-server .",
"watch:dev": "node .\\node_modules\\webpack\\bin\\webpack.js --watch --mode development .",
"buildprod": "node .\\node_modules\\webpack\\bin\\webpack.js ."
},
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^4.41.2"
},
"devDependencies": {
"webpack-cli": "^3.3.10"
}
}

17
services/db.service.js Normal file
View file

@ -0,0 +1,17 @@
export class DatabaseService {
constructor() {
// Your web app's Firebase configuration
const config = {
apiKey: "AIzaSyAbDzNHCSFh59e3r5sZA4_2ZHJnJ6SCCxM",
authDomain: "moment-188701.firebaseapp.com",
databaseURL: "https://moment-188701.firebaseio.com",
projectId: "secure-moment-188701",
storageBucket: "secure-moment-188701.appspot.com",
messagingSenderId: "113827947104",
appId: "1:113827947104:web:b176f746d8358302c51905"
};
// Initialize Firebase
firebase.initializeApp(config);
this.store = firebase.firestore();
}
}

View file

@ -0,0 +1,41 @@
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
export const DialogService = function() {
let isOpen = false;
let isInitialized = false;
const wrapper = document.createElement('div');
wrapper.className = 'dialog-wrapper';
const container = document.createElement('div');
container.className = 'dialog-container';
// add dialog wrapper and container elements
this.initialize = function() {
const bodyElement = document.getElementsByTagName('body')[0];
wrapper.appendChild(container);
bodyElement.appendChild(wrapper);
isInitialized = true;
}
this.promptMessage = function(message) {
isOpen = true;
}
this.closeDialog = function() {
if (isOpen) {
}
isOpen = false;
}
this.isInitialized = function() {
return isInitialized;
}
}

View file

@ -0,0 +1,164 @@
import { DatabaseService } from "./db.service.js";
import { TimerService } from "./timer.service.js";
import { UserService } from "./user.service.js";
import { LoadingService } from "./loading/loading.js";
const dbService = new DatabaseService();
const timerService = new TimerService();
const loadingService = new LoadingService();
const db = dbService.store;
const user = new UserService();
let previousLevel;
export class LeaderBoardService {
constructor(leaders, all) {
this.leaders = db.collection(leaders);
this.all = db.collection(all);
}
updateTimeStampsLeaders() {
const levels = ['beginner', 'intermediate', 'expert'];
levels.forEach(level => {
const collection = this.leaders.doc(level).collection('games');
collection.get()
.then(res => {
const levelArray = res.docs.map(doc => ({id: doc.id, ...doc.data()}))
// console.log(level+": ", levelArray);
levelArray.forEach(leaderGame => {
// const leaderGame = levelArray[0];
const leaderTime = leaderGame.time;
const browser = leaderGame.browserId;
this.all.doc(browser).collection('games')
.get().then(games => {
const allGames = games.docs.map(doc => ({id: doc.id, games: {...doc.data()}}));
console.log(level + '...........' + browser);
allGames.forEach(day => {
const keys = Object.keys(day.games);
const winningKeys = keys.filter(key => day.games[key].status === 'win');
winningKeys.forEach(key => {
const game = day.games[key];
const dateString = [day.id, key].join(' ').replace(/_/g, ' ');
const newGame = {time_stamp: new Date(dateString), ...leaderGame};
if (game.time === leaderTime) {
console.log('updated', newGame);
// collection.doc(leaderGame.id).get().then(res => console.log(res.data()));
collection.doc(leaderGame.id).set(newGame);
}
})
});
});
})
});
})
}
update(level, displayElement, title) {
if (level !== previousLevel) {
loadingService.addLoading(displayElement);
previousLevel = level;
if (this.unsubscribe) {
this.unsubscribe();
}
this.lastPlace = Number.MAX_SAFE_INTEGER;
this.topList = this.leaders.doc(level)
.collection('games').orderBy('time').limit(10);
this.unsubscribe = this.setListener(this.topList, displayElement, title);
}
}
renderList(displayElement, title, docs) {
if (!displayElement) return;
displayElement.innerHTML = '';
const leaderHeading = document.createElement('h3');
leaderHeading.innerText = title;
leaderHeading.style.borderBottom = '1px solid #c0c0c0';
leaderHeading.style.paddingBottom = '10px';
displayElement.style.maxWidth = '270px';
displayElement.style.margin = '0 auto';
const leaderList = document.createElement('div');
leaderList.innerHTML = '';
leaderList.style.listStyle = 'none';
leaderList.style.textAlign = 'left';
leaderList.style.marginTop = '-15px';
if (docs && docs.length) {
let i = 1;
docs.forEach(game => {
if (game) {
const prettyTime = timerService.pretty(game.data().time);
const name = game.data().name || 'Anonymous';
const item = document.createElement('div');
item.style.display = 'flex';
const nameElement =document.createElement('div'); // `<span class="ellipsis" title="${name}">${name}</span>` ;
nameElement.innerHTML = name;
nameElement.setAttribute('title', name);
nameElement.style.textOverflow = 'ellipsis';
nameElement.style.whiteSpace = 'nowrap';
nameElement.style.overflow = 'hidden';
nameElement.style.padding = '0 5px';
nameElement.style.cursor = 'pointer';
nameElement.style.fontWeight = 'bold';
nameElement.style.fontStyle = 'italic';
const indexElement = document.createElement('div');
indexElement.innerText = `#${i++}`;
const timeElement = document.createElement('div');
timeElement.innerText = prettyTime;
item.append(indexElement, nameElement, timeElement);
leaderList.append(item);
}
})
if (docs.length >= 10) {
this.lastPlace = docs[9].data().time;
}
displayElement.append(leaderHeading, leaderList);
} else {
const message = document.createElement('em');
message.innerText = 'Be the first to the top!';
displayElement.append(leaderHeading, message);
}
}
setListener(collection, displayElement, title) {
return collection.onSnapshot(list => this.renderList(displayElement, title, list.docs));
}
send(game, key) {
const sessionId = new Date().toDateString().replace(/\s/g, '_');
const gameId = new Date().toTimeString().replace(/\s/g, '_');
const data = {};
game = {
time_stamp: new Date(),
...game
}
data[gameId] = game;
this.all.doc(user.browserId).collection('games').doc(sessionId).set(data, {merge: true});
if (game.status === 'win' && game[key] < this.lastPlace) {
let name = window.prompt('Top performance! Enter your name:');
if (!name) {
name = 'Anonymous';
}
const newGame = {
name,
browserId: user.browserId,
...game
}
this.leaders.doc(game.level).collection('games').add(newGame);
}
}
}

View file

@ -0,0 +1,55 @@
.lds-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis div {
position: absolute;
top: 33px;
width: 13px;
height: 13px;
border-radius: 50%;
background: #fff;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}

View file

@ -0,0 +1,8 @@
export class LoadingService {
addLoading(element) {
element.innerHTML = '<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>';
}
removeLoading(element) {
element.innerHTML = '';
}
}

View file

@ -0,0 +1,21 @@
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
export class LoggerService {
debug(message, data) {
if (typeof message === 'string') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
} else {
console.warn(`LoggerService.debug expects a string as first parameter but got a ${typeof message}`, message);
}
}
}

View file

@ -0,0 +1,31 @@
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
export class StorageService {
constructor() {
}
saveToLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
saveToSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}
getFromLocal(key) {
const data = localStorage.getItem(key);
if (data !== 'undefined') return JSON.parse(data);
}
getFromSession(key) {
const data = sessionStorage.getItem(key);
if (data !== 'undefined') return JSON.parse(data);
}
}

72
services/timer.service.js Normal file
View file

@ -0,0 +1,72 @@
import { LoggerService } from "./logger.service.js";
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
const INTERVAL = 1;
export class TimerService {
constructor() {
this.loggerService = new LoggerService();
}
initialize(el) {
if (!el) return;
this.display = el;
this.startTime = undefined;
if (this.id) {
this.stop()
}
this.updateDisplay();
}
start() {
if (this.running || !this.display) return;
// run timer
this.running = true;
this.startTime = new Date().getTime();
this.id = window.setInterval(() => this.updateDisplay(), INTERVAL);
this.loggerService.debug(`started timer id: ${this.id}`);
}
stop() {
this.running = false;
clearInterval(this.id);
this.loggerService.debug(`stopped timer id: ${this.id}`);
this.id = undefined;
return this.time;
}
updateDisplay() {
let currentTime = new Date().getTime() - this.startTime;
this.time = Math.floor(currentTime / INTERVAL);
this.display.innerHTML = this.pretty(this.time) || '0';
}
pretty(duration) {
if (!duration) return undefined;
var milliseconds = parseInt((duration % 1000) / 100),
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? `0${hours}` : hours;
minutes = (minutes < 10) ? `0${minutes}` : minutes;
seconds = (seconds < 10) ? `0${seconds}` : seconds;
return `${this.clean(hours, ':')}${this.clean(minutes, ':')}${this.clean(seconds, '.')}${this.clean(milliseconds, '')}`;
}
clean(str, separator) {
return (str === '00') ? '' : `${str}${separator}`;
}
}

21
services/user.service.js Normal file
View file

@ -0,0 +1,21 @@
export class UserService {
constructor() {
if (!this.id) {
this.browserId = this.generateId();
}
}
generateId() {
var nav = window.navigator;
var screen = window.screen;
var guid = nav.mimeTypes.length;
guid += nav.userAgent.replace(/\D+/g, '');
guid += nav.plugins.length;
guid += screen.height || '';
guid += screen.width || '';
guid += screen.pixelDepth || '';
return guid;
}
}

13
src/index.js Normal file
View file

@ -0,0 +1,13 @@
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
import { Minesweeper } from './minesweeper.js';
/** start the game **/
const myMinesweeper = new Minesweeper();
myMinesweeper.initialize();

27
src/levels.js Normal file
View file

@ -0,0 +1,27 @@
export const levels = {
beginner: {
rows: 9,
cols: 9,
mines: 10,
name: 'beginner'
},
intermediate: {
rows: 16,
cols: 16,
mines: 40,
name: 'intermediate'
},
expert: {
rows: 16,
cols: 30,
mines: 99,
name: 'expert'
},
nightmare: {
rows: 20,
cols: 30,
mines: 150,
name: 'nightmare'
}
}

874
src/minesweeper.js Normal file
View file

@ -0,0 +1,874 @@
/*
Author: Ayo Ayco
Email: ramon.aycojr@gmail.com
Website: AyoAyco.com
Blog: FullHacker.com
Live: games.fullhacker.com/minesweeper
*/
import { StorageService } from '../services/storage.service.js';
import { TimerService } from '../services/timer.service.js';
import { LoggerService } from '../services/logger.service.js';
import { LeaderBoardService } from '../services/leader-board.service.js';
import { levels } from './levels.js';
const VERSION = "0.3.6";
const MOBILE_BUSY_DELAY = 250;
const PC_BUSY_DELAY = 500;
const TEST_MODE = false;
export const Minesweeper = function() {
const _this = this;
const storageService = new StorageService();
const timerService = new TimerService();
const loggerService = new LoggerService();
const leaderBoard = new LeaderBoardService('mw-leaders', 'mw-all');
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('app');
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() {
appElement.innerHTML = '';
const heading = `Minesweeper v${VERSION}`;
const headingElement = document.createElement('h1');
headingElement.innerText = heading;
const gameBoard = document.createElement('div');
gameBoard.setAttribute('id', 'game-board');
gameBoard.append(initializeToolbar(), grid, initializeFootbar());
appElement.append(headingElement, gameBoard);
generateGrid()
// appElement.append(gameWrapper);
}
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);
});
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<setting.cols; j++) {
let cell = row.insertCell(j);
initializeEventHandlers(cell);
if ('ontouchstart' in document.documentElement) {
isMobile = true;
initializeTouchEventHandlers(cell);
}
let status = document.createAttribute("data-status");
status.value = "default";
cell.setAttributeNode(status);
}
}
let gameStatus = document.createAttribute('game-status');
gameStatus.value = 'inactive';
grid.setAttributeNode(gameStatus);
appElement.style.minWidth = '260px';
appElement.style.width = `${grid.offsetWidth + 40}px`;
appElement.style.margin = '0 auto';
initializeLeaderBoard();
timerService.initialize(timerDisplay);
updateFlagsCountDisplay();
addMines(setting.mines);
}
function setBusy() {
isBusy = true;
if (isMobile) {
setTimeout(() => 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 = '&#128513;';
}
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) {
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) {
generateGrid();
}
resetMouseEventFlags();
}
window.onblur = function() {
resetMouseEventFlags();
}
grid.onmouseleave = function() {
removeHighlights();
}
document.oncontextmenu = () => false;
document.onmouseup = function() {
resetMouseEventFlags();
}
document.onmousedown = function(e) {
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<minesCount; i++) {
let row = Math.floor(Math.random() * setting.rows);
let col = Math.floor(Math.random() * setting.cols);
let cell = grid.rows[row].cells[col];
if (isMine(cell)) {
transferMine();
} else {
minesArray.push([row, col]);
}
if (TEST_MODE){
cell.innerHTML = 'X';
}
}
if (TEST_MODE) {
printMines();
}
}
function revealMines() {
if (grid.getAttribute('game-status') == 'done') return;
//Highlight all mines in red
const win = grid.getAttribute('game-status') == 'win';
for (let i=0; i<setting.rows; i++) {
for(let j=0; j<setting.cols; j++) {
let cell = grid.rows[i].cells[j];
if (win) {
handleWinRevelation(cell);
} else {
handleLostRevelation(cell);
}
}
}
grid.setAttribute('game-status', 'done');
const time = timerService.stop();
const game = {
time,
status: win ? 'win' : 'loss',
level: setting.name
}
leaderBoard.send(game, 'time');
// send google analytics event
if (gtag) {
gtag('event', 'mw-event', {
'event_category' : 'mw-game',
'event_label' : 'end-game'
});
}
}
function handleWinRevelation(cell) {
updateFlagsCountDisplay(0);
if (isMine(cell)) {
cell.innerHTML = ':)'
cell.className = 'correct';
setStatus(cell, 'clicked');
let correct = document.createAttribute('title');
correct.value = 'Correct';
cell.setAttributeNode(correct)
setStatus(cell, 'clicked');
}
}
function handleLostRevelation(cell) {
if (isFlagged(cell)) {
cell.className = 'flag'
if (!isMine(cell)) {
cell.innerHTML = 'X';
cell.className = 'wrong';
let wrong = document.createAttribute('title');
wrong.value = 'Wrong';
cell.setAttributeNode(wrong);
} else {
cell.innerHTML = ':)'
cell.className = 'correct';
let correct = document.createAttribute('title');
correct.value = 'Correct';
cell.setAttributeNode(correct);
}
} else {
if (isMine(cell)) {
cell.className = 'mine';
setStatus(cell, 'clicked');
}
}
}
function isOpen(cell) {
return cell.innerHTML !== '' && !isFlagged(cell);
}
function isFlagged(cell) {
return getStatus(cell) == 'flagged';
}
function isMine(cell) {
return getIndex(minesArray, cell) > -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<setting.rows; i++) {
for(let j=0; j<setting.cols; j++) {
const cell = grid.rows[i].cells[j];
if (!isMine(cell) && cell.innerHTML=="") levelComplete=false;
}
}
if (levelComplete && grid.getAttribute('game-status') == 'active') {
grid.setAttribute('game-status', 'win');
revealMines();
}
}
function setStatus(cell, status) {
cell.setAttribute('data-status', status);
}
function getCol(cell) {
return cell.cellIndex;
}
function getRow(cell) {
return cell.parentNode.rowIndex;
}
function getStatus(cell) {
if (!cell) return undefined;
return cell.getAttribute('data-status');
}
function middleClickCell(cell) {
if (grid.getAttribute('game-status') != 'active' || getStatus(cell) !== 'clicked') {
return;
}
// check for number of surrounding flags
const valueString = cell.getAttribute('data-value');
let cellValue = parseInt(valueString, 10);
let flagCount = countFlagsAround(cell);
if (flagCount === cellValue) {
clickSurrounding(cell);
if (TEST_MODE) loggerService.debug('middle click', cell);
}
}
function countFlagsAround(cell) {
let flagCount = 0;
let cellRow = cell.parentNode.rowIndex;
let cellCol = cell.cellIndex;
for (let i = Math.max(cellRow-1,0); i <= Math.min(cellRow+1, setting.rows - 1); i++) {
for(let j = Math.max(cellCol-1,0); j <= Math.min(cellCol+1, setting.cols - 1); j++) {
if (isFlagged(grid.rows[i].cells[j])) flagCount++;
}
}
return flagCount;
}
function clickSurrounding(cell) {
if (grid.getAttribute('game-status') != 'active') return;
let cellRow = cell.parentNode.rowIndex;
let cellCol = cell.cellIndex;
for (let i = Math.max(cellRow-1,0); i <= Math.min(cellRow+1, setting.rows - 1); i++) {
for(let j = Math.max(cellCol-1,0); j <= Math.min(cellCol+1, setting.cols - 1); j++) {
let currentCell = grid.rows[i].cells[j];
if (getStatus(currentCell) == 'flagged') continue;
openCell(currentCell);
}
}
}
function increaseFlagsCount() {
flagsCount++;
updateFlagsCountDisplay();
}
function decreaseFlagsCount() {
flagsCount--;
updateFlagsCountDisplay();
}
function activateGame() {
grid.setAttribute('game-status', 'active');
// start timer
timerService.start();
// send google analytics event
if (gtag) {
gtag('event', 'mw-event', {
'event_category' : 'mw-game',
'event_label' : 'start-game'
});
}
}
function gameIsDone() {
return grid.getAttribute('game-status') == 'over' || grid.getAttribute('game-status') == 'done';
}
function removeHighlights() {
for (let i=0; i<setting.rows; i++) {
const rows = grid.rows[i];
if (!rows) continue;
for(let j=0; j<setting.cols; j++) {
let currentCell = grid.rows[i].cells[j];
if (getStatus(currentCell) == 'highlighted') setStatus(currentCell, 'default');
}
}
}
function highlightCell(cell) {
if (isFlagged(cell)) return;
if (!gameIsDone() && getStatus(cell) == 'default') setStatus(cell, 'highlighted'); // currentCell.classList.add('highlight');
}
function highlightSurroundingCell(cell) {
let cellRow = cell.parentNode.rowIndex;
let cellCol = cell.cellIndex;
highlightCell(cell);
for (let i = Math.max(cellRow-1,0); i <= Math.min(cellRow+1, setting.rows - 1); i++) {
for(let j = Math.max(cellCol-1,0); j <= Math.min(cellCol+1, setting.cols - 1); j++) {
let currentCell = grid.rows[i].cells[j];
highlightCell(currentCell);
}
}
}
function rightClickCell(cell) {
if (isFlagged(cell)) setBusy();
if (grid.getAttribute('game-status') == 'inactive') {
activateGame();
}
if (grid.getAttribute('game-status') != 'active') return;
if (getStatus(cell) != 'clicked' && getStatus(cell) != 'empty') {
if (getStatus(cell) == 'default' || getStatus(cell) == 'highlighted') {
if (flagsCount <= 0) return;
cell.className = 'flag';
decreaseFlagsCount();
setStatus(cell, 'flagged');
} else {
cell.className = '';
increaseFlagsCount();
setStatus(cell, 'default');
}
if (TEST_MODE) loggerService.debug('right click', cell);
}
}
function clickCell(cell) {
if (isFlagged(cell)) setBusy();
if (grid.getAttribute('game-status') == 'inactive') {
activateGame();
}
if (grid.getAttribute('game-status') != 'active') return;
//Check if the end-user clicked on a mine
if (TEST_MODE) loggerService.debug('click', cell);
if (getStatus(cell) == 'flagged' || grid.getAttribute('game-status') == 'over') {
return;
} else if (getStatus(cell) == 'clicked') {
middleClickCell(cell);
return
} else if (isMine(cell) && firstClick) {
// cell.setAttribute('data-mine', 'false');
removeItem(minesArray, cell);
transferMine(cell);
if (TEST_MODE) printMines();
}
openCell(cell);
}
function printMines() {
let count = 0;
for (let i = 0; i < setting.rows; i++) {
for (let j = 0; j < setting.cols; j++) {
if (isMine(grid.rows[i].cells[j])) {
loggerService.debug(count++ + ' - mine: [' + i + ',' + j + ']');
}
}
}
}
function transferMine(cell = undefined) {
let found = false;
do {
let row = Math.floor(Math.random() * setting.rows);
let col = Math.floor(Math.random() * setting.cols);
const transferMineToCell = grid.rows[row].cells[col];
if (isMine(transferMineToCell) || isNeighbor(cell, transferMineToCell)) {
continue;
} else {
minesArray.push([row, col]);
if (TEST_MODE){
transferMineToCell.innerHTML = 'X';
if (TEST_MODE) loggerService.debug('transferred mine to: ' + row + ', ' + col);
}
found = true;
return;
}
} while(!found)
}
function isNeighbor(cell, nextCell) {
if (cell === undefined) {
return;
}
const rowDifference = Math.abs(getRow(cell) - getRow(nextCell));
const colDifference = Math.abs(getCol(cell) - getCol(nextCell));
return (rowDifference === 1) && (colDifference === 1);
}
function countMinesAround(cell) {
let mineCount=0;
let cellRow = cell.parentNode.rowIndex;
let cellCol = cell.cellIndex;
for (let i = Math.max(cellRow-1,0); i <= Math.min(cellRow+1,setting.rows-1); i++) {
const rows = grid.rows[i];
if (!rows) continue;
for(let j = Math.max(cellCol-1,0); j <= Math.min(cellCol+1,setting.cols-1); j++) {
const cell = rows.cells[j];
const mine = isMine(cell);
if (cell && mine) {
mineCount++;
}
}
}
return mineCount;
}
function updateCellValue(cell, value) {
const spanElement = document.createElement('span');
spanElement.innerHTML = value;
cell.innerHTML = '';
cell.appendChild(spanElement);
}
function handleEmpty(cell) {
updateCellValue(cell, ' ');
let cellRow = cell.parentNode.rowIndex;
let cellCol = cell.cellIndex;
setStatus(cell, 'empty');
//Reveal all adjacent cells as they do not have a mine
for (let y = Math.max(cellRow-1,0); y <= Math.min(cellRow+1, setting.rows - 1); y++) {
const rows = grid.rows[y];
if (!rows) continue;
for(let x = Math.max(cellCol-1,0); x <= Math.min(cellCol+1, setting.cols - 1); x++) {
//Recursive Call
const cell = rows.cells[x];
if (cell && !isOpen(cell)) {
clickCell(cell);
}
}
}
}
function openCell(cell) {
if (grid.getAttribute('game-status') != 'active') return;
cell.className="clicked";
setStatus(cell, 'clicked');
firstClick = false;
if (isMine(cell)) {
revealMines();
flagsDisplay.innerHTML = '&#128561;';
grid.setAttribute('game-status', 'over');
} else {
const mineCount = countMinesAround(cell);
if (mineCount==0) {
handleEmpty(cell);
} else {
updateCellValue(cell, mineCount.toString());
const dataValue = document.createAttribute('data-value');
dataValue.value = mineCount;
cell.setAttributeNode(dataValue);
}
//Count and display the number of adjacent mines
checkLevelCompletion();
}
}
}