feat: initial npm package @ayo-run/mnswpr
This commit is contained in:
parent
aea4ac2518
commit
304557163b
10 changed files with 342 additions and 1182 deletions
|
|
@ -19,6 +19,10 @@
|
||||||
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="./src/main.js"></script>
|
<script type="module">
|
||||||
|
import MineSweeper from './src/main.js'
|
||||||
|
const mnswpr = new MineSweeper('app', 'dev')
|
||||||
|
mnswpr.initialize()
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
17
package.json
17
package.json
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@ayco/mnswpr",
|
"name": "@ayo-run/mnswpr",
|
||||||
"version": "0.4.14",
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
"description": "Classic Minesweeper browser game",
|
"description": "Classic Minesweeper browser game",
|
||||||
"author": "Ayo Ayco",
|
"author": "Ayo",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -14,22 +15,22 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
|
||||||
"build:preview": "npm run build && npm run preview",
|
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"release": "bumpp && node scripts/release.js"
|
"release": "bumpp && node scripts/release.js"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
|
||||||
"firebase": "^12.11.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/css": "^1.1.0",
|
"@eslint/css": "^1.1.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"bumpp": "^11.0.1",
|
"bumpp": "^11.0.1",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^10.1.0",
|
"eslint": "^10.1.0",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
|
|
||||||
1243
pnpm-lock.yaml
1243
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
packages:
|
||||||
|
- "package"
|
||||||
|
|
@ -2,5 +2,4 @@ import './modules/loading/loading.css'
|
||||||
import './mnswpr.css'
|
import './mnswpr.css'
|
||||||
import Minesweeper from './mnswpr.js'
|
import Minesweeper from './mnswpr.js'
|
||||||
|
|
||||||
const myMinesweeper = new Minesweeper('app')
|
export default Minesweeper
|
||||||
myMinesweeper.initialize()
|
|
||||||
|
|
@ -1,32 +1,26 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LeaderBoardService,
|
|
||||||
LoggerService,
|
LoggerService,
|
||||||
LoadingService,
|
|
||||||
StorageService,
|
StorageService,
|
||||||
TimerService
|
TimerService
|
||||||
} from './modules/index.js'
|
} from './modules/index.js'
|
||||||
import { levels } from './levels.js'
|
import { levels } from './levels.js'
|
||||||
|
|
||||||
import * as pkg from '../package.json'
|
const TEST_MODE = false // set to true if you want to test the game with visual hints
|
||||||
|
|
||||||
const TEST_MODE = false // set to true if you want to test the game with visual hints and separate leaderboard
|
|
||||||
const VERSION = import.meta.env.MODE === 'development' ? 'dev' : `v${pkg.version}`
|
|
||||||
const MOBILE_BUSY_DELAY = 250
|
const MOBILE_BUSY_DELAY = 250
|
||||||
const PC_BUSY_DELAY = 500
|
const PC_BUSY_DELAY = 500
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create Minesweeper game board
|
* Create Minesweeper game board
|
||||||
* @param {String} appId
|
* @param {String} appId
|
||||||
|
* @param {String} version
|
||||||
*/
|
*/
|
||||||
export const Minesweeper = function(appId) {
|
export const Minesweeper = function(appId, version) {
|
||||||
const _this = this
|
const _this = this
|
||||||
const storageService = new StorageService()
|
const storageService = new StorageService()
|
||||||
const timerService = new TimerService()
|
const timerService = new TimerService()
|
||||||
const loggerService = new LoggerService()
|
const loggerService = new LoggerService()
|
||||||
const leaderBoardService = new LeaderBoardService()
|
|
||||||
const loadingService = new LoadingService()
|
|
||||||
|
|
||||||
let grid = document.createElement('table')
|
let grid = document.createElement('table')
|
||||||
grid.setAttribute('id', 'grid')
|
grid.setAttribute('id', 'grid')
|
||||||
|
|
@ -78,8 +72,8 @@ export const Minesweeper = function(appId) {
|
||||||
const headingElement = document.createElement('h1')
|
const headingElement = document.createElement('h1')
|
||||||
const gameBoard = document.createElement('div')
|
const gameBoard = document.createElement('div')
|
||||||
|
|
||||||
headingElement.innerHTML = `<span>Minesweeper</span><sup>${VERSION}</sup>`
|
headingElement.innerHTML = `<span>Minesweeper</span><sup>${version}</sup>`
|
||||||
document.title = `mnswpr [${VERSION}]`
|
document.title = `mnswpr [${version}]`
|
||||||
gameBoard.setAttribute('id', 'game-board')
|
gameBoard.setAttribute('id', 'game-board')
|
||||||
gameBoard.append(initializeToolbar(), grid, initializeFootbar())
|
gameBoard.append(initializeToolbar(), grid, initializeFootbar())
|
||||||
if(appElement) {
|
if(appElement) {
|
||||||
|
|
@ -87,7 +81,7 @@ export const Minesweeper = function(appId) {
|
||||||
appElement.append(headingElement, gameBoard)
|
appElement.append(headingElement, gameBoard)
|
||||||
appElement.append(initializeSourceLink())
|
appElement.append(initializeSourceLink())
|
||||||
}
|
}
|
||||||
generateGrid(true)
|
generateGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeSourceLink() {
|
function initializeSourceLink() {
|
||||||
|
|
@ -100,22 +94,6 @@ export const Minesweeper = function(appId) {
|
||||||
return sourceLink
|
return sourceLink
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeLeaderBoard() {
|
|
||||||
const title = `Best Times (${setting.name})`
|
|
||||||
|
|
||||||
const previousLeaderBoard = document.getElementById('leaderboard')
|
|
||||||
let loading = document.createElement('div')
|
|
||||||
loadingService.addLoading(loading)
|
|
||||||
if (previousLeaderBoard)
|
|
||||||
appElement?.replaceChild(loading, previousLeaderBoard)
|
|
||||||
else
|
|
||||||
appElement?.append(loading)
|
|
||||||
|
|
||||||
const leaderBoard = await leaderBoardService.update(setting.id ?? setting.name, title)
|
|
||||||
leaderBoard.id = 'leaderboard'
|
|
||||||
appElement?.replaceChild(leaderBoard, loading)
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeFootbar() {
|
function initializeFootbar() {
|
||||||
const footBar = document.createElement('div')
|
const footBar = document.createElement('div')
|
||||||
|
|
||||||
|
|
@ -188,11 +166,11 @@ export const Minesweeper = function(appId) {
|
||||||
function updateSetting(key) {
|
function updateSetting(key) {
|
||||||
setting = levels[key]
|
setting = levels[key]
|
||||||
storageService.saveToLocal('setting', setting)
|
storageService.saveToLocal('setting', setting)
|
||||||
generateGrid(true)
|
generateGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function generateGrid(initial = false) {
|
function generateGrid() {
|
||||||
firstClick = true
|
firstClick = true
|
||||||
grid.innerHTML = ''
|
grid.innerHTML = ''
|
||||||
grid.oncontextmenu = () => false
|
grid.oncontextmenu = () => false
|
||||||
|
|
@ -225,8 +203,12 @@ export const Minesweeper = function(appId) {
|
||||||
appElement.style.margin = '0 auto'
|
appElement.style.margin = '0 auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initial)
|
/**
|
||||||
initializeLeaderBoard()
|
* TODO: add hook afterGridGenerated
|
||||||
|
* - for initializing the leaderboard
|
||||||
|
*/
|
||||||
|
console.log('[hook]: after grid generated')
|
||||||
|
|
||||||
|
|
||||||
timerService.initialize(timerDisplay)
|
timerService.initialize(timerDisplay)
|
||||||
updateFlagsCountDisplay()
|
updateFlagsCountDisplay()
|
||||||
|
|
@ -488,9 +470,11 @@ export const Minesweeper = function(appId) {
|
||||||
isMobile
|
isMobile
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TEST_MODE) {
|
/**
|
||||||
leaderBoardService.send(game, 'time')
|
* TODO: add hook after gameSession send back `game`
|
||||||
}
|
* - for sending the game score to the db
|
||||||
|
*/
|
||||||
|
console.log('[hook]: after game session', game)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
export * from './leader-board/leader-board.js'
|
|
||||||
export * from './loading/loading.js'
|
export * from './loading/loading.js'
|
||||||
export * from './logger/logger.js'
|
export * from './logger/logger.js'
|
||||||
export * from './storage/storage.js'
|
export * from './storage/storage.js'
|
||||||
export * from './timer/timer.js'
|
export * from './timer/timer.js'
|
||||||
export * from './user/user.js'
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import { TimerService } from '../timer/timer'
|
|
||||||
import { UserService } from '../user/user'
|
|
||||||
import { LoggerService } from '../logger/logger'
|
|
||||||
|
|
||||||
import { initializeApp } from 'firebase/app'
|
|
||||||
import {
|
|
||||||
getFirestore, doc, getDocs, getDoc, setDoc, collection, query, orderBy, limit
|
|
||||||
} from 'firebase/firestore/lite'
|
|
||||||
|
|
||||||
|
|
||||||
export class LeaderBoardService {
|
|
||||||
|
|
||||||
timerService = new TimerService()
|
|
||||||
loggerService = new LoggerService()
|
|
||||||
user = new UserService()
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Create the Leader Board service
|
|
||||||
* @param {String} leaders
|
|
||||||
* @param {String} all
|
|
||||||
* @param {String} configuration
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
|
|
||||||
// necessary keys to interact with firebase
|
|
||||||
// not a secret
|
|
||||||
// https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public/37484053#37484053
|
|
||||||
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
|
||||||
const config = {
|
|
||||||
apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ',
|
|
||||||
authDomain: 'moment-188701.firebaseapp.com',
|
|
||||||
databaseURL: 'https://moment-188701.firebaseio.com',
|
|
||||||
projectId: 'secure-moment-188701',
|
|
||||||
storageBucket: 'secure-moment-188701.firebasestorage.app',
|
|
||||||
messagingSenderId: '113827947104',
|
|
||||||
appId: '1:113827947104:web:b176f746d8358302c51905',
|
|
||||||
measurementId: 'G-LZRDY0TG46'
|
|
||||||
}
|
|
||||||
const app = initializeApp(config)
|
|
||||||
this._store = getFirestore(app)
|
|
||||||
|
|
||||||
const configRef = doc(this.store, 'mw-config', 'configuration')
|
|
||||||
getDoc(configRef)
|
|
||||||
.then(res => {
|
|
||||||
this.configuration = res.data()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
get store() {
|
|
||||||
return this._store
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(level, title) {
|
|
||||||
const displayElement = document.createElement('div')
|
|
||||||
|
|
||||||
this.lastPlace = Number.MAX_SAFE_INTEGER
|
|
||||||
|
|
||||||
const q = query(
|
|
||||||
collection(this.store, 'mw-leaders', level, 'games'),
|
|
||||||
orderBy('time'),
|
|
||||||
limit(10)
|
|
||||||
)
|
|
||||||
this.topListSnapshot = await getDocs(q)
|
|
||||||
this.renderList(displayElement, title, this.topListSnapshot.docs)
|
|
||||||
|
|
||||||
return displayElement
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = this.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')
|
|
||||||
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'
|
|
||||||
// nameElement.onmousedown = () => console.log(game.data());
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async send(game, key) {
|
|
||||||
const sessionId = new Date().toDateString().replace(/\s/g, '_')
|
|
||||||
const gameId = new Date().toTimeString().replace(/\s/g, '_')
|
|
||||||
const data = { }
|
|
||||||
data[gameId] = game
|
|
||||||
|
|
||||||
const sessionRef = doc(this.store, 'mw-all', this.user.browserId, 'games', sessionId)
|
|
||||||
await setDoc(sessionRef, data, { merge: true })
|
|
||||||
|
|
||||||
if (this.configuration && game.status === this.configuration.passingStatus && game[key] < this.lastPlace) {
|
|
||||||
let name = window.prompt(this.configuration.message)
|
|
||||||
if (!name) {
|
|
||||||
name = 'Anonymous'
|
|
||||||
}
|
|
||||||
|
|
||||||
const newGame = {
|
|
||||||
name,
|
|
||||||
browserId: this.user.browserId,
|
|
||||||
...game
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameScoreRef = doc(collection(this.store, 'mw-leaders', game.level, 'games'))
|
|
||||||
await setDoc(gameScoreRef, newGame)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurationPromt() {
|
|
||||||
if (!this.configuration) {
|
|
||||||
this.loggerService.debug('Failed to fetch server configuration. Please contact your developer.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
vite.config.js
Normal file
12
vite.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(import.meta.dirname, 'src/main.js'),
|
||||||
|
name: 'mnswpr',
|
||||||
|
fileName: 'mnswpr'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue